# SEO for Starlight docs sites

*Originally published on [headlark.com](https://headlark.com/guides/starlight-seo).*

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](https://starlight.astro.build/reference/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.

```js
// 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](https://starlight.astro.build/reference/frontmatter/)):

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

- The `<title>` is the page title, `titleDelimiter` (default `|`), then the site title: "Configure the CLI | Example Docs". Keep page titles distinct; the suffix is the same everywhere.
- The page's `description` becomes both `<meta name="description">` and `og:description`.
- A page without one gets the site-wide `description` from the Starlight config. That's fine for the home page, but on a site where most pages skip the field, most pages share one description. [Duplicate titles and descriptions on a docs site](https://headlark.com/guides/duplicate-meta-descriptions-docs) shows how to find those pages in `dist`.

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:

```js
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:

```yaml
---
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`:

```yaml
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](https://headlark.com/guides/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:

```astro
---
// 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} />
  </>
)}
```

```js
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](https://starlight.astro.build/guides/route-data/) 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:

```js
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](https://starlight.astro.build/guides/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](https://headlark.com/guides/json-ld-astro-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:

```bash
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](https://headlark.com/tools/seo-checker) shows a page's Google result and its LinkedIn, Slack and X cards, and the [site checker](https://headlark.com/tools/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](https://headlark.com/docs/build) runs after `astro build` on the HTML in `dist`:

```json
"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](https://headlark.com/guides/astro) has the details.

- Each page gets a title, a description and Open Graph and X tags written from its own content, so pages stop inheriting the site description. Images without `alt` get alt text.
- Your canonical, robots directives and JSON-LD types are kept, and so is Starlight's 404 page, which Headlark skips.
- An existing `og:image` is kept. Without one, Headlark picks the page's most representative image, never an SVG. It doesn't create images, so for branded cards, generate them and set `og:image` as above.

## Try it

The free plan covers one site and 25 pages. [Add your site](https://headlark.com/app), copy its secret key, and your next build ships with SEO.

