Skip to content
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

feat(pages): add support for missing SEO front matter + improve SEO docs #9071

Merged
merged 6 commits into from
Jun 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions packages/docusaurus-plugin-content-pages/src/frontMatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,24 @@ import {
validateFrontMatter,
FrontMatterTOCHeadingLevels,
ContentVisibilitySchema,
URISchema,
} from '@docusaurus/utils-validation';
import type {FrontMatter} from '@docusaurus/plugin-content-pages';
import type {PageFrontMatter} from '@docusaurus/plugin-content-pages';

const PageFrontMatterSchema = Joi.object<FrontMatter>({
title: Joi.string(),
description: Joi.string(),
const PageFrontMatterSchema = Joi.object<PageFrontMatter>({
// See https://github.com/facebook/docusaurus/issues/4591#issuecomment-822372398
title: Joi.string().allow(''),
// See https://github.com/facebook/docusaurus/issues/4591#issuecomment-822372398
description: Joi.string().allow(''),
keywords: Joi.array().items(Joi.string().required()),
image: URISchema,
wrapperClassName: Joi.string(),
hide_table_of_contents: Joi.boolean(),
...FrontMatterTOCHeadingLevels,
}).concat(ContentVisibilitySchema);

export function validatePageFrontMatter(frontMatter: {
[key: string]: unknown;
}): FrontMatter {
}): PageFrontMatter {
return validateFrontMatter(frontMatter, PageFrontMatterSchema);
}
10 changes: 10 additions & 0 deletions packages/docusaurus-plugin-content-pages/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import type {
PluginOptions,
Metadata,
LoadedContent,
PageFrontMatter,
} from '@docusaurus/plugin-content-pages';

export function getContentPathList(contentPaths: PagesContentPaths): string[] {
Expand Down Expand Up @@ -234,6 +235,15 @@ export default function pluginContentPages(
`${docuHash(aliasedSource)}.json`,
);
},
// Assets allow to convert some relative images paths to
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of a code comment here only, we might also consider a light mention about Assets and require() calls with relative paths, perhaps most especially in the API doc pages for plugin-content-pages and additionally maybe an Admonition on the /docs/static-assets page, which does mention require() but skips saying anything about relative paths ? Anyways, just noting this since indeed relative paths are one of the tricky things that I had to figure out on my own with Docusaurus' default asset paths handling! Maybe it's Caveats section could be expanded slightly, dunno.

// require(...) calls
createAssets: ({
frontMatter,
}: {
frontMatter: PageFrontMatter;
}) => ({
image: frontMatter.image,
}),
markdownConfig: siteConfig.markdown,
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ declare module '@docusaurus/plugin-content-pages' {
import type {MDXOptions} from '@docusaurus/mdx-loader';
import type {LoadContext, Plugin} from '@docusaurus/types';

export type Assets = {
image?: string;
};

export type PluginOptions = MDXOptions & {
id?: string;
path: string;
Expand All @@ -20,9 +24,11 @@ declare module '@docusaurus/plugin-content-pages' {

export type Options = Partial<PluginOptions>;

export type FrontMatter = {
export type PageFrontMatter = {
readonly title?: string;
readonly description?: string;
readonly image?: string;
readonly keywords?: string[];
readonly wrapperClassName?: string;
readonly hide_table_of_contents?: string;
readonly toc_min_heading_level?: number;
Expand All @@ -41,7 +47,7 @@ declare module '@docusaurus/plugin-content-pages' {
type: 'mdx';
permalink: string;
source: string;
frontMatter: FrontMatter & {[key: string]: unknown};
frontMatter: PageFrontMatter & {[key: string]: unknown};
title?: string;
description?: string;
unlisted: boolean;
Expand All @@ -61,11 +67,16 @@ declare module '@theme/MDXPage' {
import type {LoadedMDXContent} from '@docusaurus/mdx-loader';
import type {
MDXPageMetadata,
FrontMatter,
PageFrontMatter,
Assets,
} from '@docusaurus/plugin-content-pages';

export interface Props {
readonly content: LoadedMDXContent<FrontMatter, MDXPageMetadata>;
readonly content: LoadedMDXContent<
PageFrontMatter,
MDXPageMetadata,
Assets
>;
}

export default function MDXPage(props: Props): JSX.Element;
Expand Down
16 changes: 13 additions & 3 deletions packages/docusaurus-theme-classic/src/theme/MDXPage/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,28 @@ export default function MDXPage(props: Props): JSX.Element {
const {content: MDXPageContent} = props;
const {
metadata: {title, description, frontMatter, unlisted},
assets,
} = MDXPageContent;
const {wrapperClassName, hide_table_of_contents: hideTableOfContents} =
frontMatter;
const {
keywords,
wrapperClassName,
hide_table_of_contents: hideTableOfContents,
} = frontMatter;
const image = assets.image ?? frontMatter.image;

return (
<HtmlClassNameProvider
className={clsx(
wrapperClassName ?? ThemeClassNames.wrapper.mdxPages,
ThemeClassNames.page.mdxPage,
)}>
<PageMetadata title={title} description={description} />
<Layout>
<PageMetadata
title={title}
description={description}
keywords={keywords}
image={image}
/>
<main className="container container--fluid margin-vert--lg">
<div className={clsx('row', styles.mdxPageWrapper)}>
<div className={clsx('col', !hideTableOfContents && 'col--8')}>
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 17 additions & 0 deletions website/_dogfooding/_pages tests/seo.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
title: custom SEO title
description: custom SEO description
keywords: [custom, keywords]
image: ./local-image.png
---

# SEO tests

Using page SEO front matter:

```yaml
title: custom SEO title
description: custom SEO description
keywords: [custom, keywords]
image: ./local-image.png
```
2 changes: 1 addition & 1 deletion website/docs/api/plugins/plugin-content-blog.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ const config = {

## Markdown front matter {#markdown-front-matter}

Markdown documents can use the following Markdown front matter metadata fields, enclosed by a line `---` on either side.
Markdown documents can use the following Markdown [front matter](../../guides/markdown-features/markdown-features-intro.mdx#front-matter) metadata fields, enclosed by a line `---` on either side.

Accepted fields:

Expand Down
2 changes: 1 addition & 1 deletion website/docs/api/plugins/plugin-content-docs.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ const config = {

## Markdown front matter {#markdown-front-matter}

Markdown documents can use the following Markdown front matter metadata fields, enclosed by a line `---` on either side.
Markdown documents can use the following Markdown [front matter](../../guides/markdown-features/markdown-features-intro.mdx#front-matter) metadata fields, enclosed by a line `---` on either side.

Accepted fields:

Expand Down
6 changes: 4 additions & 2 deletions website/docs/api/plugins/plugin-content-pages.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ const config = {

## Markdown front matter {#markdown-front-matter}

Markdown pages can use the following Markdown front matter metadata fields, enclosed by a line `---` on either side.
Markdown pages can use the following Markdown [front matter](../../guides/markdown-features/markdown-features-intro.mdx#front-matter) metadata fields, enclosed by a line `---` on either side.

Accepted fields:

Expand All @@ -93,7 +93,9 @@ Accepted fields:
| --- | --- | --- | --- |
| `title` | `string` | Markdown title | The blog post title. |
| `description` | `string` | The first line of Markdown content | The description of your page, which will become the `<meta name="description" content="..."/>` and `<meta property="og:description" content="..."/>` in `<head>`, used by search engines. |
| `wrapperClassName` | `string` | Class name to be added to the wrapper element to allow targeting specific page content. |
| `keywords` | `string[]` | `undefined` | Keywords meta tag, which will become the `<meta name="keywords" content="keyword1,keyword2,..."/>` in `<head>`, used by search engines. |
| `image` | `string` | `undefined` | Cover or thumbnail image that will be used when displaying the link to your post. |
| `wrapperClassName` | `string` | | Class name to be added to the wrapper element to allow targeting specific page content. |
| `hide_table_of_contents` | `boolean` | `false` | Whether to hide the table of contents to the right. |
| `draft` | `boolean` | `false` | Draft pages will only be available during development. |
| `unlisted` | `boolean` | `false` | Unlisted pages will be available in both development and production. They will be "hidden" in production, not indexed, excluded from sitemaps, and can only be accessed by users having a direct link. |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,16 @@ more_data:
---
```

:::info

The API documentation of each official plugin lists the supported attributes:

- [Docs front matter](../../api/plugins/plugin-content-docs.mdx#markdown-front-matter)
- [Blog front matter](../../api/plugins/plugin-content-blog.mdx#markdown-front-matter)
- [Pages front matter](../../api/plugins/plugin-content-pages.mdx#markdown-front-matter)

:::

## Quotes {#quotes}

Markdown quotes are beautifully styled:
Expand Down
71 changes: 65 additions & 6 deletions website/docs/seo.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,40 @@ Docusaurus supports search engine optimization in a variety of ways.

## Global metadata {#global-metadata}

Provide global meta attributes for the entire site through the [site configuration](./configuration.mdx#site-metadata). The metadata will all be rendered in the HTML `<head>` using the key-value pairs as the prop name and value.
Provide global meta attributes for the entire site through the [site configuration](./configuration.mdx#site-metadata). The metadata will all be rendered in the HTML `<head>` using the key-value pairs as the prop name and value. The `metadata` attribute is a convenient shortcut to declare `<meta>` tags, but it is also possible to inject arbitrary tags in `<head>` with the `headTags` attribute.

```js title="docusaurus.config.js"
module.exports = {
themeConfig: {
metadata: [{name: 'keywords', content: 'cooking, blog'}],
// This would become <meta name="keywords" content="cooking, blog"> in the generated HTML
// Declare some <meta> tags
metadata: [
{name: 'keywords', content: 'cooking, blog'},
{name: 'twitter:card', content: 'summary_large_image'},
],
headTags: [
// Declare a <link> preconnect tag
{
tagName: 'link',
attributes: {
rel: 'preconnect',
href: 'https://example.com',
},
},
// Declare some json-ld structured data
{
tagName: 'script',
attributes: {
type: 'application/ld+json',
},
innerHTML: JSON.stringify({
'@context': 'https://schema.org/',
'@type': 'Organization',
name: 'Meta Open Source',
url: 'https://opensource.fb.com/',
logo: 'https://opensource.fb.com/img/logos/Meta-Open-Source.svg',
}),
},
],
},
};
```
Expand All @@ -37,13 +64,24 @@ Similar to [global metadata](#global-metadata), Docusaurus also allows for the a
# A cooking guide

<head>
<meta name="keywords" content="cooking, blog">
<meta name="keywords" content="cooking, blog" />
<meta name="twitter:card" content="summary_large_image" />
<link rel="preconnect" href="https://example.com" />
<script type="application/ld+json">
{JSON.stringify({
'@context': 'https://schema.org/',
'@type': 'Organization',
name: 'Meta Open Source',
url: 'https://opensource.fb.com/',
logo: 'https://opensource.fb.com/img/logos/Meta-Open-Source.svg',
})}
</script>
</head>

Some content...
```

Docusaurus automatically adds `description`, `title`, canonical URL links, and other useful metadata to each Markdown page. They are configurable through front matter:
Docusaurus automatically adds `description`, `title`, canonical URL links, and other useful metadata to each Markdown page. They are configurable through [front matter](./guides/markdown-features/markdown-features-intro.mdx#front-matter):

```md
---
Expand All @@ -58,7 +96,17 @@ When creating your React page, adding these fields in `Layout` would also improv

:::tip

Prefer to use front matter for fields like `description` and `keywords`: Docusaurus will automatically apply this to both `description` and `og:description`, while you would have to manually declare two metadata tags when using the `<head>` tag.
Prefer to use [front matter](./guides/markdown-features/markdown-features-intro.mdx#front-matter) for fields like `description` and `keywords`: Docusaurus will automatically apply this to both `description` and `og:description`, while you would have to manually declare two metadata tags when using the `<head>` tag.

:::

:::info

The official plugins all support the following [front matter](./guides/markdown-features/markdown-features-intro.mdx#front-matter): `title`, `description`, `keywords` and `image`. Refer to their respective API documentation for additional [front matter](./guides/markdown-features/markdown-features-intro.mdx#front-matter) support:

- [Docs front matter](./api/plugins/plugin-content-docs.mdx#markdown-front-matter)
- [Blog front matter](./api/plugins/plugin-content-blog.mdx#markdown-front-matter)
- [Pages front matter](./api/plugins/plugin-content-pages.mdx#markdown-front-matter)

:::

Expand All @@ -74,6 +122,17 @@ export default function page() {
<Layout title="Page" description="A React page demo">
<Head>
<meta property="og:image" content="image.png" />
<meta name="twitter:card" content="summary_large_image" />
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is generally about improving SEO docs. And front matter is a pretty big deal for this, we should also in this same PR try to clean up some missing links about front matter.

One thing I noticed above on line 61 is the mention of front matter. That should probably be turned into a link to the page on front matter. In general, I've noticed we are pretty bad at just typing names, when they really should be links to their respective pages, especially anything like plugin, api, etc.

I think the general link for the feature would be appropriate here I think:
https://docusaurus.io/docs/markdown-features#front-matter

But I also notice we don't have a link to the API when we describe front matter on that feature section! Which should have a link to the API for the plugin, right? https://docusaurus.io/docs/api/plugins/@docusaurus/plugin-content-docs#markdown-front-matter

But we DO have an API link on the Create a Doc section about Doc front matter

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure how this comment is related to the twitter:card meta? 🤔

Will add links to front matter

Note: the SEO page is generic so it can only link to the generic front matter page.

It is normal that a docs page links to docs-specific front matter. Each plugin can have its own front matter.

To not bias toward a single plugin I'll add links to all the API ref supporting front matter:

<link rel="preconnect" href="https://example.com" />
<script type="application/ld+json">
{JSON.stringify({
'@context': 'https://schema.org/',
'@type': 'Organization',
name: 'Meta Open Source',
url: 'https://opensource.fb.com/',
logo: 'https://opensource.fb.com/img/logos/Meta-Open-Source.svg',
})}
</script>
</Head>
{/* ... */}
</Layout>
Expand Down