Next.js
This guide builds a working docs site from scratch using Next.js App Router and document0.
1. Create a Next.js app
npx create-next-app@latest my-docs --typescript --tailwind --app --no-src-dir
cd my-docsOr scaffold instantly with create-document0:
npx create-document0 my-docs2. Install document0
npm install @document0/core @document0/mdx @document0/next-dev shiki @mdx-js/mdx3. Configure Next.js
Add serverExternalPackages and wrap the config with withDocument0 from @document0/next-dev:
// next.config.ts
import type { NextConfig } from "next";
import { withDocument0 } from "@document0/next-dev";
const nextConfig: NextConfig = {
serverExternalPackages: ["@document0/core", "@document0/mdx", "shiki"],
};
export default withDocument0({ contentDir: "content/docs" })(nextConfig);contentDir is relative to your Next project root and must match the folder you pass to DocsSource.
4. Create your source loader
Import the content stamp once in the same file as DocsSource so the dev bundler treats your docs tree as a dependency (same idea as Fumadocs: content on the webpack graph). When anything under content/docs changes, this module re-runs and you get a fresh DocsSource.
// lib/source.ts
import "@document0/next-dev/content-stamp";
import path from "node:path";
import { DocsSource } from "@document0/core";
const rootDir = path.join(process.cwd(), "content/docs");
export const source = new DocsSource({ rootDir, baseUrl: "/docs" });5. Create a Shiki highlighter
// lib/highlighter.ts
import { createHighlighter } from "shiki";
let highlighter: Awaited<ReturnType<typeof createHighlighter>> | null = null;
export async function getHighlighter() {
if (highlighter) return highlighter;
highlighter = await createHighlighter({
themes: ["github-dark"],
langs: ["typescript", "javascript", "bash", "json"],
});
return highlighter;
}6. Create your docs page
// app/docs/[[...slug]]/page.tsx
import { notFound } from "next/navigation";
import { run } from "@mdx-js/mdx";
import * as runtime from "react/jsx-runtime";
import { source } from "@/lib/source";
import { getHighlighter } from "@/lib/highlighter";
import { processMdx } from "@document0/mdx";
export async function generateStaticParams() {
return (await source.getPages()).map((page) => ({
slug: page.slugs.filter(Boolean),
}));
}
export default async function DocPage({
params,
}: {
params: Promise<{ slug?: string[] }>;
}) {
const { slug } = await params;
const page = await source.getPage(slug ? slug.join("/") : "");
if (!page) notFound();
const highlighter = await getHighlighter();
const { code } = await processMdx(page.content, { highlighter });
const { default: MDXContent } = await run(code, {
...(runtime as object),
baseUrl: import.meta.url,
} as Parameters<typeof run>[1]);
return (
<article>
<h1>{page.frontmatter.title}</h1>
<MDXContent />
</article>
);
}7. Add a search route
// app/internal/search/route.ts
import { createSearchRoute } from "@document0/core/search";
import { source } from "@/lib/source";
export const { GET } = createSearchRoute(source);8. Add llms.txt routes (optional)
// app/llms.txt/route.ts
import { createLlmsTxtRoute } from "@document0/core/llms";
import { source } from "@/lib/source";
export const { GET } = createLlmsTxtRoute(source, {
title: "My Docs",
description: "Documentation for my project",
baseUrl: "https://docs.example.com",
});9. Add some content
mkdir -p content/docsCreate content/docs/index.mdx:
---
title: My Docs
description: Welcome to my documentation.
---
# My Docs
Hello world!Content hot reload in development
DocsSource caches pages after the first read. Editing markdown or _meta.json does not change your app/**/*.tsx modules, so by default the dev server would keep serving cached data.
@document0/next-dev fixes that in development by registering your contentDir as a webpack context dependency (via a small loader on content-stamp). Any file change under that directory invalidates the module that imports @document0/next-dev/content-stamp, which should be the same module that constructs DocsSource — so that file re-executes and reads from disk again.
Requirements:
- Run
next devwith webpack (the default in many setups). Custom webpack is not used when you pass--turbo; use plainnext devornext dev --webpackif Turbopack is your default. - Keep the
content-stampimport in the same file asnew DocsSource(...).
Scaffolds from create-document0 apply withDocument0 and the content-stamp import by default.
Vite does not use this package. For manual file watching there, use watchDocsSource from @document0/core/watch in a dev plugin (for example server.ws.send({ type: "full-reload" }) in onInvalidate). See the @document0/core package README and the React + Vite guide.
10. Run the dev server
npm run devOpen http://localhost:3000/docs. Your docs site is live.
Next steps
- Install UI components:
npx document0 add document0/sidebar document0/toc document0/search-dialog - Core package reference: all APIs for source, navigation, search, and llms.txt