# Site-level config

Six small `writedocs.json` fields that each solve one common site-wide need, none of them requiring a whole new page or component to use.

## `redirects`

An array of `{ source, destination }` pairs:

```json
{
  "redirects": [
    { "source": "/old-page", "destination": "/docs/getting-started" },
    { "source": "/v1/setup", "destination": "/docs/setup" }
  ]
}
```

Visiting `source` on the built site sends the visitor to `destination`. `writedocs build` also writes a `_redirects` file into `dist/` alongside the site — a format Cloudflare Pages and Netlify both read natively and turn into a real, instant edge redirect (no page ever renders). Deployed to one of those hosts, that's what a visitor actually gets. On any other static host (S3, GitHub Pages, a plain nginx server, etc.), the `_redirects` file is simply ignored, and the visitor instead lands on a small redirect page that sends them on before it's drawn — the visitor sees the destination, not the redirect. The page keeps the address's `?query` and `#anchor`, and still works with JavaScript off (through a `<meta http-equiv="refresh">` tag). Either way there's no real HTTP 3xx status code at that URL, since a static build has no server to issue one from — worth knowing if you're checking redirect behavior with a tool that inspects raw status codes rather than rendering the page.

The same applies to the automatic `/` → first-navigation-page redirect a site gets for free when no page's slug is `index` (see [Navigation basics](/docs/configuration/navigation-basics/)) — it's covered by the same generated `_redirects` file. That one is a temporary redirect (302), not a permanent one: browsers remember a permanent redirect, and would keep sending `/` to that page even after you add a home page.

## `variables`

A flat map of names to text, substituted into every page's prose wherever you write `[[name]]`:

```json
{
  "variables": {
    "productName": "Acme",
    "supportEmail": "support@acme.com"
  }
}
```

```mdx
Welcome to [[productName]]! Questions? Reach us at [[supportEmail]].
```

Change the value once in `writedocs.json` and every page using `[[productName]]` updates together. A placeholder with no matching entry (a typo, or one you haven't defined yet) is left as literal text rather than silently disappearing, so a mistake is easy to spot in the built page. Text inside a fenced or inline code block is never substituted — `` `[[productName]]` `` inside backticks always renders as literal backtick text.

`{{name}}` works too - it's how Mintlify writes the same thing - as long as `name` is one of your `variables` and uses only letters, digits and underscores. MDX reads `{{name}}` as JavaScript, so a name with a hyphen (`{{support-email}}`) fails to parse; write those as `[[support-email]]`.

## `banner`

A dismissible strip above the top navigation bar:

```json
{
  "banner": {
    "content": "You're viewing docs for an upcoming release.",
    "dismissible": true,
    "type": "warning"
  }
}
```

`type` is `"info"` (default), `"warning"`, or `"critical"`, each with its own color. `dismissible` (default `false`) adds a close button; once dismissed, it stays dismissed for that visitor (remembered in their browser) until you change the banner's content or they clear their browsing data. Leave `dismissible` off for something that should stay visible until you remove it from `writedocs.json` yourself.

## `notFound`

Custom title and description for the page shown when a visitor hits a URL that doesn't exist:

```json
{
  "notFound": {
    "title": "Lost in the docs?",
    "description": "That page doesn't exist — try the search bar or the sidebar."
  }
}
```

Both are optional; a site with no `notFound` config gets a plain, reasonable default. The page still renders inside your normal top navigation and footer, just without a sidebar or table of contents (there's no active section to build them from).

## Custom CSS and JS — just drop the file in

Any `.css` or `.js` file anywhere in your project — the root, `docs/`, `snippets/`, any folder you've made — loads automatically on every page. No `writedocs.json` field, no filename to register anywhere, no dedicated assets folder to put it in:

```
my-docs-site/
├── writedocs.json
├── theme.css              ← loads automatically
└── docs/
    ├── getting-started.mdx
    └── extra-styles.css   ← also loads automatically
```

That's the entire setup. Add a `.css`/`.js` file, edit it, refresh the page — it's there. Rename it, delete it, move it to another folder — writedocs picks up whatever `.css`/`.js` files it finds anywhere in the project each time it builds or serves a page. Multiple files are fine; they load in alphabetical order by path.

<Callout type="note">
  If you're coming from Astro and already keep a `public/` folder in your project, a `.css`/`.js` file placed there is picked up too — it just loads as a linked `<link rel="stylesheet">`/`<script src>` instead of having its content inlined into the page, since a `public/` file is already served as its own static asset. Same effect either way (the file loads on every page); this is only worth knowing if you're specifically checking network requests or cache behavior. There's no reason to create a `public/` folder just for this — every folder works identically otherwise.
</Callout>

<Callout type="warning">
  Because this checks the whole project, not just the root, any `.css`/`.js` file you keep around for another reason — a snippet's own helper script, a stylesheet you're drafting but haven't wired up yet — will also load on every page. There's no per-file opt-out short of giving it a different extension while you're not using it.
</Callout>

## `scripts`

For anything the drop-a-file convention above doesn't cover — an external URL (a CDN-hosted widget, an analytics snippet), or inline code you'd rather keep in `writedocs.json` than as a separate file — `scripts` gives you explicit control:

```json
{
  "scripts": {
    "head": [
      { "src": "https://example.com/widget.js" }
    ],
    "body": [
      { "content": "console.log('loaded');" }
    ]
  }
}
```

Each entry is exactly one of `src` (an external URL or a local path) or `content` (inline code) — never both. `scripts.head` loads in `<head>`, before the page renders; `scripts.body` loads right before the page finishes, after everything else has already run — usually the right spot for a third-party snippet's own install instructions. If both a project file and a `writedocs.json` entry try to affect the same thing, the project file wins — it's the more specific, more locally-owned override.

A local root-relative path (`"/widget.js"`, say, matching a file anywhere in your project) still works here too — it's just no longer the only way to load one, since the drop-a-file convention above already covers every local file automatically. Pointing `scripts` at a real external URL (as above) is the case this still exists for.

<Callout type="tip">
  See `docs.json-examples/00-kitchen-sink/` in the Writedocs repo for a complete, buildable example combining all of this, including its own zero-config `custom.css` at the root.
</Callout>