Repository navigation
feat(core): add WordPress-style body classes #2689
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
7c944fd
feat(core): add WordPress-style body classes with WordPressTemplate's…
josephfusco 28834ce
docs(core): add bodyClass to every page that renders WordPressTemplate
josephfusco 826374f
fix(core): sanitize term slugs in body classes like WordPress does
josephfusco 382a302
docs(core): note that body class slugs are sanitized like WordPress
josephfusco 46ee2dc
fix(core): give a previewed posts page only the blog body class
josephfusco 66af6b9
fix(core): use the previewed post's ID in body classes and add logged…
josephfusco de339c3
test(core): reset the preview URL even when a body class test fails
josephfusco File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| --- | ||
| title: "useBodyClass" | ||
| description: "Reference for adding WordPress-style body classes like single-post or category-news to Faust.js pages." | ||
| --- | ||
|
|
||
| WordPress themes add classes like `home`, `single-post`, `postid-123` and `category-news` to `<body>`. Faust.js can add the same classes, so CSS ported from a theme keeps working. | ||
|
|
||
| ## Add body classes to every WordPress page | ||
|
|
||
| Pass `bodyClass` to `WordPressTemplate` on every page that renders it. In the getting-started example, that's `pages/index.js`, `pages/[...wordpressNode].js` and `pages/preview.js`: | ||
|
|
||
| ```js title="pages/[...wordpressNode].js" | ||
| export default function Page(props) { | ||
| return <WordPressTemplate {...props} bodyClass />; | ||
| } | ||
| ``` | ||
|
|
||
| The classes are worked out from the seed node Faust.js already loads, and they also work in previews. They're off unless you set `bodyClass`. | ||
|
|
||
| | Page | Classes | | ||
| | --- | --- | | ||
| | Static front page | `home`, plus its page classes | | ||
| | Posts page | `blog` | | ||
| | Single post or custom post type | `single`, `single-{post type}`, `postid-{id}` | | ||
| | Attachment | `single`, `single-attachment`, `postid-{id}`, `attachment`, `attachmentid-{id}` | | ||
| | Page | `page`, `page-id-{id}`, `page-template-default` or `page-template-{template name}` | | ||
| | Category | `archive`, `category`, `category-{slug}`, `category-{id}` | | ||
| | Tag | `archive`, `tag`, `tag-{slug}`, `tag-{id}` | | ||
| | Other taxonomy term | `archive`, `tax-{taxonomy}`, `term-{slug}`, `term-{id}` | | ||
| | Author | `archive`, `author`, `author-{id}` | | ||
| | Post type archive | `archive`, `post-type-archive`, `post-type-archive-{post type}` | | ||
|
|
||
| Previews also get `logged-in`, since you have to be logged in to see them. When the Faust toolbar is showing, it adds `admin-bar`, even without `bodyClass`. | ||
|
|
||
| These are a subset of what WordPress's `body_class()` adds. Page template classes use the template's name, such as `page-template-full-width` for "Full Width", rather than its file name, and filters on `body_class` don't apply. As in WordPress, characters that can't go in a class name are removed from slugs. | ||
|
|
||
| ## useBodyClass | ||
|
|
||
| To add your own classes, call the `useBodyClass` hook from any component. The classes are added while the component is mounted and removed when it unmounts or the list changes. | ||
|
|
||
| ```js | ||
| import { useBodyClass } from "@faustwp/core"; | ||
|
|
||
| export default function Landing() { | ||
| useBodyClass(["landing", "dark"]); | ||
|
|
||
| return <main>…</main>; | ||
| } | ||
| ``` | ||
|
|
||
| To get the WordPress classes yourself, for example to add them to an element other than `<body>`, call `getBodyClasses(seedNode)`. It returns them as an array. |
2 changes: 1 addition & 1 deletion
2
examples/next/faustwp-getting-started/pages/[...wordpressNode].js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,5 @@ | ||
| import { WordPressTemplate } from '@faustwp/core'; | ||
|
|
||
| export default function Preview(props) { | ||
| return <WordPressTemplate {...props} />; | ||
| return <WordPressTemplate {...props} bodyClass />; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| import { useEffect } from 'react'; | ||
|
|
||
| /** | ||
| * Add classes to the page's `<body>` while the component is mounted. | ||
| * | ||
| * The classes are removed when the component unmounts or the list changes. | ||
| * If two components add the same class, the first to remove it removes it for | ||
| * both. | ||
| * | ||
| * @param classes The classes to add. | ||
| */ | ||
| export function useBodyClass(classes: string[]): void { | ||
| // Compare by value, so a new array with the same classes doesn't re-run the effect. | ||
| const classNames = classes.filter(Boolean).join(' '); | ||
|
|
||
| useEffect(() => { | ||
| const list = classNames.split(' ').filter(Boolean); | ||
|
|
||
| if (list.length === 0) { | ||
| return undefined; | ||
| } | ||
|
|
||
| document.body.classList.add(...list); | ||
|
|
||
| return () => { | ||
| document.body.classList.remove(...list); | ||
| }; | ||
| }, [classNames]); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,121 @@ | ||
| import type { SeedNode } from '../queries/seedQuery.js'; | ||
|
|
||
| /** | ||
| * Make a slug safe to use in a class name, like WordPress's | ||
| * sanitize_html_class(): percent-encoded characters and anything other than | ||
| * letters, numbers, underscores and hyphens are removed. | ||
| */ | ||
| function sanitizeClass(value?: string): string { | ||
| return (value ?? '') | ||
| .replace(/%[a-fA-F0-9]{2}/g, '') | ||
| .replace(/[^A-Za-z0-9_-]/g, ''); | ||
| } | ||
|
|
||
| /** | ||
| * Turn a page template name like "Full Width" into "full-width". | ||
| */ | ||
| function toClassName(value: string): string { | ||
| return value | ||
| .toLowerCase() | ||
| .replace(/[^a-z0-9_-]+/g, '-') | ||
| .replace(/^-+|-+$/g, ''); | ||
| } | ||
|
|
||
| /** | ||
| * Get WordPress-style body classes for a page from its seed node. | ||
| * | ||
| * The classes follow the names WordPress's `body_class()` uses, such as | ||
| * `home`, `single-post`, `postid-123` and `category-news`, so CSS written for a | ||
| * WordPress theme keeps working. They're worked out from the seed node alone, | ||
| * so they're a subset of what WordPress adds, and page template classes use | ||
| * the template's name rather than its file name. | ||
| * | ||
| * @param seedNode The seed node for the current page. | ||
| * | ||
| * @returns The body classes. | ||
| */ | ||
| export function getBodyClasses(seedNode?: SeedNode | null): string[] { | ||
| if (!seedNode) { | ||
| return []; | ||
| } | ||
|
|
||
| const classes: string[] = []; | ||
| const id = seedNode.databaseId; | ||
| // eslint-disable-next-line no-underscore-dangle | ||
| const typeName = seedNode.__typename; | ||
|
|
||
| if (seedNode.isFrontPage) { | ||
| classes.push('home'); | ||
| } | ||
|
|
||
| // The posts page lists posts, so it gets no page or archive classes, even | ||
| // when a preview resolves it to a Page. | ||
| if (seedNode.isPostsPage) { | ||
| classes.push('blog'); | ||
|
|
||
| return classes; | ||
| } | ||
|
|
||
| if (seedNode.isContentNode) { | ||
| const postType = seedNode.contentType?.node?.name; | ||
| const templateName = seedNode.template?.templateName; | ||
|
|
||
| if (postType === 'page') { | ||
| classes.push('page', `page-id-${id}`); | ||
|
|
||
| if (templateName) { | ||
| classes.push( | ||
| templateName === 'Default' | ||
| ? 'page-template-default' | ||
| : `page-template-${toClassName(templateName)}`, | ||
| ); | ||
| } | ||
| } else if (postType) { | ||
| classes.push('single', `single-${postType}`, `postid-${id}`); | ||
|
|
||
| if (postType === 'attachment') { | ||
| classes.push('attachment', `attachmentid-${id}`); | ||
| } | ||
| } | ||
|
|
||
| return classes; | ||
| } | ||
|
|
||
| if (seedNode.isTermNode) { | ||
| const { taxonomyName } = seedNode; | ||
| const slug = sanitizeClass(seedNode.slug); | ||
| // Like WordPress, skip the slug class if nothing usable is left. | ||
| const termClasses = (prefix: string) => | ||
| slug ? [`${prefix}-${slug}`, `${prefix}-${id}`] : [`${prefix}-${id}`]; | ||
|
|
||
| if (taxonomyName === 'category') { | ||
| classes.push('archive', 'category', ...termClasses('category')); | ||
| } else if (taxonomyName === 'post_tag') { | ||
| classes.push('archive', 'tag', ...termClasses('tag')); | ||
| } else if (taxonomyName) { | ||
| classes.push( | ||
| 'archive', | ||
| `tax-${sanitizeClass(taxonomyName)}`, | ||
| ...termClasses('term'), | ||
| ); | ||
| } | ||
|
|
||
| return classes; | ||
| } | ||
|
|
||
| if (typeName === 'User') { | ||
| classes.push('archive', 'author', `author-${id}`); | ||
|
|
||
| return classes; | ||
| } | ||
|
|
||
| if (typeName === 'ContentType' && seedNode.name) { | ||
| classes.push( | ||
| 'archive', | ||
| 'post-type-archive', | ||
| `post-type-archive-${seedNode.name}`, | ||
| ); | ||
| } | ||
|
|
||
| return classes; | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.