Skip to main content

Menu collection

A menu is a reusable, flat navigation defined once as data and shared across many pages. It’s an ordinary content collection of JSON files: one file is one menu, and a page opts into it with a single frontmatter field. This is how a “reference section” is built — the entries are ordinary docs pages, and the menu is just the list that ties them together.

Registering the collection

Add the menu collection to your content config alongside the docs collection. Both come from astro-pigment/content:

// src/content.config.ts
import { defineDocsCollections, defineMenuCollection } from "astro-pigment/content";

export const collections = {
  ...defineDocsCollections(),
  ...defineMenuCollection(),
};

Menu files live in src/content/menu/. The filename is the menu id: src/content/menu/api.json is the menu "api".

Attaching a menu to a page

Point a page’s menu at a menu id. That page renders the menu in its left column and its own outline on the right (a three-column layout):

---
title: "debounce"
description: Delay a function until input settles.
order: 12
menu: "api"
---

The menu item whose href matches the current path is marked active and auto-centered within the rail’s own scroll box.

Schema

A menu is a list of groups; each group is a label (or null for an unlabelled, flat group) and a list of items. A group heading can itself be a link — give it an href and it renders as a top-level link, with an empty items for a standalone link or nested items for a section landing. Each item has a title and an href:

menu      = { groups: MenuGroup[] }
MenuGroup = { label: string | null; href?: string; attrs?: LinkAttrs; items?: MenuItem[] }
MenuItem  = { title: string; href: string; attrs?: LinkAttrs }
LinkAttrs = Record<string, string | number | boolean>

Any link — group heading or item — takes an optional attrs object that is spread onto the rendered <a>: target, rel, data-*, anything. A false value drops the attribute.

An href carrying a scheme (https://, mailto:) or a leading // points outside the site: it is used verbatim and never marked active. Everything else is a site path, absolute (/api) or relative (api), resolved against the site base.

The JSON is validated at build time; a malformed menu fails the build with a clear error rather than rendering wrong.

Example

// src/content/menu/api.json
{
  "groups": [
    { "label": "Overview", "href": "/api" },
    {
      "label": "Reference",
      "items": [
        { "title": "Page frontmatter", "href": "/api/page-frontmatter" },
        { "title": "Menu collection", "href": "/api/menu-collection" }
      ]
    },
    {
      "label": "Astro docs",
      "href": "https://docs.astro.build",
      "attrs": { "target": "_blank", "rel": "noopener" }
    }
  ]
}

Here “Overview” and “Astro docs” are group-level links (no nested items), while “Reference” is a plain heading over its items. “Astro docs” is external by its href alone and opens in a new tab via its attrs. Menus are flat (one level) — nested menus are not supported; sub-structure belongs in a page’s own on-this-page outline.

Theme copied