Updated 2026-10-21
SEO for Docusaurus: titles, descriptions, link previews and structured data
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,
};
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)'
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'},
],
},
};
imageis the defaultog:imageandtwitter:image, a path relative to thestaticfolder (or a full URL). Docusaurus makes it absolute fromurl. It can't be an SVG; Facebook, LinkedIn and X won't render one. Use a PNG or JPG around 1200×630.metadatais a list of<meta>tags; every field is passed straight to the tag, and an entry overrides a default with the same name, such astwitter: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
---
titlebecomes<title>(followed by the site title) andog:title. Without it Docusaurus uses the Markdown#heading, then the doc id.sidebar_labelkeeps a short name in the sidebar while the title says what the page is.descriptionbecomes both<meta name="description">andog:description. Front matter is the better place for it than a hand-written tag, which would need both.imagebecomes this page'sog:image, replacingthemeConfig.image.keywordsbecomes 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
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'},
},
],
};
One Markdown or MDX page: a <head> block in the file itself:
# Migrating from v2
<head>
<meta property="og:type" content="article" />
</head>
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>
);
}
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 anoindexrobots tag. - A page you link to privately:
unlisted: truein 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: trueon that version in the docs plugin'sversionsoption. - The whole site (a staging deployment):
noIndex: trueat the top level of the config addsnoindex, nofollowto 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/**'],
},
},
],
],
ignorePatternstakes globs matched against route paths. With abaseUrlother 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 useslastmodwhen it's consistently accurate and ignoreschangefreqandpriority, so setting those two tonulljust makes the file smaller.createSitemapItemsis 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
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"
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
noindexpages stay as they are. - An existing
og:image, from front matter orthemeConfig.image, is kept. Headlark doesn't create images: with onlythemeConfig.imageset, every page keeps that one image, so setimageon the docs that deserve their own card. WithoutthemeConfig.image, the default share image you set in the dashboard does the same job. - 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.