Shoalrun documentation framework
For agents
Start with https://www.shoalrun.com/llms.txt for the documentation index, then read the Markdown pages it links. Skip to article. Documentation index. Read this page as Markdown.
How to

Organize site navigation

Arrange guides, reference pages, and other documents into a clear site menu.

Updated
Read as Markdown

The navigation setting controls the site's left menu. It can be a list in docs.config.yaml or import a separate YAML file. Add guides, tutorials, concepts, and reference pages by their stable document IDs. Nodes appear in the order you write them.

Each node has a stable id for the menu entry. It needs exactly one of these targets:

  • doc places any page, including a guide or API reference, by its stable document ID.
  • label with items makes a group of pages or nested groups.
  • operation places an API reference by stable operation ID.
  • generated fills in API references from the registered OpenAPI source.

The menu follows document IDs rather than file paths or slugs. Moving a source file or changing a page URL does not require a navigation edit. A doc node uses the page title by default; set label when the menu needs a shorter name. Pages omitted from navigation can still be built and linked from other pages.

Arrange authored pages

- id: overview
  doc: guide.overview
- id: learn
  label: Learn
  items:
    - id: first-steps
      doc: guide.first-steps
      label: First steps
    - id: core-concepts
      doc: concept.core
- id: tasks
  label: Tasks
  items:
    - id: create-payment
      doc: guide.create-payment
    - id: handle-errors
      doc: guide.handle-errors

Each doc value must match a page's frontmatter ID. Groups can contain other groups, so a larger site can place task guides under a product area. Use a structure that matches the reader's tasks; the source directory layout does not control the menu. The build rejects missing document IDs and malformed groups. For a publication where a page has no matching channel, version, or language, its menu entry is omitted. An empty group is omitted too.

Use search on an individual page or operation node to add terms to the reader's sidebar menu filter. The filter also matches the page label and its group labels. For example, search: refunds returns helps readers find a page whose title uses different words. This setting does not add terms to the site-wide search index; site search uses page content and search metadata. Groups and generators cannot have search.

Split navigation into tabs

Use navigation tabs for parallel sections such as Guides, API reference, and Recipes. Mark a top-level group with tab: true and name its landing page by stable document ID:

config/nav.yaml
- id: guides
  label: Guides
  tab: true
  landing: guide.overview
  items:
    - { id: overview, doc: guide.overview }
    - { id: first-page, doc: guide.add-page }
- id: api
  label: API reference
  tab: true
  landing: guide.api-references
  items:
    - { id: api-intro, doc: guide.api-references }
    - { id: generated, api: notes, generated: tags }

Replace the example IDs and API name with your project's values. A tab ID uses lowercase letters, digits, and hyphens and starts with a letter. Tabs require a label, nonempty items, and a landing document inside those items. Nested tabs fail the build. A published page can belong to only one tab, or to shared navigation; placing it in both fails the build. Tabs whose items have no pages in a publication are omitted. A nonempty tab whose landing page is unavailable in that publication fails the build.

Each tab links to its landing page. The current page's URL selects its tab and sidebar on the server. Opening a direct link, following a search result, or navigating without JavaScript selects the same section. Groups outside tabs remain shared navigation. A page outside every tab shows the shared navigation with no tab selected. Existing navigation lists work without adding tabs. Tab placement does not change page URLs or publication routes.

The section links appear below the masthead on docs and wide pages. Landing and splash pages hide them by default. Set showNavigationTabs: true or false in page frontmatter to override the layout default; see page layouts. On smaller screens they use a Sections disclosure. They inherit each theme's typography and colors. Site-wide search still includes all sections; the sidebar filter searches only the current sidebar. Reader views can filter that sidebar without changing tab membership or agent discovery.

Agent discovery across tabs

Both /llms.min.txt and /llms.txt keep every published page and add links to a scoped text index for each tab and publication. Follow the published indexUrl; do not construct it from the page's URL. Each scoped index links to all of that tab's Markdown pages and back to the global index, which also includes shared pages and pages omitted from navigation.

Each member page's Markdown identifies its navigation section, stable tab ID, and scoped index. Its breadcrumbs include the tab. documents.json publishes a navigationTabs catalog and a matching navigationTab record on each member page. Records contain id, label, variant, url, markdownUrl, and indexUrl. Both search indexes carry page membership too. The generated offline documentation bundle includes the scoped indexes.

Tab selection never removes page content from Markdown, filters the published inventory, or requires browser state. Navigation tabs provide links between pages; the Tabs component groups content inside one page. Neither replaces access policy or host authorization.

Hide group page counts

Navigation groups show their page counts by default. To omit the counts from the HTML menu, set outputs.html.navigation.showCounts to false:

docs.config.yaml
outputs:
  html:
    navigation:
      showCounts: false

The setting applies to every group, including nested groups and API navigation. The menu filter still finds pages and hides empty groups. Set showCounts: true, or omit it, to show counts again. This setting does not change Markdown output.

Optionally split a large menu into files

You can keep the whole navigation in one file, as this site's config/nav.yaml does. When a menu becomes difficult to edit, an items list can include another YAML file. Shoalrun resolves each $include relative to the file that contains it, so groups can include smaller groups.

One possible layout is:

  • config
    • nav.yaml
    • nav
      • author.yaml
      • components.yaml

