Authoring Docs
Write docs as a maintained LLM-wiki - atomic, linked, linted pages in the Fumadocs app, gated by config.docs.
The docs site is apps/docs (a Fumadocs app) gated by config.docs.enabled. It renders the /docs folder (config.docs.contentDir, see Configuration), and the same files ship as AI-agent context - so treat /docs as a maintained LLM-wiki: atomic, cross-linked, linted pages that a reader scans and an agent retrieves from.
Structure: one concept per page
Each page is the smallest useful unit, so a reader, human or AI, lands on exactly the concept they need.
- One concept per page. A focused page is easier to scan and retrieves better than a grab-bag. When a page covers two things, split it (see Restructuring).
- The path is the page's stable key.
payments/creditsis referenced by that path. Pick a clear, stable slug; renaming it breaks every inbound link and any backlink, so redirect when you must change it. - Category is the folder. Related pages share a folder (
payments/,deployment/); a top-level page has no category. Folder = sidebar group. - Prefer many small linked pages over a few large ones. Splitting and linking beats sprawling.
The cross-reference graph: no orphans
The wiki is the links, not just the pages. A page nobody links to is invisible to a reader navigating by topic and weak for retrieval.
- Every page is reachable. A new page must be linked from at least one related page (and link out to its neighbors), and listed in its folder's
meta.json. Cross-reference liberally. - Link instead of re-explaining. Point at the sibling page that owns a concept rather than restating it; that keeps each page atomic and the graph dense.
- Close the loop both ways. When page A references B, make sure B (or its section) also points back where it helps a reader going the other direction.
Health: lint the wiki
After any docs change, run:
pnpm docs:lintIt fails on broken links (a cross-link or <Card href> to a missing page), orphans (a page no other page links to), and missing frontmatter (title/description). Fix every finding before you are done.
Writing style
Readers scan and agents retrieve chunks - structure carries the facts, prose only connects them. A page that breaks a rule below gets rewritten, not excused:
- Structure over sentences. Every fact lives in a table, bullet list,
<Steps>, or code block. Prose is connective tissue only. - Two-sentence paragraphs, never stacked. At most one short paragraph per section; two prose paragraphs in a row is a wall of text.
- High-impact only. Cover what most buyers act on: what it does, how to enable it, how to change it. Narrow edge cases, internals, and design rationale get one
<Callout>line - or nothing. - ~600 words per page, code blocks excluded. Over budget: cut low-impact content first; split into a folder only when everything left is high-impact.
- Summary first. Open with one self-contained paragraph naming the subject and what the page covers - a skimmer or a retrieved chunk must survive on it alone.
- Self-contained sections. Each
##/###leads with its subject by name, never "this" or "it" referring back. - Action first, concrete always. Lead with the command, config key, or file path; link the sibling page that owns a concept instead of re-explaining it.
- Complete, not thin. Every step the reader needs is present - cutting depth is never an excuse to leave them guessing what to do next.
- Terse imperatives. Plain words, present tense, active voice. No filler ("simply", "just"), no history, no rationale.
Frontmatter
Validated against Fumadocs' pageSchema. Unlisted keys are silently dropped:
| Field | Required | Description |
|---|---|---|
title | yes | H1 + sidebar label. |
description | yes | One dense, self-contained sentence: name the subject in full and say what the page covers. Doubles as SEO, the llms.txt blurb, and the retrieval hint, so it is the single highest-value line on the page. |
icon | no | A Lucide PascalCase name shown in the sidebar. |
full | no | true renders the page full-width (no on-this-page TOC). |
---
title: Credits
description: How the numeric credit balance is spent, metered, and granted in platform billing mode.
icon: Coins
---Sidebar order with meta.json
Every folder's meta.json orders its own sidebar entries. The docs root owns one for the top-level pages; a root: true folder additionally becomes a switcher tab.
{
"title": "CLI",
"root": true,
"pages": ["index", "---Getting Started---", "quick-start", "..."]
}| Key | Effect |
|---|---|
title | Folder label, shown in the switcher dropdown. |
root: true | Marks the folder a top-level section. Fumadocs builds the switcher from every root folder and isolates the sidebar to the active one. |
pages | Slugs in display order. |
"---Label---" | A pages entry rendered as a section divider. |
"..." | A pages entry that expands to "everything else." |
defaultOpen / collapsible / icon | Optional folder display tweaks (see Fumadocs docs). |
The feature pages sit at the docs root, so its meta.json carries neither title nor root - it is the default section. Reserve root: true for a self-contained tool that earns its own tab.
Components
apps/docs/components/mdx.tsx returns the global component map (getMDXComponents): Fumadocs defaults - <Cards>/<Card>, <Callout> - plus the explicitly added <Steps>/<Step>. Use any of them in any .mdx with no import.
| Component | Use for |
|---|---|
<Cards> / <Card> | Next-step / routing grids. Doubles as graph edges out of the page. |
<Callout> | A genuine warning or prerequisite. |
<Steps> / <Step> | Ordered setup or checklists. |
Register new MDX components in getMDXComponents, never with a per-file import. Because /docs lives outside apps/docs, a bare import inside a docs page fails the build.
Cross-links
- Link to another page by its absolute path from the docs root:
[Payments](/payments). - Never use
/docs/..., a relative path, or a stale framework prefix such as/next/payments. - These links are the edges
docs:lintwalks to find orphans and broken links, and they resolve the same on the site and in the Markdown that ships as agent context.
Add a new page
.md / .mdx file in /docs, or in the folder that owns its category, with title + description frontmatter.pages in that folder's meta.json to place it in the sidebar.pnpm docs:lint (no orphans / broken links / missing frontmatter), then pnpm --filter docs dev (default port 3040) to preview.Restructuring a large page
When a page covers more than one concept, promote it to a collapsible folder of focused sub-pages:
payments/) and move the page into it as index.mdx. This keeps the original /payments URL working as the folder landing page, so no redirect is needed..mdx files (plans-and-pricing.mdx, credits.mdx, ...), each one focused concept.meta.json with title, an icon, and pages in display order. The folder renders as a collapsible group; add "defaultOpen": false to ship it collapsed.<Cards> grid linking the sub-pages, so the folder landing page is a hub in the graph rather than a wall of text.// docs/payments/meta.json
{ "title": "Billing & Payments", "icon": "CreditCard", "pages": ["plans-and-pricing", "credits", "organizations"] }Renaming or moving a page
A changed URL must never strand a reader or lose its backlinks.
- No broken links: after any move or rename, search the whole
/docstree for the old path and update every reference - sibling pages,<Cards>, andmeta.json(grep -rn "/old-path" docs).pnpm docs:lintcatches any you miss. - Redirect the old URL: add a permanent (308) redirect to the closest new page so SEO and backlinks transfer. Next applies
basePathautomatically, so write the path without it:
// apps/docs/next.config.ts
const nextConfig: NextConfig = {
// ...existing config
async redirects() {
return [{ source: "/old-page", destination: "/section/new-page", permanent: true }];
},
};index.mdx keeps the original URL, so it needs no redirect. Only redirect URLs that actually change.DOCS_NOINDEX=1 on a deployment whose docs must stay out of search - it blanket-disallows robots.txt, empties the sitemap and emits noindex, nofollow. Leave it unset otherwise; see Environment variables.Customization
Rebrand the boilerplate by editing the identity, logo, social, and SEO fields in @repo/config plus your English i18n copy - one source of truth per surface.
Troubleshooting
Symptom-to-fix table for the errors you are most likely to hit - missing env vars, background jobs not firing locally, unauthenticated server fetches, and edge-incompatible modules.