Content
Outline
Take any number of collections and an arrangement, and derive one normalized nav tree plus an address map. A bare collection arranges by convention; pick, groups, and links arrange by hand.
On this page
The outline is the one place a site’s structure is decided. It takes your collections and an arrangement spec, and derives a single normalized tree (the NavTree) plus the address map every other read function uses. Nav, prev/next, section labels, sitemap, prerender entries: all are views of this one tree.
When to use
You give site() an outline whenever a site is more than one flat list: multiple collections, hand-ordered sections, a landing link in the sidebar. For a single convention-ordered collection, pass it bare and skip this entirely.
A bare collection arranges itself
The simplest outline is a collection. When that collection is a folder(), its filenames already carry structure, and the outline reads it as data:
import { site } from 'ogygia/content';
import { guides } from './collections.server';
export const docs = site({ outline: guides });NN- prefixes order siblings and are stripped from the URL (03-routing → /routing, sorted third). A +meta.json sidecar names a section. That is all: the tree is the folder tree.
The spec grammar
When you want to arrange by hand, outline() takes a list. Every form composes with every other:
import { outline, pick, site } from 'ogygia/content';
export const docs = site({
outline: outline([
{ label: 'Start', items: pick(docs, 'overview', 'install', 'quickstart') },
docs, // a bare collection: the not-yet-placed remainder
{ label: 'API', items: reference }, // a whole collection under an explicit group
{ label: 'Changelog', href: '/changelog' } // a plain link — points anywhere
])
});- a bare collection → the remainder of it, convention-expanded.
pick(coll, ...patterns)→ a flat, ordered selection. Exact ids and*/**globs, in the order written.{ label, items }→ an explicit group holding a collection, a selection, or nested items.{ label, href }→ a plain link node, backed by no entry.() => items→ a computed subtree, the escape hatch for anything dynamic.
With pick(), the order you write is the order readers get, and a glob sweeps a subtree in convention order:
pick(docs,
'quickstart', // this exact id, first
'guides/**', // then every guide, convention-ordered
'*' // then whatever else is left at the top level
)Placement is single-assignment
Every entry lands exactly once. A bare collection consumes whatever is not yet placed, so pick() a few pages into a “Start” group and drop the collection later to sweep up the rest. Unlike a stringly sidebar config, mistakes here are named build errors, not silent gaps:
- an unknown
pickid, or a glob that matches nothing - an entry placed twice, or a slug collision
- an orphan: an entry no spec ever placed
Each fails the build with the id and the reason.
Move fearlessly
Because placement is verified, reorganizing is safe by construction: move a file, and either the tree just updates (convention placement) or the build names the pick id that no longer matches. Pair it with the link audit and inbound links are covered too.
Conventions: order and dates
Structure comes from filenames through a convention, passed to folder(). Two ship:
import { folder, numbered } from 'ogygia/content';
// NN- prefixes order siblings, +meta.json labels the section
export const docs = folder(
import.meta.glob('../content/docs/**/{+doc.svx,+meta.json}', { eager: true }),
{ convention: numbered() }
);
// 03-routing/+doc.svx → /routing, sorted thirdimport { folder, dated } from 'ogygia/content';
// YYYY-MM-DD-slug orders chronologically, date stripped from the URL
export const posts = folder(
import.meta.glob('../content/blog/**/*.md', { eager: true }),
{ convention: dated() }
);
// 2026-08-13-release.md → /release (date recoverable via dateOf())numbered() verifies siblings as it reads: mixing prefixed and unprefixed files, or inconsistent padding, is an error unless a +meta.json opts the directory out with "ordered": false. dated() recovers the date from the filename for display via dateOf(filePath) while keeping it out of the URL.
The address map
Beyond the tree, the outline knows how to resolve a slug to its entry and how to enumerate every address. Downstream read functions use this directly: docs.entries() for prerender inputs, docs.page(slug) to resolve one page, switcher() on a dimensioned site. When you build custom chrome, hrefOf(base, slug) is the one sanctioned way to turn an address into a link; it is what keeps a site correct under any mount prefix:
import { hrefOf } from 'ogygia/content';
hrefOf('/docs', 'routing'); // '/docs/routing'
hrefOf('', 'routing'); // '/routing'Prev/next
“Keep reading” comes from two dials on site():
site({ outline: guides, prevNext: 'graph', trail: 'group' });prevNext: 'order'(default): plain document order, the sidebar read top to bottom.prevNext: 'graph': follow the content graph (an entry’s declared relations), falling back to order; a page’s next is the one it links onward to.trail: 'site'(default): the trail runs through the whole site.trail: 'group': prev/next stops at the top-level section boundary, so a multi-topic site never links across topics.
The <Pager> component and docs.page()’s trail field both read whatever you set.
Do / don’t
- Do let a bare collection sweep the remainder. Place the few pages that need a specific home, and let convention handle the rest.
- Do trust the build errors; an orphan or a collision is a real structural mistake, surfaced with its id.
- Don’t hand-maintain a parallel list of pages. The outline IS the list; a new file appears in the nav the moment the glob sees it.
- Don’t encode order in a
+meta.json. Order is theNN-prefix (one channel); the sidecar is for labels only.