Docs, Blog & Legal Content
Author documentation, blog posts, and legal pages with Fumadocs MDX.
The template keeps documentation, blog posts, and legal pages in versioned MDX files. Fumadocs compiles them into typed collections consumed by App Router pages—no CMS or content database is required.
Important files
| Path | Role |
|---|---|
source.config.ts | Defines docs, blog, and legal collections and schemas |
src/lib/source.ts | Loads the localized docs tree at /docs |
src/lib/blog.ts | Sorts, filters, paginates, and finds blog/legal entries |
mdx-components.tsx | Global default Fumadocs MDX component map |
content/docs/ | Documentation tree and meta.json navigation |
content/blog/ | Blog post collection |
content/legal/ | Legal page collection |
.source/ | Generated collection modules and types (gitignored) |
Documentation
defineDocs({ dir: "content/docs" }) registers the docs collection.
src/lib/source.ts loads it with baseUrl: "/docs" and declares en and zh
languages. The catch-all page resolves content with:
const page = source.getPage(params.slug, params.locale);Add a bilingual page
For a page named guides/launch, create both files:
content/docs/guides/launch.mdx
content/docs/guides/launch.zh.mdx---
title: Launch
description: Prepare your product for release.
---
## Checklist
Your English content.Use the same slug for both languages. The unsuffixed file is English and
.zh.mdx is Chinese. This bilingual naming applies to the docs collection;
the current blog and legal collections are not configured as localized
collections, so the same entries render inside both locale route shells.
Control navigation with meta.json
The root content/docs/meta.json orders top-level sections and folders; nested
meta.json files order pages inside a folder. Separators use labels such as
"---Essentials---":
{
"title": "Guides",
"pages": ["index", "launch", "---Advanced---", "operations"]
}The shipped template has one documentation root. Fumadocs can represent
additional roots by placing a meta.json with "root": true in another
top-level docs folder, but exposing a polished root switcher also requires a
corresponding docs-layout customization. Do not add a root or navigation slug
until its pages (and required translations) exist.
MDX components
mdx-components.tsx returns Fumadocs' default component map. The docs, blog,
and legal renderers also pass defaultMdxComponents to their compiled body.
Default elements such as Callout, Cards, and Card are therefore available.
To add a non-default component, export it in a shared component map and pass that map to every renderer that should support it—docs, blog, and/or legal. Updating only one renderer creates content that works on one surface but fails on another.
Blog
Blog frontmatter is validated by the blog schema:
---
title: Shipping the First Version
description: What we learned while launching.
date: 2026-06-23
author: Leo
tags: [Product, Launch]
image: /images/blog/launch.jpg
---title and date are required. description, author, and image are
optional; tags defaults to an empty array.
src/lib/blog.ts provides the behavior:
- Posts are sorted newest-first by
date. getBlogPostsByTag(tag, page)filters by an exact tag and paginates six posts per page.getAllTags()counts tags and returns the ten most-used by default./blog?tag=Product&page=2drives filters and pagination./blog/[slug]finds a post by its file path, renders its body and TOC, and generates static params/metadata.
The file path becomes the slug after removing .mdx; avoid duplicate paths.
Legal pages
Legal frontmatter uses this shape:
---
title: Privacy Policy
description: How we handle personal data.
date: 2026-06-23
published: true
---title and date are required. description is optional and published
defaults to true. Pages render through /legal/[slug].
published is currently parsed but not enforced. getLegalPages() returns
the whole collection and getLegalPage() can find an entry with
published: false, so that flag does not keep a page private.
Filter both list/static-param and single-page lookups before using the field as a publishing control:
export function getLegalPages() {
return legal.filter((page) => page.published);
}
export function getLegalPage(slug: string) {
return getLegalPages().find((page) => getLegalSlug(page) === slug);
}Generated .source directory
Fumadocs generates .source/ during development/build. It is intentionally
gitignored and mapped in tsconfig.json for imports such as
fumadocs-mdx:collections/server.
If collection imports or generated types are missing:
- Check MDX frontmatter against the schemas in
source.config.ts. - Check
meta.jsonJSON and ensure every listed page/folder exists. - Stop the dev server, remove the generated
.source/directory, and runpnpm devorpnpm buildto regenerate it. - Run
pnpm buildto catch invalid content and route rendering errors.
Do not hand-edit or commit .source/; fix the source MDX/config instead.
