---
title: Navigation
description: Tabs, groups, and the two page shapes you author by hand.
---

`navigation` is the one required field that describes your site's shape. It is a list of
tabs; each tab holds groups; each group holds pages.

```json
{
  "navigation": {
    "tabs": [
      {
        "label": "Docs",
        "groups": [
          { "label": "Get started", "pages": ["index", "quickstart"] },
          { "label": "Authoring", "pages": ["authoring/pages", "authoring/navigation"] }
        ]
      }
    ]
  }
}
```

**Every tab needs at least one group, and every group at least one page.** An empty tab
is a schema error, not an empty section — so a tab comes into existence at the moment it
has real content to hold, not before.

## Page references

A bare string is a **page id** — and a page id is not quite the same thing as a file
path. It is the identifier Reed derives from the file, and four rules shape it:

<Steps>
  <Step title="Every path segment is slugified independently">
    Lowercased, punctuation stripped. `Guide/Intro.mdx` has the page id `guide/intro`,
    not `Guide/Intro`.
  </Step>
  <Step title="A nested index collapses to its directory">
    `derived/index.mdx` has the page id `derived`. Write `"derived"`, never
    `"derived/index"`. A root-level `index.mdx` is the site root and is written
    `"index"`.
  </Step>
  <Step title="A frontmatter slug wins outright">
    A page whose frontmatter sets `slug: custom-route` has the page id `custom-route`,
    whatever its path.
  </Step>
  <Step title="Some files are not pages at all">
    A file whose name begins with `_`, or one inside a dot-prefixed directory, is not
    loaded and therefore has no page id.
  </Step>
</Steps>

The written form is still constrained to `^[A-Za-z0-9_-]+(?:/[A-Za-z0-9_-]+)*$` — which
permits uppercase, so `"Guide/Intro"` is a legal *string* that names no page.

`reed check` reports a page id that resolves to nothing, naming both what you wrote and
the id the file actually has. It is an error, so a navigation entry can never point at
nothing.

## Nesting

A group entry can itself be a group object, nesting up to 5 levels:

```json
{
  "label": "Reference",
  "pages": [
    { "label": "CLI", "pages": ["reference/cli/init", "reference/cli/build"] }
  ]
}
```

## Generated API pages

An entry of the shape `{ "label": "...", "api": true, "spec": "ingestion" }` generates a
complete set of endpoint pages from one OpenAPI spec. No MDX file exists for these —
Reed synthesizes them.

```json
{
  "label": "Ingestion API",
  "groups": [
    {
      "label": "Ingestion API",
      "pages": [{ "label": "Ingestion API", "api": true, "spec": "ingestion" }]
    }
  ]
}
```

`spec` names a key declared under `api.specs`; you may omit it only when `api.specs` has
exactly one entry.

Two structural rules come with it: an API entry must be the **only** page in its group,
and that group the **only** group in its tab. Reed requires a dedicated tab per spec —
generated endpoint pages cannot be mixed alongside hand-written pages in one group.
