Skip to content

nav.json reference

src/content/nav.json holds the site’s authored navigation tree: the order pages appear in the sidebar, and which of them are collected into collapsible groups.

It is a sidecar — a file of its own, deliberately not a key inside index.json. It is also optional: a site without one gets a flat sidebar in pages[] order, which is exactly how every portal behaved before groups existed.

{
"entries": [
{ "kind": "page", "pageId": "home" },
{
"kind": "group",
"id": "brand-basics",
"label": "Brand Basics",
"startCollapsed": false,
"pageIds": ["logo", "colors", "typography"]
},
{ "kind": "page", "pageId": "contact" }
]
}

entries is the top-level sidebar order. Each entry is one of two kinds.

FieldTypeNotes
kind"page"Discriminator.
pageIdstringA page id from index.json’s pages[]. Must be non-empty.
FieldTypeDefaultNotes
kind"group"Discriminator.
idstringRequired, non-empty, unique. The group’s stable identity — renaming the group does not change it.
labelstring""Display name. May be blank; the UI then shows “Untitled group”.
startCollapsedbooleanfalseThe viewer’s default state: whether the group renders closed until a reader opens it. Unrelated to anything in the editor.
pageIdsstring[][]Member page ids, in the order they render inside the group.

Groups do not nest, and a page belongs to at most one group. label, startCollapsed, and pageIds all have defaults so that a hand- or agent-authored group missing a key still parses. Entries are parsed one at a time, so a malformed entry costs you that entry, not the file — the loader keeps every entry that validates, drops the rest, and marks the navigation degraded with a warning naming how many went. id is the one key with no default, so a group without one is the case that gets dropped.

Source of truth: NavFileSchema in packages/primitives/src/schemas/nav.ts.

Reconciliation: the file is a preference, not a constraint

Section titled “Reconciliation: the file is a preference, not a constraint”

nav.json is never trusted to match index.json. On every load the authored tree is repaired against the current page set by reconcileNav() (packages/primitives/src/lib/nav-tree.ts), which is total, pure, and idempotent — every possible input maps to a valid nav in which each page appears exactly once:

  • An entry naming a page that no longer exists is dropped.
  • A page that appears more than once keeps only its first placement.
  • A duplicate group id keeps only the first group.
  • A page in index.json that no nav entry mentions is appended to the end.
  • A malformed file (or unparseable JSON) degrades to a flat nav, with a warning — never a failed load.

So the worst outcome of a stale or partial nav.json is a page in the wrong position. It cannot brick the viewer or the editor, and there is no state it can reach that needs manual repair.

Read the nav through navEntries(index), which always reconciles. It is the only sanctioned accessor — callers never have to reason about whether index.nav is present, stale, or written by an older package version.

pnpm exec authoring validate reports a parse failure as an error and the repairable cases above as warnings, matching that split: reconciliation has already fixed the warnings by the time the site renders, so they must not fail a build.

Writes are wholesale replacements, and that makes the two directions asymmetric:

  • Leave nav.json alone → the existing grouping is preserved. This is the safe default. Editing only index.json never disturbs the nav.
  • Write nav.json → it replaces the previous grouping entirely.

The consequence worth stating outright: {"entries": []} is not a no-op. It is a valid nav that flattens every group on the site. If you are not deliberately changing the grouping, do not write the file.

A nav payload that is not a valid navigation tree is declined, not applied: the rest of the write lands, nav.json is left exactly as it was, and the response carries a warning saying so. So a rejected grouping never corrupts the file — but check the warnings, because the write still reports success.

Grouping is managed in the Pages modal — + Add group, then drag pages onto the group header to move them in and out. The editor writes nav.json on save.

The nav arrives as a nav key on the index returned by get_site_index and get_site_content, and is written back the same way via save_sections. The same asymmetry applies:

  • Omit nav from the posted indexnav.json is left untouched.
  • Include it → the file is replaced. Posting nav: [] deletes every group.

Either post back the nav you read, or leave the key out.

Prefer indexOps over posting a nav at all. Four ops manage grouping by id, so an agent never has to transmit — or reconstruct — the tree:

opeffect
addNavGroupCreates a group (id, optional label / startCollapsed / pageIds). Listing pageIds moves those pages in, in that order.
removeNavGroupRemoves the grouping only. Member pages survive as top-level entries at the group’s former slot.
setNavGroupMetaEdits label and/or startCollapsed.
movePageInNavMoves one page to groupId (or null for the top level), at an optional position.

addPage also takes a navGroupId, so a page can be created directly inside a group.

The safety property differs by op kind, and it is deliberate:

  • Section-level ops (insertAfter, insertAt, move, remove, setMeta, setPageMeta) emit no nav whatsoever, so nav.json is not in the write’s file list and page groups cannot be disturbed.
  • Page and nav ops do write nav.json, from a tree run through reconcileNav against the resulting page set — so every surviving page appears exactly once and a removed one cannot linger. This is why addPage and removePage keep the sidebar correct without the caller managing it.

Note that nav is a key on the index in the wire format only. It is never persisted into index.json — the write path lifts it off before the index is validated, and IndexSchema strips it.

This is the part not to “simplify” away.

applyContentWrite re-serializes the Zod-parsed index. Zod strips unknown keys, so any writer running a package version that predates a new index key silently deletes that key from disk. Put the nav tree in index.json and the sequence is:

  1. A current editor saves a grouped site. index.json gains a nav key.
  2. A client site (or a connector deploy, or a teammate’s stale checkout) running an older @drawnagency/* version saves anything at all.
  3. That writer parses the index, does not know about nav, and writes the file back without it. Every group on the site is gone, with no error and nothing in a log.

A separate file cannot be caught by that: it is simply absent from an old writer’s file list, so it survives untouched. Combined with reconcileNav() tolerating an index.json edited by a writer that has never heard of grouping, the worst mixed-version outcome is a page drifting to the bottom of the sidebar rather than silent, unrecoverable data loss.

That failure mode is the entire reason this file exists. Folding it back into index.json would reintroduce it.