DEV Community

Cover image for SEO for Docusaurus: titles, descriptions, link previews and structured data
Headlark
Headlark

Posted on Originally published at headlark.com

SEO for Docusaurus: titles, descriptions, link previews and structured data

Originally published on headlark.com.

Docusaurus renders a static HTML file for every route, so crawlers and link previewers get real content without running JavaScript. Out of the box each page also gets a <title>, a canonical link, og:url, twitter:card set to summary_large_image, and hreflang links when you have more than one locale. What it can't do on its own is write a good description for every page, pick a preview image per page, or decide which pages belong in search. This guide goes through each setting in order, for Docusaurus 3 (Docusaurus: SEO).

1. Site URL and trailing slashes

Every absolute URL Docusaurus writes (canonical, og:url, og:image, sitemap entries) is built from url and baseUrl in docusaurus.config.js. Set them to the production address:

export default {
  url: 'https://docs.example.com',
  baseUrl: '/',
  trailingSlash: true,
};
Enter fullscreen mode Exit fullscreen mode

Set trailingSlash explicitly. With the default (undefined) Docusaurus leaves links as written and emits /docs/intro/index.html for docs/intro.md, but the canonical and sitemap URL is /docs/intro without a slash. Many static hosts, GitHub Pages among them, answer that URL with a redirect to /docs/intro/. The result is a canonical that points at a redirect, and Search Console reports such as "Page with redirect" or "Alternate page with proper canonical tag".

Setting File written for docs/intro.md URL in canonical and sitemap
true build/docs/intro/index.html /docs/intro/
false build/docs/intro.html /docs/intro
undefined build/docs/intro/index.html /docs/intro (as linked)

Pick the value that matches how your host serves those files without redirecting; the Docusaurus deployment docs point to the trailing slash guide, which lists the behavior of each host. Then check one page:

curl -sI https://docs.example.com/docs/intro | grep -iE '^(HTTP|location)'
Enter fullscreen mode Exit fullscreen mode

A 200 means the canonical URL is served directly. A 301 or 308 means trailingSlash and your host disagree.

2. Site-wide metadata and the default preview image

Two settings in themeConfig apply to every page (theme configuration):

export default {
  themeConfig: {
    image: 'img/social-card.png',
    metadata: [
      {name: 'twitter:site', content: '@example'},
    ],
  },
};
Enter fullscreen mode Exit fullscreen mode
  • image is the default og:image and twitter:image, a path relative to the static folder (or a full URL). Docusaurus makes it absolute from url. It can't be an SVG; Facebook, LinkedIn and X won't render one. Use a PNG or JPG around 1200×630.
  • metadata is a list of <meta> tags; every field is passed straight to the tag, and an entry overrides a default with the same name, such as twitter:card.

Without image, pages have no preview image at all unless the doc sets one. If a card still comes out without a picture, og:image not showing covers the other causes.

3. Per-page front matter

The docs, blog and pages plugins all accept the same four SEO fields (docs front matter):

---
title: Configure the CLI
description: Every option in the config file, with defaults and an example for monorepos.
image: /img/social/configure.png
keywords: [cli, configuration]
sidebar_label: Configuration
---
Enter fullscreen mode Exit fullscreen mode
  • title becomes <title> (followed by the site title) and og:title. Without it Docusaurus uses the Markdown # heading, then the doc id. sidebar_label keeps a short name in the sidebar while the title says what the page is.
  • description becomes both <meta name="description"> and og:description. Front matter is the better place for it than a hand-written tag, which would need both.
  • image becomes this page's og:image, replacing themeConfig.image.
  • keywords becomes a <meta name="keywords"> tag. Google doesn't use that tag for ranking, so leave it out unless your own site search reads it.

The first-line description

If a doc has no description, Docusaurus uses the first line of Markdown content. Imports, code blocks and the # title are skipped, but other headings, admonitions and short intro sentences aren't. A page that opens with ## Prerequisites gets "Prerequisites" as its description, and so does every other page that opens the same way. To see what your build produced:

grep -rhoE --include='*.html' '<meta name="description" content="[^"]*"' build | sort | uniq -c | sort -rn | head
Enter fullscreen mode Exit fullscreen mode

Any description with a count above 1 is shared. Duplicate titles and descriptions on a docs site goes through finding and fixing them, including versioned docs.

4. Other head tags

For tags the front matter doesn't cover, there are three places, from widest to narrowest.

Every page: headTags in docusaurus.config.js, a list of objects with tagName, attributes and optionally innerHTML:

export default {
  headTags: [
    {
      tagName: 'link',
      attributes: {rel: 'preconnect', href: 'https://fonts.example.com'},
    },
  ],
};
Enter fullscreen mode Exit fullscreen mode

One Markdown or MDX page: a <head> block in the file itself:

# Migrating from v2

<head>
  <meta property="og:type" content="article" />
