Updated 2026-10-26

MkDocs SEO: site_url, meta descriptions, social cards and the sitemap

MkDocs builds a static HTML page per Markdown file, so crawlers and link previewers get real content. With Material for MkDocs, the theme most MkDocs sites use, each page also gets a <title>, a meta description and, once you set site_url, a canonical link and a sitemap with real URLs. Open Graph tags come only from the social plugin or a template override. This guide goes through each setting for MkDocs 1.6 and Material 9 (MkDocs: configuration, Material for MkDocs).

Material for MkDocs has been in maintenance mode since November 2025: no new features, critical fixes through May 2027, and its team now builds Zensical, which reads existing mkdocs.yml files. Everything below works on current Material releases; check the plugin list before moving to Zensical.

1. site_url, site_name and site_description

# mkdocs.yml
site_name: Example Docs
site_url: https://docs.example.com/
site_description: Reference and guides for the Example CLI.
theme:
  name: material

2. Per-page title and description

Material reads the page's YAML front matter (MkDocs calls it meta-data):

---
title: Configure the CLI
description: Every option in the config file, with defaults and an example for monorepos.
---

# Configuration

To see which pages still use the fallback, count descriptions in the build:

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

Duplicate titles and descriptions on a docs site goes through finding and fixing them.

3. Link previews with the social plugin

Material's social plugin draws a 1200×630 card image for every page from its title and description and adds the tags that use it (social plugin):

plugins:
  - search
  - social:
      cards_layout_options:
        background_color: "#1f2937"
        color: "#ffffff"

It writes og:type, og:title, og:description, og:url, og:image with its type and size, and twitter:card set to summary_large_image with matching twitter: title, description and image. Things to know:

4. Open Graph tags without the plugin

If you can't install Cairo, or want one fixed image, add the tags in a theme override (customization). Set custom_dir:

theme:
  name: material
  custom_dir: overrides

and extend the extrahead block in overrides/main.html:

{% extends "base.html" %}
{% block extrahead %}
  {% set desc = page.meta.description if page.meta and page.meta.description else config.site_description %}
  <meta property="og:type" content="website">
  <meta property="og:title" content="{{ page.title | striptags }}">
  <meta property="og:description" content="{{ desc }}">
  {% if page.canonical_url %}<meta property="og:url" content="{{ page.canonical_url }}">{% endif %}
  <meta property="og:image" content="{{ config.site_url }}assets/og-default.png">
  <meta name="twitter:card" content="summary_large_image">
  {% if page.meta and page.meta.robots %}<meta name="robots" content="{{ page.meta.robots }}">{% endif %}
{% endblock %}

Put a PNG or JPG around 1200×630 at docs/assets/og-default.png; previewers don't render SVG. The last line lets a page opt out of search with robots: noindex in its front matter, which Material doesn't support on its own. If a card still has no picture, og:image not showing covers the other causes.

5. use_directory_urls

With the default use_directory_urls: true, docs/setup.md is written to site/setup/index.html and linked as /setup/, and that's the URL in the canonical and the sitemap. Leave it on. MkDocs suggests false (which writes setup.html) only for docs opened from the file system or hosts that can't serve index.html for a folder. Switching it on a live site changes every URL, so old links and the URLs Google has indexed break unless you add redirects, for example with the mkdocs-redirects plugin.

6. The sitemap and excluded pages

MkDocs writes sitemap.xml and sitemap.xml.gz on every build, with nothing to configure beyond site_url. Two details:

Add Sitemap: https://docs.example.com/sitemap.xml to docs/robots.txt and submit the sitemap in Search Console.

7. Check the build

Run mkdocs build --strict (warnings fail the build, so broken links don't ship), then check site/:

grep -rL --include='*.html' 'rel="canonical"' site
grep -rL --include='*.html' 'property="og:image"' site

The first list should be empty once site_url is set. Then check a few deployed pages with the free SEO checker, which shows the Google result and the LinkedIn, Slack and X cards, or up to 50 sitemap pages with the site checker.

Automating it with Headlark

The settings above take an afternoon. A description for each of several hundred pages is what never gets written. Headlark's build step runs after mkdocs build on the HTML in site/ and writes them from each page's content. It's a Node.js CLI, so the build environment needs Node 20 or newer alongside Python; GitHub Actions, Netlify and Cloudflare Pages have it. Add it once with npm i -D headlark (that creates a package.json and a lockfile next to mkdocs.yml), then:

mkdocs build
npx headlark site --site-url https://docs.example.com/

Use the same address as site_url. Set HEADLARK_KEY in the build environment. On GitHub Pages, run both in an Actions workflow and upload site, as in GitHub Pages (Actions); mkdocs gh-deploy builds and pushes in one step, which leaves no point to run Headlark in between. On Read the Docs, add nodejs: "22" under build.tools in .readthedocs.yaml, run npm ci in build.jobs.post_install, and run npx headlark $READTHEDOCS_OUTPUT/html --site-url … in build.jobs.post_build, with HEADLARK_KEY as a project environment variable.

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.