Navigation
A template declares where a menu can go. The engine decides what goes there. Neither one names the other, which is what lets a page override the site menu without the template knowing that pages can do that.
Regions
Section titled “Regions”A region is a slot in your template. You declare them in defineSite:
export default defineSite({ brand: 'Example CMS', settings, sections, navigation: { regions: [ { id: 'header', label: 'Header', default: 'menu:navigation', pageMenu: true }, { id: 'footer', label: 'Footer', default: 'menu:footerNavigation' }, ], anchors: [ { id: 'top', label: 'Top of the page', position: 'start', derived: false }, { id: 'contact', label: 'Contact', position: 'end' }, ], },});| Field | Type | Meaning |
|---|---|---|
id |
string |
The key the template reads out of view.menus |
label |
string |
What the admin calls this region |
default |
`menu:${string}` | 'none' |
Which menu fills it when the page has no opinion |
pageMenu |
boolean |
Whether a page may replace what is in it |
The key in menu:navigation is a setting key, and that setting must be a
menuEntry(). 'none' leaves the region empty by
default, which is useful for a region only some pages fill.
Rendering a region
Section titled “Rendering a region”view.menus is a Record<string, MenuLink[]> keyed by region id. Every region
you declared is present, possibly as an empty array.
---import type { Menus } from '@ouncepage/core/navigation';
interface Props { menus: Menus;}
const { menus } = Astro.props;const items = menus.header ?? [];---
{items.length > 0 && ( <nav aria-label="Main"> {items.map((item) => ( <a href={item.href} aria-current={item.current ? 'page' : undefined}>{item.label}</a> ))} </nav>)}Each link is { label, href, current }. current is true only for a link to the
page being rendered, and only when that link has no # in it, because an anchor
link on the current page is the scrollspy’s business rather than the server’s.
Page references
Section titled “Page references”A menu item stores a page id, not a path:
{ label: 'Story', page: 1, anchor: 'ounce-ourStory', href: '', enabled: true }Exactly one of page and href is set, which the schema enforces. page is a
reference resolved to the page’s current slug at render time, so renaming a page
keeps every link to it working. href is an external address and is stored
verbatim. anchor only applies with page.
The engine resolves references with a map of every page id to its slug. If you
call resolveMenus yourself, pass it, or every page-referencing item resolves to
an empty href and gets dropped:
import { resolveMenus } from '@ouncepage/core/navigation';import { pageRoutes } from 'ounce:config';
const menus = resolveMenus(ounce.navigation, settings, page, anchors, await pageRoutes());Section anchors
Section titled “Section anchors”Sections.astro wraps every rendered section in <div id="ounce-{key}">, so
each section on a page has an anchor without the template doing anything. A menu
item can point at one by setting anchor to that id.
A section type that should never appear in a menu says so on its entry:
banner: entry({ schema: bannerSchema, nav: false, form: { /* ... */ }, defaults: { /* ... */ },}),nav |
Meaning |
|---|---|
| omitted | The section is navigable and is in the menu by default |
{ default: false } |
Navigable, but starts out of the menu |
false |
Never navigable. The editor is not offered the choice. |
nav sets the type’s default. Per page, an editor can include or exclude a
section and rename its menu label; those two choices live on the page’s section
row, not on the type, and a page that has never been touched follows the type.
anchors in the navigation config declares the ids your template renders itself,
outside of any section: a <body id="top">, a contact block in the footer.
position puts them before or after the section links. derived: false keeps an
anchor out of the generated list while still offering it in the admin’s anchor
picker, which is right for something like “Top of the page” that no menu should
list but a hand-built menu might want.
Per-page menus
Section titled “Per-page menus”A region marked pageMenu: true can be overridden by the page being rendered.
The page picks one mode on its Navigation tab:
| Mode | What fills the region |
|---|---|
| (default) | The region’s default menu |
| Links to its own sections | One link per navigable section on this page, plus the declared anchors |
| A menu of its own | A list of links this page alone owns |
| No menu | Nothing |
The page never says where. It says what, and every pageMenu region on that
page gets it. A template that moves its menu, or grows a second one, needs no
content change.
A page with no page-menu regions is not offered the tab at all, so a site that never declared one does not gain an editor control it cannot use.
Stored shape
Section titled “Stored shape”pages.navigation holds the mode and, for A menu of its own, the items:
{ "mode": "own", "items": [{ "label": "Home", "page": 1, "anchor": "", "href": "", "enabled": true }] }mode is '', 'sections', 'own' or 'none'. Items use the same shape as a
site menu, so the two editors and the same validation are shared.
Unchecked anchors
Section titled “Unchecked anchors”An anchor is stored as a plain string. Nothing verifies that the id exists on
the target page, because the page may not have that section yet, or the id may
belong to the template rather than to a section. A menu item pointing at a
missing anchor renders a link that scrolls nowhere.
The ids on one page come from three places: ounce-{key} from Sections.astro,
whatever your template hardcodes, and the anchors you declared. Only the first
and third are offered in the admin picker. If your template adds an id and does
not declare it, an editor cannot select it.
Limits
Section titled “Limits”Can: declare any number of regions, fill each with any menu, let a page take over the ones you marked, and generate a menu from the sections that are actually on a page.
Cannot: let a page choose which region its menu lands in, nest a menu, or point a menu item at anything other than a page, a page plus an anchor, or an external address.