Builds the sidebar tree once and binds every navigation lookup to it.
# createNavigation
Creates a `Navigation` over a list of content entries. It builds the sidebar
tree a single time and binds the four lookups a page needs to that one tree, so
the rail, the breadcrumbs, and the pager can never disagree about what the site
contains.
```ts
import { createNavigation } from "@teasim/astra";
const docs = createNavigation(entries, { base: "/docs" });
docs.sidebar; // the tree, as getSidebar builds it
docs.prevNext("guides/installing"); // { prev, next }
docs.breadcrumbs("guides/installing"); // [{ label, href, current }, …]
docs.sections("guides/installing"); // top level, with the current one marked
docs.find("guides/installing"); // one node, or null
// prevNext reads the entry's own frontmatter, so a page hand-wires nothing.
docs.prevNext("intro"); // honours `next: false` in intro's frontmatter
// An explicit override still wins.
docs.prevNext("intro", { next: "guides/installing" });
// Nothing here reads astro:content, touches the filesystem, or needs a build.
createNavigation([{ slug: "a", title: "Alpha" }]).sidebar[0].href; // "/a"
```
## API Reference
### Signature
```ts
createNavigation(entries: readonly ContentEntry[], options?: SidebarOptions): Navigation;
```
### Parameters
| Parameter | Type | Required | Notes |
| --- | --- | --- | --- |
| `entries` | `readonly ContentEntry[]` | Yes | Usually `collection.map(toContentEntry)`. Order is not significant — the tree sorts by `sidebar.order`, then label |
| `options` | `SidebarOptions` | No | `base`, `trailingSlash`, `includeDrafts`, `transform` |
#### SidebarOptions
| Option | Type | Default | Notes |
| --- | --- | --- | --- |
| `base` | `string` | `"/"` | Prefix for every `href`; a path or an absolute URL. A path with no leading slash is still rooted |
| `trailingSlash` | `boolean` | `false` | Append `/` to every `href` except the root |
| `includeDrafts` | `boolean` | `false` | Keep entries marked `draft` |
| `transform` | `(nodes: SidebarNode[]) => SidebarNode[]` | — | Reshape the finished tree |
### Returns
Returns a `Navigation` — the tree plus four lookups bound to it.
| Member | Type | Notes |
| --- | --- | --- |
| `sidebar` | `SidebarNode[]` | The finished tree, already sorted and pruned |
| `find(slug)` | `SidebarNode \| null` | `null` for a heading, an unknown slug, or an empty one |
| `breadcrumbs(slug)` | `Breadcrumb[]` | Ancestry, `current: true` on the last crumb; `[]` for an unknown slug |
| `prevNext(slug, overrides?)` | `PrevNext` | Both ends `SidebarLink \| null`; never wraps |
| `sections(slug)` | `SidebarSection[]` | The top level, with the one holding `slug` marked `current` |
### Throws
**Nothing.** Bad content does the most sensible thing and carries on — two
entries that normalise to one slug become one node, last wins. That is how a
duplicate slug becomes one silently missing page, so run `validateContent` on
the same entries when you want the problems named instead — the scaffolded
`src/helpers/docs.ts` does exactly that, in dev only.
## Agent Contract
| Field | Value |
| --- | --- |
| Kind | factory |
| Canonical name | `createNavigation` |
| Aliases | None |
| Mutates input | No |
| Returns | `Navigation` |
| Reads `astro:content` | No |
| Needs a build | No |
| Configuration dependencies | None |
| Related | `getSidebar`, `getPrevNext`, `getBreadcrumbs`, `getSidebarSections`, `findSidebarNode` |
## Agent Notes
- Generate `createNavigation(entries, options)` for a page. The five underlying
functions stay exported for a test driving one answer, or a page that wants
one without the rest.
- Do not thread `{ prev: entry.data.prev, next: entry.data.next }` into
`prevNext` — it reads the entry's frontmatter itself. A mapper that forgets
makes those two schema fields silently inert.
- Build it once per page, not once per lookup. Each call rebuilds the tree.
- Map entries with `toContentEntry`, not by hand: a hand mapper that drops
`externalLink`, `prev`, `next`, or `previousSlug` makes those fields inert and
`validateContent` cannot report what it never receives.
- Both ends of `prevNext` are `SidebarLink`, whose `slug` and `href` are never
`null`. Do not generate a null check for them.
- `breadcrumbs` keeps headings and `prevNext` skips them, so a trail is not a
slice of the reading order. Do not derive one from the other.
- Pass `base: import.meta.env.BASE_URL` in an Astro project so every `href`
survives a deploy under a subpath.
Creates a Navigation over a list of content entries. It builds the sidebar
tree a single time and binds the four lookups a page needs to that one tree, so
the rail, the breadcrumbs, and the pager can never disagree about what the site
contains.
ts
import { createNavigation } from "@teasim/astra";const docs = createNavigation(entries, { base: "/docs" });docs.sidebar; // the tree, as getSidebar builds itdocs.prevNext("guides/installing"); // { prev, next }docs.breadcrumbs("guides/installing"); // [{ label, href, current }, …]docs.sections("guides/installing"); // top level, with the current one markeddocs.find("guides/installing"); // one node, or null// prevNext reads the entry's own frontmatter, so a page hand-wires nothing.docs.prevNext("intro"); // honours `next: false` in intro's frontmatter// An explicit override still wins.docs.prevNext("intro", { next: "guides/installing" });// Nothing here reads astro:content, touches the filesystem, or needs a build.createNavigation([{ slug: "a", title: "Alpha" }]).sidebar[0].href; // "/a"
Usually collection.map(toContentEntry). Order is not significant — the tree sorts by sidebar.order, then label
options
SidebarOptions
No
base, trailingSlash, includeDrafts, transform
SidebarOptions
Option
Type
Default
Notes
base
string
"/"
Prefix for every href; a path or an absolute URL. A path with no leading slash is still rooted
trailingSlash
boolean
false
Append / to every href except the root
includeDrafts
boolean
false
Keep entries marked draft
transform
(nodes: SidebarNode[]) => SidebarNode[]
—
Reshape the finished tree
Returns
Returns a Navigation — the tree plus four lookups bound to it.
Member
Type
Notes
sidebar
SidebarNode[]
The finished tree, already sorted and pruned
find(slug)
SidebarNode | null
null for a heading, an unknown slug, or an empty one
breadcrumbs(slug)
Breadcrumb[]
Ancestry, current: true on the last crumb; [] for an unknown slug
prevNext(slug, overrides?)
PrevNext
Both ends SidebarLink | null; never wraps
sections(slug)
SidebarSection[]
The top level, with the one holding slug marked current
Throws
Nothing. Bad content does the most sensible thing and carries on — two
entries that normalise to one slug become one node, last wins. That is how a
duplicate slug becomes one silently missing page, so run validateContent on
the same entries when you want the problems named instead — the scaffolded
src/helpers/docs.ts does exactly that, in dev only.
Generate createNavigation(entries, options) for a page. The five underlying
functions stay exported for a test driving one answer, or a page that wants
one without the rest.
Do not thread { prev: entry.data.prev, next: entry.data.next } into
prevNext — it reads the entry’s frontmatter itself. A mapper that forgets
makes those two schema fields silently inert.
Build it once per page, not once per lookup. Each call rebuilds the tree.
Map entries with toContentEntry, not by hand: a hand mapper that drops
externalLink, prev, next, or previousSlug makes those fields inert and
validateContent cannot report what it never receives.
Both ends of prevNext are SidebarLink, whose slug and href are never
null. Do not generate a null check for them.
breadcrumbs keeps headings and prevNext skips them, so a trail is not a
slice of the reading order. Do not derive one from the other.
Pass base: import.meta.env.BASE_URL in an Astro project so every href
survives a deploy under a subpath.