Guides, pages & prose
A generated reference tells a reader what each endpoint does. It rarely tells them where to start, how to authenticate, or how the pieces fit together. Docuccino lets you write that narrative as plain Markdown, compile it into the same document as your reference, and link it directly to the operations and schemas it describes — with a guarantee those links can’t silently rot.
Point Docuccino at a folder
Section titled “Point Docuccino at a folder”Set content.dir to a folder of Markdown files, relative to your application root:
// config/docuccino.php — documents.default'content' => [ 'dir' => 'resources/docs/api',],Every *.md file under that folder becomes a page. The folder structure becomes your navigation:
each subfolder is a nav group by default, and the file’s location gives it a slug.
Directoryresources/docs/api/
- overview.md
Directoryguides/
- authentication.md
- pagination.md
Directorywebhooks/
- signing.md
That tree compiles to two groups — Guides and Webhooks — plus a top-level Overview page, all without any configuration. Frontmatter (next section) overrides any of it when the defaults aren’t what you want.
A relative content.dir is confined to your application root, and a directory that doesn’t exist
raises a content.dir-missing warning rather than failing the build — so a typo tells you instead of
silently producing no pages.
Frontmatter reference
Section titled “Frontmatter reference”A leading --- block sets a page’s metadata. Every key is optional — omit the block entirely and
Docuccino derives sensible defaults from the file’s name and location.
| Key | Type | Default | Behavior |
|---|---|---|---|
title |
string | File name, humanized (getting-started.md → Getting Started) |
The page’s display title. |
slug |
string | Path under content.dir, without .md |
The page’s stable slug; its identity is derived from this, so keep it stable across renames. Slugs must be unique — a collision is an error and the later page is dropped. |
summary |
string | none | A short description carried onto the page. |
tags |
string[] | none | Freeform tags carried onto the page. |
nav.group |
string | Parent folder name, humanized (no group at the root) | The sidebar group the page sits under. |
nav.order |
integer | unordered | Explicit sort weight within its group. |
nav.hidden |
boolean | false |
Compile the page but leave it out of the navigation tree. |
nav.type |
page | operation | tag |
page |
Whether the nav entry links to this page, or references an operation or a tag. |
nav.ref |
string | none | For type: operation or tag, the target — an operationId or METHOD /path, or a tag name. |
A typical page:
---title: Authenticating requestssummary: How to obtain and send a bearer token.nav: group: Guides order: 1---
Every request to a protected endpoint carries a bearer token…Ordering. Within a group, pages sort by nav.order, then title, then slug; pages with no order
come after those that have one. A group takes the lowest order among its children, so giving one
page in a folder order: 1 lifts the whole group.
Setting nav.type: operation with a nav.ref turns a nav entry into a link straight to an operation
in your reference — handy for surfacing a key endpoint in the sidebar. A nav.type: tag entry points
at a tag name (either one your operations carry, or one you defined in tags.definitions). A ref
that doesn’t resolve raises content.unresolved-nav-ref and the entry is left out, rather than
shipping a dead link.
Linking to your API
Section titled “Linking to your API”Inside a page, reference an operation or schema with a directive. Docuccino resolves it against the assembled document at build time:
Paginated results come back from ::operation{id="invoices.index"}, each item shaped like::schema{name="InvoiceData"}.::operation{id="…"}takes an operationId, or aMETHOD /pathsignature —GET /api/v1/invoices, matching the OpenAPI path template, with the method case-insensitive. The signature form is how you address a route that has no name.::schema{name="…"}takes a component schema name, as it appears undercomponents.schemas.
When the build runs, each directive is checked against the real document. A directive that resolves
gets the operation or schema’s stable UIR identity appended as a ref, so a consumer can link to it
without re-resolving:
::operation{id="invoices.index" ref="op:v1:mfz3q6k2w5r7t4ua"}A directive that points at something that doesn’t exist becomes an error diagnostic — the
directive is left as you wrote it, and your build can fail on it with --fail-on=error. This is the
whole point: your guides can’t quietly drift away from the API they describe. Rename an endpoint and
forget to update the guide, and the next build tells you.
An unknown directive name — anything other than ::operation or ::schema — passes through
untouched with a warning, so directives your own renderer understands are safe to use.
Content diagnostics
Section titled “Content diagnostics”| Code | Severity | Means |
|---|---|---|
content.dir-missing |
warning | The configured content.dir doesn’t exist. |
content.duplicate-slug |
error | Two pages resolved to the same slug; the later one was dropped. |
content.unresolved-directive |
error | An ::operation/::schema directive resolved to nothing, or is missing its selector attribute. |
content.unknown-directive |
warning | A directive Docuccino doesn’t handle; passed through untouched. |
content.unresolved-nav-ref |
error | A nav.type: operation/tag entry pointed at something absent. |
content.duplicate-operation-id |
warning | Two operations share an operationId, so ::operation{id="…"} is ambiguous. |
Symbol-anchored prose with #[DescriptionFromFile]
Section titled “Symbol-anchored prose with #[DescriptionFromFile]”There’s a second way to bring Markdown into your docs, for a different job. When the prose belongs to
one specific symbol — a controller action, or a Data class — attach it at the source with
#[DescriptionFromFile]:
use Docuccino\Attributes\DescriptionFromFile;
#[DescriptionFromFile('resources/docs/invoices/index.md')]public function index(): AnonymousResourceCollection { /* … */ }The file’s contents become that operation’s description, right where the endpoint appears in the
reference. The path is resolved within your application root, and the file joins the operation’s cache
dependencies — so editing the Markdown correctly invalidates just that fragment.
What each format keeps
Section titled “What each format keeps”The content layer is a first-class part of the UIR, but it’s a UIR feature — plain OpenAPI has nowhere to put a page tree. What survives an export depends on the format:
| Format | Guide pages & nav tree | Descriptions in standard fields |
|---|---|---|
UIR (--format=uir) |
Kept in full under x-docuccino.content |
Kept |
| OpenAPI 3.2 / 3.1 / 3.0 | Dropped — no home in the OpenAPI object | Kept (they’re standard OAS fields) |
In other words: your standalone guide pages and the navigation tree live in the UIR. Exporting to
OpenAPI drops them, because there’s no standard place for them to go. Descriptions that sit in
ordinary OpenAPI fields — info.description, tag descriptions, and per-operation descriptions
(including ones set with #[DescriptionFromFile]) — survive, because they were standard fields all
along.
Rendering the guide pages and their multi-page navigation is a job for a UIR consumer. The bundled viewer serves OpenAPI, so it shows the reference — rich descriptions and all — but not the standalone guides; the same is true of any plain OpenAPI tool you point at the export. Today the content layer is for pipelines you build on the UIR: a docs site generator, a changelog, or an agent tool catalog that wants your prose alongside your operations.