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

PathRole
source.config.tsDefines docs, blog, and legal collections and schemas
src/lib/source.tsLoads the localized docs tree at /docs
src/lib/blog.tsSorts, filters, paginates, and finds blog/legal entries
mdx-components.tsxGlobal 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
content/docs/guides/launch.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=2 drives 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 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:

  1. Check MDX frontmatter against the schemas in source.config.ts.
  2. Check meta.json JSON and ensure every listed page/folder exists.
  3. Stop the dev server, remove the generated .source/ directory, and run pnpm dev or pnpm build to regenerate it.
  4. Run pnpm build to catch invalid content and route rendering errors.

Do not hand-edit or commit .source/; fix the source MDX/config instead.

On this page