Skip to content

Oink 0.8.0 — A whole section in one fetch, the sidebar as data, and who links here

Oink 0.8.0 adds two opt-in output formats for readers that arrive as programs — a full-text bundle holding an entire section in one file, and a navigation tree published as JSON — plus static backlinks, which list the pages that link to a page in its right rail. All three stay off until you ask.

Oink 0.8.0 changes no component API and needs no content edits. Two of its three additions serve the reader that arrives as a program. Every page already publishes a .md twin, which serves an agent that knows the page it wants; an agent that wants the manual still had to crawl it a page at a time, discovering links as it went. Two new output formats answer the other half: give me the whole section, and tell me what is in the site before I fetch anything. The third addition is for the reader who is a person — a page’s right rail can now list the pages that link to it.

At a glance

  • LLMSFULL publishes llms-full.txt per top-level section: every page of the section, in sidebar reading order, in one file.
  • NAVJSON publishes navigation.json per language: the sidebar tree as data, versioned by a JSON Schema.
  • params.ui.backlinks lists the pages that link to a page in its right rail, derived at build time from the links already in your Markdown.
  • All three are opt-in and none is ever turned on for you. A site that asks for none of them builds byte-for-byte what it built before.
  • llms.txt lists whichever ones you enabled, so discovery stays in the file an agent already fetches.
  • A data/docs_nav.json node without a children key no longer crashes the build.

The full-text bundle

LLMSFULL collapses a whole section into one file: llms-full.txt at the section root, holding every page of that section concatenated in the order the sidebar and pager present them, each page introduced by a separator carrying its source URL. For an agent, /docs/llms-full.txt is one fetch where the alternative was one fetch per page plus a link graph to follow — and the result is ordered, so the section reads as a manual rather than as a bag of pages.

A section asks for it in its own front matter; the theme never adds it to a site’s output set:

content/docs/_index.md
---
title: Docs
outputs: [HTML, print, RSS, markdown, LLMSFULL]
---

Front matter outputs replaces the site-level list wholesale, so write back the formats the section already had. It is per language, so _index.zh.md repeats the line to get the Chinese bundle.

What a page contributes is the same semantic Markdown its own .md carries — not a second rendering of it. The per-page Markdown body moved into a shared partial that both outputs call, so a bundle entry is byte-identical to that page’s .md and the two cannot drift apart. Order comes from the same authority the sidebar reads: the explicit data/docs_nav.json tree where a docs or book section declares one, the weighted content tree otherwise. Pages held out of the sidebar stay out of the bundle.

A bundle belongs to a top-level section, and there is no whole-site variant: an agent that wants everything reads one bundle per section. Listing LLMSFULL further down the tree warns and emits nothing, so hugo server keeps working while a publishing build with --panicOnWarning stops there.

This site’s docs section has it enabled: https://oink.pgsty.com/docs/llms-full.txt is the entire documentation in one fetch. Details, including the file’s exact shape: Full-text bundle.

The sidebar is the site’s table of contents, and an agent that can read it plans a route before spending a single fetch on content. NAVJSON publishes it as data — navigation.json, one file per language at the language root. The site turns it on for the home page:

hugo.yml
outputs:
  home: [HTML, LLMS, NAVJSON]

The tree is not a second description of the site’s structure. It serializes the same authority the sidebar and the pager already read, through the same partial: the explicit data/docs_nav.json tree where one is declared, the weighted content tree everywhere else. A check asserts that the docs subtree flattens to exactly the page sequence the full-text bundle emits — two template paths, one authority.

Each node carries an id (the path with the language prefix removed, so the same page has the same id in every language), the absolute url, the markdown URL where the page publishes one, title, description, a kind, and its ordered children. Two properties are worth reading as promises rather than as implementation details:

  • Array order is the contract. The ordering has already been applied, and weight is never serialized — a consumer re-sorting the array would disagree with the sidebar the array came from.
  • The format is versioned. schemaVersion is 1, and the contract ships in the theme repository as schema/nav.v1.schema.json. Validate against it if you consume the file.

This site’s https://oink.pgsty.com/navigation.json is a live instance. Details, including the placeholder rows and the full key table: Navigation JSON.

A reader who lands on a page from search can see where it points and nothing about where it sits. Backlinks close that half: the pages that link to this one appear as a “Backlinks” group in the right rail below the table of contents, expanded by default; more than eight entries fold behind a disclosure. One key turns it on:

hugo.yml
params:
  ui:
    backlinks: true

A page overrides it with the front matter key backlinks, and a section cascades it to everything below.

The index is derived at build time from what you already wrote: ordinary Markdown links and ref / relref in the page source. There is no [[wikilink]] syntax to adopt, nothing to migrate, and no JavaScript — the links are in the HTML, they are in the page’s Markdown output, and they are there for a reader with scripts off. Code fences and inline code are stripped before scanning; repeated links to one target merge into one entry; self links, external links and same-page anchors never count; each language has its own graph. Order is the stable page path, so the same content always builds the same list, and when nothing links in there is no block at all.

One honest limit: reading the source misses a URL buried in a custom shortcode’s parameters or in a raw <a href>, and a destination that will not resolve is dropped quietly. This is navigation, not link checking — a link checker is still the tool for finding broken links.

This site enables it site-wide: look at the right rail of any docs page, and the most-referenced page — Configuration — lists more than forty inbound links. Details: Backlinks.

Discovery stays in llms.txt

Neither file is an alternate representation of a page, so neither appears in <head> or gains a page action. Instead llms.txt — the file an agent fetches first anyway — grows a ## Full-text bundles list of this language’s bundles, and lists this language’s navigation.json in its site index. Both entries appear only where the site actually publishes the file: the theme never points at something it did not emit.

A childless nav node no longer crashes the build

A node in data/docs_nav.json with no children key ended the build with a reflection error from inside the sidebar walker. The walker assumed every node had the key, which holds for generated JSON and does not hold for JSON someone writes by hand — where a leaf is naturally written as a node without children. Authored data now degrades instead of erroring: the childless node renders as the leaf it is.

Upgrading

hugo mod get github.com/pgsty/[email protected]
hugo mod tidy

Nothing changes until you ask for it. No component API changed and no content edits are required — the two formats are declared in outputs, backlinks are one boolean under params.ui, and a site that asks for none of the three publishes what it published under 0.7.1. The two output formats, and the shape of what they produce, are on AI-agent support; the backlink switch is on Navigation and menus.

The complete list is in CHANGELOG.md.