The files above could contain:

docs.config.yaml
navigation: { $include: ./config/nav.yaml }
config/nav.yaml
- id: author
  label: Write documentation
  items: { $include: ./nav/author.yaml }
config/nav/author.yaml
- id: components
  label: Components
  items: { $include: ./components.yaml }
- { id: first-page, doc: guide.first-page }
config/nav/components.yaml
- { id: component-index, doc: guide.components }
- { id: component-callout, doc: guide.component.callout }

Each fragment contains the list for its items, without an extra items: wrapper. The order of included items is their order in the sidebar. The build tracks included files as dependencies and rejects cycles.

To join two lists inside one group, replace the single include above with $concat:

config/nav/author.yaml
- id: components
  label: Components
  items:
    $concat:
      - { $include: ./components-basics.yaml }
      - { $include: ./components-more.yaml }
config/nav/components-basics.yaml
- { id: component-index, doc: guide.components }
- { id: component-callout, doc: guide.component.callout }
config/nav/components-more.yaml
- { id: component-tabs, doc: guide.component.tabs }
- { id: component-fields, doc: guide.component.fields }

The Components group shows the two basics first, then Tabs and Fields. Each included file is a YAML list, and $concat joins them in the listed order. Splitting files changes authoring organization only; the rendered menu uses the resolved list.

HTML, Markdown, and agents

The build resolves the navigation list before publishing either format. HTML documentation pages show the current tab's hierarchy and shared navigation as links inside expandable groups. Without tabs, the sidebar shows the full hierarchy. The active page is marked aria-current="page". JavaScript adds the sidebar filter; the links and groups remain usable without it. A page's breadcrumb trail also follows its navigation position.

The Markdown twin does not repeat the full sidebar on every page. It puts that page's breadcrumb path near the title—for example, Write documentation > Components > Callouts. The document manifest records the same ordered path as a breadcrumbs array with labels and URLs where a level has a page. /llms.txt lists every published page with a link to its Markdown twin, including pages omitted from the sidebar.

An agent can use /llms.txt to discover pages and /documents.json to recover their hierarchy, then read the Markdown pages. Changing only how YAML files are split leaves the rendered navigation and breadcrumb paths unchanged; the manifest still records the included files as build dependencies. Moving or relabeling a page changes its HTML navigation, Markdown breadcrumb, and manifest path. The node's search words help only the HTML sidebar filter; they do not affect Markdown or agent search.

Mix guides and API references

An API reference is a document too, so doc: reference.create-payment works beside a task guide. Use operation when you want to place the reference by its stable operation ID instead. That ID is independent of the OpenAPI operationId, HTTP path, and page slug.

- id: payments
  label: Payments
  items:
    - id: payment-guide
      doc: guide.create-payment
    - id: payment-api
      operation: payments.create
    - id: other-payment-api
      api: payments
      generated: tags

generated: tags groups the remaining endpoints by their first OpenAPI tag. Declared tag order and source operation order are retained only for those remaining endpoints. To override the position of a generated reference, add its stable operation ID as an explicit operation: entry at the desired point in this YAML list. You do not need to create an MDX page. An explicit doc or operation placement wins regardless of where the generator appears, so the same endpoint appears once. Leave out the generator when you want to choose every menu entry yourself. Navigation resolves separately for each channel, version, and language.

An API might tag every /Query/ route as Query, while readers look for GET /Query/widgets under Widgets. Place its stable operation ID among the widget operations:

- id: widgets
  label: Widgets
  items:
    - { id: get-widget, operation: widgets.getWidget }
    - { id: query-widgets, operation: widgets.queryWidgets }
    - { id: create-widget, operation: widgets.createWidget }
    - { id: other-widgets, api: widgets, generated: tags }

The menu shows Get widget, Query widgets, then Create widget in that order, regardless of the OpenAPI tag or source order. The id values name menu entries; the operation values select reference pages. The tag generator fills only endpoints not already placed. This works whether the reference is generated at build time or comes from an authored MDX page. For complete control, list every operation or document and omit generated: tags.

Fill an API tag group

Set api and tag on a group, then add generated: remaining inside items:

- id: reports-tools
  label: Reports tools
  api: payments
  tag: Reports
  items:
    - id: reports-rest
      generated: remaining

The build rejects unknown operation IDs, duplicate placements, and duplicate tag generators.

Local API preview

The local API preview creates one page per source operation through the same compiler. With --nav layout.yaml, its layout can use source operation IDs; the preview converts them to stable IDs in its generated config:

navigation:
  - id: featured
    label: Featured endpoints
    items:
      - operation: createPayment
  - generated: tags

The preview layout must cover every selected operation directly or through generated: tags. An authored project may intentionally omit pages from navigation.

Subject lifecycle labels

Set subjectLifecycle to beta, in-development, experimental, or deprecated in page frontmatter. Navigation and page headers show a text badge. Markdown and the document manifest carry the same status.

OpenAPI deprecated: true marks an endpoint as deprecated and cannot be overridden by an editorial label. The preview layout can also assign a label by source operation ID. Labels communicate maturity; they do not control access.

For a deprecated subject, add deprecation guidance with optional dates, replacement, and migration references. These details appear in the article and Markdown resources; navigation keeps its concise status badge.

See current limitations for the remaining navigation and authoring work.

Reference while working