Skip to content

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.

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.

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.mdGetting 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 requests
summary: 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.

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 a METHOD /path signature — 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 under components.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.

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.

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.