# SEO and sitemap

Three related pieces of config live together: the site's own deployed `domain`, a `sitemap.xml` generated automatically at build time, and meta tags (Open Graph, Twitter card, canonical URL) — site-wide defaults in `writedocs.json`, overridable per page in frontmatter.

## `domain`

```json
{
  "domain": "docs.example.com"
}
```

The scheme is optional — `"docs.example.com"` and `"https://docs.example.com"` both work and resolve to the same `https://` origin. This one field turns on everything below:

- **`sitemap.xml`** — `writedocs build` only generates one when `domain` is set. A relative-URL sitemap isn't meaningful, so a site with no `domain` configured just doesn't get one (and a `writedocs] No writedocs.json "domain" set - skipping sitemap.xml generation.` line is logged during the build, not an error). The sitemap is a single `dist/sitemap.xml` file listing every page except those with `seo.noindex: true`. Only a site with more than 50,000 pages — the sitemap protocol's per-file limit — gets split files instead: `sitemap-index.xml` pointing at `sitemap-0.xml`, `sitemap-1.xml`, and so on.
- **Canonical URLs** — every page gets `<link rel="canonical" href="https://docs.example.com/your-page/">`.
- **Absolute `og:url`/`og:image`/`twitter:image`** — social crawlers generally require absolute URLs, not page-relative ones.

Leave `domain` unset for local-only builds, fixtures, or a site that hasn't picked a deployment URL yet — everything else in this page still works, just without absolute URLs or a sitemap.

<Callout type="tip">
  `domain` is metadata only — it doesn't configure hosting, DNS, or routing by itself. It just tells Writedocs what URL the site *will* be reachable at, so it can generate correct absolute links.
</Callout>

## `seo` — site-wide defaults

```json
{
  "seo": {
    "ogImage": "/social-card.png",
    "ogType": "website",
    "twitterCard": "summary_large_image",
    "keywords": ["docs", "api", "developer-tools"],
    "noindex": false
  }
}
```

<Parameter name="ogImage" type="string" />

<Parameter name="ogType" type="string" default='"website"' />

<Parameter name="twitterCard" type='"summary" | "summary_large_image"' default='"summary_large_image" if ogImage is set, otherwise "summary"' />

<Parameter name="keywords" type="string array" />

<Parameter name="noindex" type="boolean" default="false" />

`ogImage` can be a relative path (resolved against `domain` into an absolute URL) or an already-absolute `https://` URL. `og:title`/`og:description`/`twitter:title`/`twitter:description` aren't separately configurable — they're always the page's own `title`/`description`.

## Per-page overrides

A page's frontmatter can set its own `seo` (and its own `description`), overriding the site-wide default field by field — a page only needs to set what it wants to change, everything else still falls back to `writedocs.json`.

```mdx
---
title: Enterprise SSO
description: How SSO works for Enterprise plan customers.
seo:
  ogImage: /social/enterprise-sso.png
  keywords:
    - sso
    - enterprise
    - authentication
---
```

### `seo.noindex`

```mdx
---
title: Internal migration notes
seo:
  noindex: true
---
```

Renders `<meta name="robots" content="noindex, nofollow">` on that page and excludes it from `sitemap.xml` — the page still builds and is normally linkable/reachable, it's just not something you want search engines to index or crawl into as canonical content (a draft, an internal-only page kept in the same nav for convenience, a duplicate of content that lives canonically elsewhere).

<Callout type="tip">
  See `docs.json-examples/00-kitchen-sink/` in the Writedocs repo for a complete, buildable example covering all of this — site-wide defaults, `about.mdx` overriding several fields, and `docs/core/2025-09/guides/overview.mdx` as a `noindex` page.
</Callout>