Updated 2026-10-21

SEO for Starlight docs sites

Starlight, the docs theme for Astro, gets more SEO right by default than most docs generators. Every page is static HTML with a <title>, og:title, og:type, og:locale, og:site_name and twitter:card set to summary_large_image. With a site URL configured it adds a canonical link, og:url, a sitemap and, on multilingual sites, hreflang links. Three things are left to you: a description for each page, a preview image, and anything beyond those tags. This guide covers each one, checked against Starlight 0.42 (Starlight configuration).

1. Set site first

Starlight reads your production URL from Astro's site option. Without it, pages have no canonical link and no og:url, and no sitemap is written.

// astro.config.mjs
import { defineConfig } from 'astro/config';
import starlight from '@astrojs/starlight';

export default defineConfig({
  site: 'https://docs.example.com',
  integrations: [
    starlight({
      title: 'Example Docs',
      description: 'Guides and reference for the Example CLI.',
    }),
  ],
});

If the docs live under a path, such as a GitHub Pages project site, set base as well (base: '/repo'); canonicals and sitemap entries include it. Check that site is the production domain in the environment that deploys, not a preview URL.

2. Titles and descriptions

Every page needs a title in its front matter; description is optional (frontmatter reference):

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

To replace the generated title on one page, put a title tag in its head front matter (next section). Use this sparingly: the visible heading and the search result should usually match.

3. The head option and a preview image

Starlight doesn't set og:image, so links to your docs show a card without a picture until you add one. The head option in the config adds tags to every page:

starlight({
  title: 'Example Docs',
  head: [
    {
      tag: 'meta',
      attrs: { property: 'og:image', content: 'https://docs.example.com/og.png' },
    },
    {
      tag: 'meta',
      attrs: { name: 'twitter:image', content: 'https://docs.example.com/og.png' },
    },
  ],
}),

Use a full URL: entries are converted straight to HTML, so nothing makes a relative path absolute. Put the file in public/ so its name doesn't change between builds, and use a PNG or JPG around 1200×630, not an SVG.

The head front matter field takes the same format for one page:

---
title: Migrating from v2
head:
  - tag: meta
    attrs:
      property: og:image
      content: https://docs.example.com/og/migrating.png
---

Starlight merges the three sources in order: its defaults, then the config head, then the page's head. A <meta> with the same name or property, a <title> or a canonical link replaces the earlier one instead of being added twice, so the page's og:image wins over the site default. The same works for a per-page noindex:

head:
  - tag: meta
    attrs:
      name: robots
      content: noindex

pagefind: false only hides a page from Starlight's own search; it has no effect on search engines. For other reasons a card can stay blank, see og:image not showing.

4. Overriding the Head component

When tags depend on the page, such as a generated image per doc, override Starlight's Head component. It renders the merged list from the page's route data, so you can render the default and add to it:

---
// src/components/Head.astro
import Default from '@astrojs/starlight/components/Head.astro';

const { id, head } = Astro.locals.starlightRoute;
const hasImage = head.some((t) => t.attrs?.property === 'og:image');
const image = new URL(`/og/${id || 'index'}.png`, Astro.site).href;
---
<Default />
{!hasImage && (
  <>
    <meta property="og:image" content={image} />
    <meta name="twitter:image" content={image} />
  </>
)}
starlight({
  title: 'Example Docs',
  components: { Head: './src/components/Head.astro' },
}),

The check keeps any image set in front matter or config. Only reference image files that exist: an og:image that returns 404 is worse than none, because previewers give up on the card. Starlight's route data middleware is an alternative: it can change head before any component renders.

5. The sitemap

Starlight adds @astrojs/sitemap itself, so with site set, astro build writes sitemap-index.xml and sitemap-0.xml to dist, and every page links to the index with <link rel="sitemap">. On a multilingual site the sitemap lists each page's language versions too.

To change it, for example to leave out pages you've marked noindex, add the integration yourself; Starlight then doesn't add its own copy:

import sitemap from '@astrojs/sitemap';

export default defineConfig({
  site: 'https://docs.example.com',
  integrations: [
    starlight({ title: 'Example Docs' }),
    sitemap({
      filter: (page) => !page.startsWith('https://docs.example.com/internal/'),
    }),
  ],
});

Your own copy doesn't get Starlight's language settings, so on a multilingual site pass i18n: { defaultLocale, locales } to it with the same locales. Add Sitemap: https://docs.example.com/sitemap-index.xml to public/robots.txt and submit the index in Search Console.

6. Multilingual sites and hreflang

With more than one locale configured and site set, every page gets a <link rel="alternate" hreflang="…"> for each locale, using each locale's lang, plus an x-default that points to the default locale. og:locale is set to the page's language.

Starlight links every locale, translated or not. A page missing in one language is still built at that locale's URL, showing the default-language content with a "This content is not available in your language yet" notice (Starlight i18n). So hreflang can point Google at a German URL that contains English text, and each such page has its own canonical. On a site with many untranslated pages, that's a lot of near-duplicate URLs. Translate the pages that matter in each language first, and use the route data's isFallback flag in a Head override if you want fallback pages to carry noindex.

7. Structured data

Starlight emits no JSON-LD. A script tag with type: application/ld+json works in the config or front matter head for fixed data, and the Head override can build an Article or BreadcrumbList for every page from the route. JSON-LD for Astro and Docusaurus has the Starlight code, the types worth using and how to validate them.

8. Check the build

Pages still on the site-wide description, and pages without a preview image:

grep -rlF --include='*.html' 'content="Guides and reference for the Example CLI."' dist
grep -rL --include='*.html' 'property="og:image"' dist

Replace the first string with your own site description. After deploying, the free SEO checker shows a page's Google result and its LinkedIn, Slack and X cards, and the site checker runs the same checks on up to 50 pages from your sitemap.

Automating it with Headlark

The configuration above takes an afternoon. A description and alt text for every page of a large docs site is the work that doesn't get finished. Headlark's build step runs after astro build on the HTML in dist:

"build": "astro build && headlark dist --site-url https://docs.example.com"

Use the same address as site, including base if you set one, and set HEADLARK_KEY in the build environment. The Astro guide has the details.

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.