</head>
Enter fullscreen mode Exit fullscreen mode

A React page in src/pages: the @docusaurus/Head component. The default Layout also takes title and description props, which fill the same tags as front matter:

import Layout from '@theme/Layout';
import Head from '@docusaurus/Head';

export default function Pricing() {
  return (
    <Layout title="Pricing" description="Plans and limits for the hosted version.">
      <Head>
        <meta property="og:image" content="https://docs.example.com/img/social/pricing.png" />
      </Head>
      {/* page content */}
    </Layout>
  );
}
Enter fullscreen mode Exit fullscreen mode

Tags set this way go into the static HTML at build time, so crawlers see them.

5. Keeping pages out of search

  • One page: <head><meta name="robots" content="noindex" /></head> in the file. The sitemap plugin leaves out pages with a noindex robots tag.
  • A page you link to privately: unlisted: true in front matter. It's built, but hidden from the sidebar, marked not to be indexed and left out of the sitemap.
  • An old docs version: noIndex: true on that version in the docs plugin's versions option.
  • The whole site (a staging deployment): noIndex: true at the top level of the config adds noindex, nofollow to every page and skips the sitemap. Make sure it's off in production.

Don't use robots.txt for this: it stops crawling, not indexing, and a blocked page can't show Google its noindex.

6. The sitemap

@docusaurus/plugin-sitemap is part of preset-classic and writes sitemap.xml during docusaurus build (plugin-sitemap). Configure it through the preset:

presets: [
  [
    'classic',
    {
      sitemap: {
        lastmod: 'date',
        changefreq: null,
        priority: null,
        ignorePatterns: ['/tags/**', '/blog/page/**'],
      },
    },
  ],
],
Enter fullscreen mode Exit fullscreen mode
  • ignorePatterns takes globs matched against route paths. With a baseUrl other than /, include it in the pattern (/repo/tags/**). Tag and paginated list pages are the usual candidates: thin pages that compete with your docs.
  • lastmod ('date' or 'datetime') adds <lastmod> from each file's last update. Google uses lastmod when it's consistently accurate and ignores changefreq and priority, so setting those two to null just makes the file smaller.
  • createSitemapItems is a function for filtering or changing items when globs aren't enough.

Point crawlers at it with a line in static/robots.txt, Sitemap: https://docs.example.com/sitemap.xml, and submit it in Search Console.

7. Structured data

Recent Docusaurus 3 releases already emit BreadcrumbList JSON-LD on docs pages and BlogPosting on blog posts, so check a built page before adding your own. Site-wide data such as an Organization goes in headTags as a script with type: 'application/ld+json'; one-off data goes in the page's <head> block. JSON-LD for Astro and Docusaurus has working code, the types worth using and how to validate them.

8. Check the build

Everything above ends up in the HTML in build, so check that rather than the config. After npm run build, list pages without a description and pages without a preview image:

grep -rL --include='*.html' 'name="description"' build
grep -rL --include='*.html' 'property="og:image"' build
Enter fullscreen mode Exit fullscreen mode

Then check a few deployed pages with the free SEO checker, which shows the Google result and the LinkedIn, Slack and X cards each page produces, or the site checker for up to 50 pages from your sitemap at once.

Automating it with Headlark

The settings above are one-time work. Writing a distinct title, description and image alt text for each of hundreds of docs is the part that doesn't get done. Headlark's build step runs after docusaurus build on the HTML in build:

"build": "docusaurus build && headlark build --site-url https://docs.example.com"
Enter fullscreen mode Exit fullscreen mode

The first build after headlark is the output folder. If you set a baseUrl such as /repo/, include it: --site-url https://example.github.io/repo/. Set HEADLARK_KEY in the build environment.

  • Each page gets a title, a description and Open Graph and X tags written from its own content, and alt text for images that have none.
  • Your canonical, robots directives and JSON-LD types are kept, so the Docusaurus defaults and your noindex pages stay as they are.
  • An existing og:image, from front matter or themeConfig.image, is kept. Headlark doesn't create images: with only themeConfig.image set, every page keeps that one image, so set image on the docs that deserve their own card.
  • Docusaurus switches pages in the browser after the first load and applies its own title from route data, so the tab may show your original title after a click. Crawlers and link previewers load each page's HTML and get the rewritten tags.

Try it

The free plan covers one site and 25 pages. Add your site, copy its secret key, and your next build ships with SEO.

Top comments (1)

Collapse
 
citedy profile image
Dmitry Sergeev •

We need to produce a comment as per dev instructions. Must be short, one or two sentences, maybe with a mention of tool. Must include product URL exactly once. Must not be too promotional but can mention a tool that automates boring SEO parts, referencing product URL. We need to start with a specific reaction or question about this video, not generic praise. So something like "noticed the trailingSlash thing, had that bug before" or "any tips on JSON-LD for Docusaurus?" Then may mention tool. Make citedy.com