A project's config starts at docs.config.yaml. The CLI looks for that name by default, and shoalrun scaffold -n searches parent directories for it. For the configuration workflow, composition rules, and debugging, start with configure your project. This page lists every top-level key. Keys with a dedicated guide link there for the full rules. For complete, working configs, see example configs.
The root file must be a YAML mapping with schemaVersion: 1. YAML is parsed strictly: duplicate keys fail. Any top-level key not listed here fails with unknown setting <key>. Keys nested inside site, apis, publications, and outputs are not all checked yet, so check spelling there carefully.
schemaVersion: 1
site:
title: Example documentation
publications:
defaults: { channel: production, version: current, language: en }
channels:
production: { pathPrefix: "" }
versions:
current: { pathPrefix: "" }
enabled:
- { channel: production, version: current }
navigation: { $include: ./config/nav.yaml }
Split and combine config files
Four directives let one config span several files. Paths are relative to the file that contains them.
$include
{ $include: ./path.yaml } replaces a value with the contents of another YAML file. The included content becomes the value at that position. If the containing file extends a preset, the normal merge rules then apply to that value. A mapping that uses $include can have no other keys. Included files can include others, and a cycle fails with Configuration cycle and the chain of files.
Move a large section into its own file:
navigation: { $include: ./config/nav.yaml }
apis: { $include: ./config/api.yaml }
Include a value at any depth, not only at the top level:
apis:
payments:
source: { kind: file, path: ./apis/payments/openapi.yaml }
operations: { $include: ./config/payment-operations.yaml }
An API source path is relative to the file declaring source.path. Here config/api.yaml points one level up to reach apis/:
payments:
source: { kind: file, path: ../apis/payments/openapi.yaml }
extends
A top-level extends: [./base.yaml] in any config file layers that file over one or more preset files, in order. Mappings merge key by key, and lists and single values replace what came before. Use it to share settings across several sites:
schemaVersion: 1
extends: [../shared/docs-base.yaml]
site:
title: Payments documentation
List several presets to layer them. Later files override earlier ones, and the current file overrides them all:
schemaVersion: 1
extends:
- ../shared/docs-base.yaml
- ../shared/brand-theme.yaml
A preset is an ordinary config file, and it can have its own extends:
schemaVersion: 1
site:
title: Company documentation
header:
links:
- { label: Support, href: "https://support.example.com" }
theme:
preset: reef
A top-level extends is always read as a list of config files. So a file you $include can't use a top-level extends for any other purpose. See the palette example in palettes.
$concat and $remove
{ $concat: [...] } appends items to a list inherited through extends. Without an inherited list, it becomes the list itself. { $remove: [name, ...] } drops inherited mapping keys, or list items by their id. $remove fails when nothing is inherited.
Add pages after the shared navigation, and drop a relationship the shared config declares:
extends: [../shared/docs-base.yaml]
navigation:
$concat:
- { id: payments, doc: guide.payments }
- { id: refunds, doc: guide.refunds }
relationships: { $remove: [legacy-overview-link] }
Remove inherited mapping keys by name. This drops a snippet and a template that the preset registers:
extends: [../shared/docs-base.yaml]
snippets: { $remove: [legal.beta-notice] }
templates: { $remove: [troubleshooting-legacy] }
$concat also accepts included lists, so each team can keep its own navigation file:
extends: [../shared/docs-base.yaml]
navigation:
$concat:
- { $include: ./config/nav-payments.yaml }
- { $include: ./config/nav-reports.yaml }
Run shoalrun config explain <key> docs.config.yaml to see which file set a value and what it replaced. The CLI reference covers the other config commands.
search
Optional. search.synonyms is a list of groups of two or more words or phrases. Each term in a group can find the others, with exact matches ranked higher. A term can appear in only one group. Use $include to keep the groups in a project-owned YAML file. The site search guide shows the format and which search paths use it.
authoring
Optional project guidance and read-only source locations. These settings do not choose a model or read external files during a site build.
| Key | Meaning |
|---|---|
directives |
Nonempty project writing instructions. |
guideFiles |
Project-relative writing-guide paths. |
exemplarFiles |
Project-relative reviewed examples. |
sourceRoots |
Named local folders for source search. Each entry has path, optional name, and optional include and exclude glob lists. |
pageSources |
Stable page IDs mapped to source file paths or globs. These seed evidence selection for known features. |
Source paths resolve from the root configuration directory. Source folders and page-source files may be outside the docs project; guide and exemplar paths remain inside it. URLs are not source folders. Git is not required. Your coding agent selects the evidence scope under its own permissions.
authoring:
sourceRoots:
- name: Application implementation
path: ../application/src
include: ["**/*.ts", "**/*.tsx"]
exclude: ["**/*.test.ts"]
pageSources:
guide.authentication:
- ../application/src/authentication.ts
Project discovery reports these configured source locations. Use your coding agent's own search and read tools, with its execution permissions, to investigate external implementation files. docs source reads only paths inside the project. See authoring tools commands for current discovery and validation commands.
site
Required.
| Key | Default | Meaning |
|---|---|---|
title |
None | Site name in the masthead and the heading of /llms.txt. |
contentDir |
content |
Page directory, relative to the root config file. Every .md and .mdx file in it is read. |
basePath |
"" |
Mount point for public pages and resources, such as /docs. Use URL path segments without escapes or dot segments. Build tools keep internal manifests at the output root; the public site is also complete inside the docs/ output subtree. |
origin |
None | The site's HTTPS origin, such as https://docs.example.com, with no path or trailing slash. Required to publish the API catalog, and makes link-preview URLs absolute. |
indexing |
true |
Set false on previews to add noindex to pages, disallow crawling in robots.txt, and omit the XML sitemap. |
updated |
None | Optional YYYY-MM-DD fallback for sitemap lastmod, especially on generated API pages. Page updated overrides it. |
favicon |
A mark drawn from the preset and palette | A local .svg or .png file. See share images and favicon. |
customCss |
None | A local .css file linked after the framework styles. See CSS. |
editionBadge |
auto |
Masthead publication label: auto shows only the channel, version, or language values that differ across publications; always or never. |
header.links |
None | Masthead links. See header. |
agentDiscovery |
Visually hidden until focused | HTML agent guidance, visual presentation, and link labels. See site.agentDiscovery below. |
footer |
None | Footer columns, bottom-row links, and a text line. See site footer. |
A minimal site needs only a title:
site:
title: Payments documentation
Keep pages in another directory, add a stylesheet, and always show the publication label:
site:
title: Payments documentation
contentDir: pages
customCss: ./styles/project.css
editionBadge: always
Publish the API catalog. origin is required once any API sets discovery.endpoint:
site:
title: Payments documentation
origin: https://docs.example.com
Add masthead links:
site:
title: Payments documentation
header:
links:
- { label: Guides, href: /start }
- { label: GitHub, href: "https://github.com/example/payments", style: button }
- { label: Sign up, href: "https://example.com/signup", style: primary }
Add a footer:
site:
title: Payments documentation
footer:
columns:
- title: Guides
links:
- { label: Get started, href: /start }
links:
- { label: Privacy, href: /legal/privacy }
text: © 2026 Example, Inc.
site.agentDiscovery
Optional mapping. It controls the early agent guidance in HTML pages.
| Key | Default | Meaning |
|---|---|---|
mode |
focus |
Controls visual presentation and whether the block is included. See the visual and agent behavior below. |
label |
For agents |
Nonempty plain text before the message and links. Shoalrun adds the colon. |
message |
None | Optional plain-text guidance. HTML tags are escaped; Markdown syntax stays literal. |
links.article |
Skip to article |
Label for the generated #main link. |
links.index |
Documentation index |
Label for the generated /llms.txt link, including site.basePath. |
links.markdown |
Read this page as Markdown |
Label for the current page's Markdown twin. |
site:
title: Payments documentation
agentDiscovery:
mode: visible
label: For agents
message: Use the Markdown version for complete examples and nested schemas.
links:
article: Jump to the article
index: Browse all documentation
markdown: Read Markdown
Only link labels are configurable; Shoalrun generates their destinations for each page. Labels must be nonempty strings. Unknown keys and invalid mode values fail validation and builds.
The mode controls visual presentation and whether the instruction is included:
| Value | Visual presentation | Agent reading HTML |
|---|---|---|
focus |
Visually hidden until a link receives keyboard focus. | The instruction and links are present in the HTML. |
visible |
Always shown as a banner. | The same instruction and links are present in the HTML. |
disclosure |
A compact header toggle, closed initially. | Links remain in HTML source; open it for visual navigation. |
off |
No agent guidance block appears. | The instruction and its links are omitted from the HTML. |
Screen readers and agents parsing raw HTML can encounter the block in all enabled modes. An agent using screenshots sees the same visual presentation as a human. Some HTML extraction tools discard visually hidden elements; the document head also provides discovery links. Visually hiding the block does not make it private.
This setting affects HTML only. It does not change the Markdown index link, generated discovery files, or the standard accessibility skip link. The focus and visible blocks follow that skip link before the header. Disclosure mode places the toggle inside the header. It is hidden in print.
theme
Accepts preset, palette, mode, toggle, and fonts. See themes, palettes, and color modes. The build checks palette colors for WCAG contrast.
| Key | Default | Values |
|---|---|---|
preset |
shoal |
shoal, reef, ledger, vice |
palette |
The preset's palette | kelp, coral, ink, harbor, amethyst, vice, or a custom mapping |
mode |
light |
light, dark, system |
toggle |
true with mode: system, otherwise false |
true or false. true is allowed only with mode: system. |
fonts |
The preset's bundled fonts | system, or a mapping of body, headings, and code. See Fonts in themes. |
Change the layout and keep its colors:
theme:
preset: ledger
Follow the reader's system setting and let them switch:
theme:
preset: reef
palette: harbor
mode: system
Use a built-in palette as a base and change the brand colors:
theme:
palette:
extends: harbor
light: { accent: "#0a6a48", link: "#0a6a48" }
dark: { accent: "#4fd6a4", link: "#7fe0bd" }
Use dark mode only, without a switch:
theme:
mode: dark
publications
Required. Declares which channel, version, and language combinations to build. See how Shoalrun works for the concepts.
These settings select page variants within a build. For separate branches and domains, see use Git branches for separate docs sites.
| Key | Meaning |
|---|---|
enabled |
At least one { channel, version, language } entry. language is optional and defaults to the default language. |
channels.<name>.pathPrefix |
Route prefix for a channel. Every channel used in enabled must be declared, even with pathPrefix: "". |
versions.<name>.pathPrefix |
Route prefix for a version. Same rule as channels. |
defaults.channel, defaults.version |
The publication that uses each API's top-level source, and the defaults for scaffold and api select. Required when the project has apis. |
defaults.language |
The default language, which gets no route prefix. Global frontmatter can override it. Falls back to en. |
externalSites |
Optional links in the Version menu. Each entry needs a label and HTTP(S) baseUrl, which links to that site's root. Add a local manifest file containing its built documents.json to link only matching document IDs at their published paths. |
For a page in several enabled combinations, set its frontmatter publications to a list of { channel, version } pairs. This is separate from apis.<name>.publications, which binds API source files. A page without a list uses its resolved single channel and version. A list may include a declared but disabled combination; that copy is omitted from the build. See share pages across documentation versions for an example with version-specific sections.
A single publication with no prefixes. Pages set channel: production and version: current:
publications:
defaults: { channel: production, version: current, language: en }
channels:
production: { pathPrefix: "" }
versions:
current: { pathPrefix: "" }
enabled:
- { channel: production, version: current }
Two API versions side by side. A v1 page at /guides/start is published at /v1/guides/start:
publications:
defaults: { channel: production, version: v2 }
channels:
production: { pathPrefix: "" }
versions:
v1: { pathPrefix: /v1 }
v2: { pathPrefix: /v2 }
enabled:
- { channel: production, version: v1 }
- { channel: production, version: v2 }
A sandbox channel, with a version that is declared but not yet built. Leave a combination out of enabled to stop publishing it without deleting its pages:
publications:
defaults: { channel: production, version: v1 }
channels:
production: { pathPrefix: "" }
sandbox: { pathPrefix: /sandbox }
versions:
v1: { pathPrefix: "" }
v2: { pathPrefix: /v2 }
enabled:
- { channel: production, version: v1 }
- { channel: sandbox, version: v1 }
English at the root and German under /de. Pages without a language are English:
publications:
defaults: { channel: production, version: current, language: en }
channels:
production: { pathPrefix: "" }
versions:
current: { pathPrefix: "" }
enabled:
- { channel: production, version: current }
- { channel: production, version: current, language: de }
navigation
A list of sidebar nodes. Each node has a unique id and exactly one of doc, operation, generated, or items. Unknown node keys fail. See navigation.
| Key | Meaning |
|---|---|
id |
Unique node ID. Required. |
doc |
A page ID. |
operation |
A stable operation ID; shows that operation's reference page. |
items |
Child nodes, optionally loaded from another YAML list with $include; makes the node a group. Groups need a label. |
label |
Menu text. Defaults to the page title for doc and operation nodes. |
search |
Extra words for the sidebar navigation filter, separate from site-wide search. Allowed on page and operation nodes, not groups or generators. |
api |
Limits a group or generator to one API. Children inherit it. |
tab |
Set true on a top-level group to give it a navigation tab and sidebar. Requires landing; nested tabs are rejected. |
landing |
Stable ID of a page inside the tab's items. The tab links to that page in each publication. |
tag |
An OpenAPI tag for a group that uses generated: remaining. |
generated |
tags or remaining. Adds generated reference pages. |
Pages and groups:
- id: start
doc: guide.overview
- id: guides
label: Guides
items:
- id: first-payment
doc: guide.create-payment
label: Your first payment
search: checkout charge
- id: errors
doc: guide.handle-errors
Mix authored pages, a pinned API operation, and generated reference pages grouped by OpenAPI tag:
- id: payments
label: Payments API
api: payments
items:
- id: payment-guide
doc: guide.create-payment
- id: create-payment
operation: payments.create
- id: other-payment-pages
generated: tags
Fill one hand-made group with the remaining operations for a tag:
- id: reports
label: Reports
api: payments
tag: Reports
items:
- id: report-guide
doc: guide.reports
- id: report-operations
generated: remaining
redirects
A list of old URLs to send to a current page. Shoalrun's own docs use redirects to preserve links when changelog entries are combined.
Add a redirect when a URL that readers, bookmarks, or other sites already use would stop working:
- You change a page's
slug. The slug sets the URL, so the old path no longer has a page. - You change
site.basePath, a channel or versionpathPrefix, or a language prefix. Every route under it moves. - You delete a page, merge two pages, or replace a page with one in another version.
You don't need a redirect when a page keeps its slug. Moving or renaming the source file doesn't change a URL, and neither does changing navigation. The URL comes from the slug, not the file.
List the redirects under redirects in docs.config.yaml, or keep them in a file and include it:
redirects: { $include: ./config/redirects.yaml }
The versioned API documentation example shows a complete config with a redirect. Each entry looks like this:
- from: /guides/old-payment
to: { doc: guide.create-payment, channel: production, version: current, language: en }
Use a different status, such as 301 for clients that don't support 308:
- from: /getting-started
to: { doc: guide.overview, channel: production, version: current, language: en }
status: 301
Send an old URL to a page in another publication, such as a retired v1 page to its v2 version:
- from: /v1/guides/webhooks
to: { doc: guide.webhooks, channel: production, version: v2, language: en }
Because the target is a page ID, the redirect follows the page when its slug changes. Nothing else needs updating.
| Key | Meaning |
|---|---|
from |
Old path, starting with /. Compared without regard to case; a duplicate or a path that belongs to a current page fails. |
to |
Target page ID and its full publication: doc, channel, version, and language are all required. |
status |
HTTP status: 301, 302, 303, 307, or 308. Defaults to 308. |
The build resolves each target to its current route and writes redirects.json. shoalrun serve and the Vercel build apply the redirects, with or without a trailing slash. The old page's .md URL redirects to the target's Markdown twin with the same status.
apis
A mapping from API ID to its source and operations. See API references and focused contracts.
| Key | Meaning |
|---|---|
source.kind |
Must be file. |
source.path |
Local OpenAPI 3.0 or 3.1 file, relative to the declaring config file. |
source.focused |
true writes a focused contract for each reference page. |
source.publish |
true publishes the full source file, which must end in .yaml, .yml, or .json. Also turns on focused contracts. |
publicReview.digest |
Required SHA-256 review digest when API content appears on a public page or in a focused contract. Generate a candidate with shoalrun api review <config> --output <local-directory>, inspect it, then pin the reported digest. |
discovery.endpoint |
Public HTTPS API URL for /.well-known/api-catalog. Needs source.publish and site.origin. |
operations |
A list of { id, sourceOperationId, reference } bindings. Stable IDs are unique across all APIs. |
publications |
A list of { channel, version, source, discovery } entries for publications other than the default. |
generateReferences |
true, or { pathPrefix, include } or { pathPrefix, exclude }. pathPrefix defaults to /reference/<apiId>. |
mappings.subjectLifecycle |
Explicitly map an operation-level x-* scalar or dotted property to beta, in-development, experimental, or deprecated. See OpenAPI extension mappings. |
querySyntax |
Opt-in rules for a placeholder query parameter whose filter expressions go directly in the URL. Each rule has parameter, pathPrefixes, style: unwrapped, and example. See API generation. |
One authored reference page for one operation. The spec stays private:
payments:
source: { kind: file, path: ../apis/payments/openapi.yaml }
operations:
- id: payments.create
sourceOperationId: createPayment
reference: reference.create-payment
Publish the spec, a focused contract per page, and the API catalog. This also needs site.origin:
payments:
source: { kind: file, path: ../apis/payments/openapi.yaml, publish: true }
discovery:
endpoint: https://api.example.com
operations:
- { id: payments.create, sourceOperationId: createPayment, reference: reference.create-payment }
Keep the spec private but publish focused contracts:
payments:
source: { kind: file, path: ../apis/payments/openapi.yaml, focused: true }
operations:
- { id: payments.create, sourceOperationId: createPayment, reference: reference.create-payment }
Generate reference pages for every operation except two:
payments:
source: { kind: file, path: ../apis/payments/openapi.yaml }
generateReferences:
pathPrefix: /api-reference/payments
exclude: [internalReconcile, debugEcho]
Give the v2 publication its own spec. The top-level source covers the default publication:
payments:
source: { kind: file, path: ../apis/payments/v1.yaml }
publications:
- channel: production
version: v2
source: { kind: file, path: ../apis/payments/v2.yaml, publish: true }
operations:
- { id: payments.create, sourceOperationId: createPayment, reference: reference.create-payment }
audit
Optional local audit settings. Run shoalrun check [config] or shoalrun audit [config] to execute configured tests and build a temporary site. The checks compare API representations, inspect focused contracts, and validate local links. check prints readable results (--json selects JSON); audit returns JSON. Both exit with code 1 on failure.
audit:
tests:
- id: project-tests
run: [node, --test, scripts/docs.test.mjs]
api: true
focusedContracts: true
links: true
audit.tests is the integration point for external test suites. We recommend Doc Detective for documentation behavior tests; any command that exits nonzero on failure can be used. Each test needs a unique lowercase id and a nonempty run argument list. The first argument names the executable; the rest are passed without a shell. Commands run from the directory containing docs.config.yaml, so use project-relative paths. The api, focusedContracts, and links switches default to true. Set any to false to skip that group. The site build always runs because it checks the complete project and supplies focused contracts. See audit a docs project for a Doc Detective example, execution order, output, timeouts, and limits.
For snapshot validation, declare local check dependencies with optional inputs paths relative to the directory containing docs.config.yaml:
audit:
tests:
- id: formatting
run: [vale, --config=../format.ini, content]
inputs: [../format.ini, ../styles]
The snapshot copies declared files and directories within the project parent. It rejects missing inputs, symlinks, credentials, dependency folders, and paths outside that boundary. Directory declarations also track added and deleted files for freshness. Declare the configuration's supporting files too; Shoalrun does not infer dependencies from command arguments. Ordinary check and audit commands still run in the original project.
cookbooks
Optional mapping of cookbook IDs to a chooser title and ordered reader options. Each option has a unique id, label, and doc ID for a public type: recipe page in the same publication as the chooser. The build rejects duplicate options and unavailable targets. Set mode: single for alternative goals or use mode: multiple (the default) for compatible recipes. Optional option fields when, needs, and outcome explain each choice. The browser loads selected published recipe content in configured order and supports selection links, Markdown downloads, and printing. The CookbookChooser reference shows a live example and the HTML and Markdown behavior.
cookbooks: { $include: ./config/cookbooks.yaml }
relationships
A list of authored links between pages, with id, from, to, type, reason, and optional order. type is prerequisite, next-step, reference, advanced, see-also, or not-confused-with. See related reading.
A reading path through three pages:
- id: auth-before-payment
from: guide.create-payment
to: guide.authentication
type: prerequisite
reason: Create an API key before sending a payment request.
- id: payment-then-refund
from: guide.create-payment
to: guide.refunds
type: next-step
reason: Refund a payment that was created by mistake.
Order links in a group. Lower numbers come first:
- id: payment-reference
from: guide.create-payment
to: reference.create-payment
type: reference
reason: Check every request field and error code.
order: 1
- id: payment-errors
from: guide.create-payment
to: guide.handle-errors
type: reference
reason: Look up what each decline code means.
order: 2
Two-way links. Declare see-also and not-confused-with once; both pages show them:
- id: refunds-vs-disputes
from: guide.refunds
to: guide.disputes
type: not-confused-with
reason: A refund is sent by you; a dispute is started by the cardholder's bank.
variables
A mapping of project-wide values for snippets. They override snippet defaults and are overridden by page variables and invocation values. Values are strings, numbers, or booleans.
variables:
product: Payments
supportEmail: support@example.com
retryLimit: 3
betaFeatures: false
A page overrides a project value for itself:
variables:
product: Payments sandbox
A single include overrides both:
<Snippet id="payment.request-prep" values={{ product: "Payments test mode" }} />
snippets
A mapping from snippet ID to { file, variables }. Each variable has a type (string, number, or boolean) and optional required, default, and enum. See snippets.
A snippet with no variables:
legal.beta-notice:
file: ../snippets/beta-notice.mdx
Required and default values:
payment.request-prep:
file: ../snippets/request-prep.mdx
variables:
product: { type: string, required: true }
header: { type: string, default: Idempotency-Key }
A value limited to a list, and a number:
limits.rate:
file: ../snippets/rate-limit.mdx
variables:
plan: { type: string, enum: [free, team, enterprise], default: free }
perMinute: { type: number, required: true }
callouts
Custom callout types and color tones. A type has a label, a tone, and an optional Lucide icon. A tone has line, background, and text colors, and an optional dark set. See callouts.
A new type that uses a built-in tone:
types:
security:
label: Security
tone: warning
icon: shield-alert
A new tone with light and dark colors, used by a new type:
tones:
laboratory:
line: "#087f8c"
background: "#e9f8f8"
text: "#075c66"
dark: { line: "#3cb9c4", background: "#0e2a2d", text: "#8fe3ea" }
types:
experiment:
label: Experiment
tone: laboratory
icon: flask-conical
Relabel a built-in type:
types:
note:
label: Good to know
tone: note
badges
badges.variants adds or overrides inline badge colors. Each lowercase kebab-case name needs light.background and light.text hex colors; light.border is optional and defaults to the text color. Add the same fields under dark when the site uses dark or system mode. The build checks badge text contrast in every shown mode. See Badges for the built-in variant table and a separate custom partner example.
badges:
variants:
partner:
light: { background: '#e5f5f3', text: '#175b57', border: '#4c9992' }
dark: { background: '#102e2d', text: '#a6e7df', border: '#4caaa1' }
templates
An optional mapping from template ID to { type, body, required }. Built-in templates need no registration; reuse a built-in ID to override it or add a custom ID. type becomes the new page's type, and body is a .md or .mdx file that can open with commented frontmatter. The only required value is operations. See templates.
how-to:
type: how-to
body: ../templates/how-to.mdx
Override the built-in API reference while retaining its operation requirement:
api-reference:
type: api-reference
body: ../templates/api-reference.mdx
required: [operations]
Replace a built-in template by reusing its ID:
guide:
type: guide
body: ../templates/our-guide.mdx
readerFilters
Optional reader views for one site. facets maps each stable facet ID to a reader-facing label and an options map of stable choice IDs to labels. typeDefaults optionally marks all pages of a document type unless a page supplies a value for that facet. controls selects sidebar and header pickers; module names a local browser module for custom controls. The first visit shows all content. See reader views for page and section marks, controls, saved choices, and share links.
readerFilters:
facets:
audience:
label: Reader
options: { ui: Use the UI, developer: Build with the API }
typeDefaults:
api-reference: { audience: [developer] }
frontmatter
Global defaults, enforced values, per-language formatting locales, custom fields declarations, and changelogIdentifier. Pages can replace defaults but not enforced values. id and slug can't be set globally. See frontmatter and release notes and changelog.
Let pages leave out common values:
defaults:
type: guide
channel: production
version: current
access: { policy: public }
Force a value on every page:
enforced:
audiences: [partners]
Choose what identifies each changelog entry. The default is either; date and version are also available:
changelogIdentifier: either
Here version means the page's releaseVersion field, not the documentation publication version.
Declare custom fields. public: true lets a field appear in the Markdown twin's header:
fields:
owner:
type: string
required: true
description: Team that maintains the page.
agent.intent:
type: string
public: true
reviewStatus:
type: string
enum: [draft, reviewed, approved]
mcp
Enable a read-only HTTP MCP endpoint on Node or in Vercel output for one public publication:
mcp:
enabled: true
access: public
path: /mcp
publication: { channel: production, version: current, language: en }
path defaults to /mcp and is mounted beneath site.basePath. The build rejects conflicting output routes. enabled must be a boolean, and enabled configurations require access: public and all three publication coordinates.
Optional allowedHosts lists exact hostnames without ports or wildcards. Optional allowedOrigins lists exact HTTP(S) browser origins. site.origin contributes its hostname and origin automatically; the local Node server also permits loopback requests. Unknown settings fail validation.
Omit the section, set mcp: false, or set enabled: false to disable it. The Node server reads generated settings, and shoalrun vercel build packages an independent function automatically. Read-only SDK calls have been verified on this site’s production Vercel endpoint. See connect an MCP client for setup and request limits.
Optional mcp.analytics.module names a local .js or .mjs server adapter exporting capture(event) and optional flush(). It resolves relative to the declaring config file. The build copies the module and its dependencies without executing it. Relative imports must stay within its directory. Credentials belong in runtime environment variables.
Optional mcp.analytics.consent contains header and value. When configured, only requests with that exact header value can load or call the analytics adapter. Other requests can use the documentation tools without tracking. The header must be a lowercase custom x- name; forwarded, Vercel, and MCP protocol headers are reserved. The value is a case-sensitive policy identifier of up to 64 letters, digits, dots, underscores, or hyphens.
The header gate is off when omitted or set to false; readers then need no special client configuration for analytics. Use the gate only when your deployment requires explicit caller opt-in. In that mode, removing the header stops collection on future requests. This feature does not determine your privacy obligations. See analytics safeguards.
Optional mcp.analytics.clientMetadata: true adds normalized client labels and model labels from a fixed registry when supported request metadata is present. It defaults to false. Labels are unverified; raw headers, unrelated metadata, queries, and intent are excluded.
Optional mcp.analytics.intent and mcp.analytics.conversationIds are booleans that default to false. An adapter’s optional instrument(server, options) hook implements them. The PostHog example can add context/model arguments and conversation handles while excluding raw queries and results. Its context argument requests a fixed intent category. Free-text or invalid values are replaced with a per-tool category before analytics delivery; raw intent is never stored by that example. Other adapters must implement their own safeguards.
Omit mcp.analytics or set it to false to disable collection. It is independent of browser analytics and does not instrument stdio. See measure MCP tool usage for fields, service mappings, and a PostHog example.
analytics
Analytics is disabled by default. Configure a local browser adapter to receive interaction events:
analytics:
module: ./analytics.js
The adapter must export track(event) and can export an asynchronous init(context). module resolves relative to the declaring config file and is copied to the public assets without being executed during the build. analytics: false disables it. Shoalrun emits documented events; your adapter owns the service, consent, and delivery. See bring your own analytics.
metadata
metadata.provider.module names an optional local module that exports provideMetadata(context). The module can supply owned page fields to the build. The framework does not run an external vocabulary validator or publish supplied fields automatically. See custom metadata for the return shape and ownership rule.
metadata:
provider:
module: ./metadata/provider.mjs
The path is relative to the config file that declares it. provider accepts only module.
outputs
Use outputs to configure the HTML, Markdown, discovery, and text artifacts produced by a build.
| Key | Meaning |
|---|---|
markdown.metadata |
The Markdown twin's YAML header: format, fields, addFields, removeFields, omitNull, byType, and profiles. format defaults to none. See Markdown metadata. |
html.shareImages |
true: a generated 1200×630 link-preview image per page. false turns them off; a mapping with template and fonts replaces the design. See share images and favicon. |
html.highlighting |
Code highlighting provider, theme, and module. Defaults to Shiki with the site theme, which takes code colors from the palette. See code blocks. |
html.navigation.showCounts |
Show page counts beside navigation groups. Defaults to true; set false to omit them. See navigation. |
html.search |
Search provider, module, and index. See search. |
text |
Ordered frontmatter field paths for card, pageHeader, search, htmlDescription, and socialCard. See custom metadata. |
The build always writes /llms.txt, documents.json, redirects.json, and resource-headers.json; there are no settings for them.
A short Markdown header on every page:
outputs:
markdown:
metadata:
format: yaml-frontmatter
fields:
id: frontmatter.id
title: frontmatter.title
summary: frontmatter.summary
description: frontmatter.description
canonical: publication.canonicalUrl
Add fields for one page type, and define a profile pages can select with markdownMetadata: versioned:
outputs:
markdown:
metadata:
format: yaml-frontmatter
fields: { id: frontmatter.id, title: frontmatter.title }
byType:
api-reference:
addFields: { operations: frontmatter.operations }
profiles:
versioned:
addFields: { version: publication.version, language: publication.language }
Use another Shiki theme, and turn off search:
outputs:
html:
highlighting: { provider: shiki, theme: github-light }
search: { provider: none }
Plug in your own search and highlighter modules:
outputs:
html:
highlighting: { provider: custom, module: ./highlight.mjs }
search: { provider: custom, module: ./search/hosted.js }
Related reading
Read first
Related topics
Agent discovery index
Every published page is included automatically, using its title and frontmatter summary. You do not need to list page IDs. Omit outputs.llms for automatic grouping, or choose a bare alphabetical list of links and summaries:
outputs:
llms:
format: flat
format accepts grouped (the default) or flat. Flat mode includes all pages under Pages, without optional groups or repeated type labels. Discovery resources and API contracts remain linked, and fictional labels still apply. Grouping overrides are optional; primaryIds is an exception list, not an inventory.
outputs.llms controls grouping in /llms.txt. The build groups primary pages by type, puts secondary pages under ## Optional, and lists /search-index.json under Discovery only when that file is generated. The complete manifest remains available for inventory. fictionalPrefixes also sets demo: true on matching pages in documents.json, search-index.json, and search-index.min.json. Matching uses the authored slug, including descendants, independently of publication or site path prefixes. Other pages receive demo: false, meaning unmarked rather than verified as a live service.
outputs:
llms:
primaryIds: [guide.components]
optionalTypes: [recipe, ingredient, cookbook]
optionalPrefixes: [/author/components]
fictionalPrefixes: [/example-api]
primaryIds keeps named document IDs in their primary type section even when an optional type or prefix matches; fictional examples remain optional. Entries sort alphabetically within each section or optional group. Type labels appear only in the mixed optional groups. optionalTypes defaults to recipe, ingredient, and cookbook. Both prefix lists default to empty. Prefixes match page slugs before publication and base-path prefixes, on path boundaries. Fictional entries are always optional, labeled in the index, and carry a disclaimer in their Markdown twins. Use fictional prefixes only for synthetic examples, not real product API references.
Content freshness
site.freshness: { history: ./config/content-updates.json } opts into persistent content-digest dates. Paths resolve from the root config directory and must stay inside the project. The first build seeds declared dates; changed content later receives today's UTC date while unchanged content retains its date. Commit the history file for reproducible clean builds. Without this setting, declared updated and site.updated remain authoritative. Native page verified is an independent, author-set calendar date. See revision and verification dates.
Agent evaluation reports
audit.evals opts into checking a reviewed task report after the audit's temporary build. Set tasks to a YAML or JSON task file and report to a JSON result file. Both paths resolve relative to the file declaring them. Optional run is a nonempty command/argument list. It runs without a shell from the root config directory after a successful build. The timeout is two minutes; environment variables supply eval paths and digests. It can call external services and write the report; a nonzero exit fails independently of report validation. No evaluator is built into Shoalrun. See evaluate agent tasks for the report format, environment variables, and CLI commands.