Choose rendering boundaries first
Server Components are the App Router default. Build document content, navigation data, and SEO metadata on the server. Keep only browser state—search dialogs, theme selection, and scroll spy—in small Client Component boundaries. This reduces initial JavaScript and serialization.
export default async function Page({ params }: PageProps) {
const { locale, slug } = await params;
const document = await getDocument(locale, slug);
return <DocumentPage document={document} />;
}MDX pipeline
@next/mdx requires a root mdx-components.tsx with App Router. Process GFM and frontmatter with remark and heading IDs with rehype. Prefer a static loader map over arbitrary dynamic import paths so tracing and 404 behavior stay analyzable.
Caching and parallelism
Start independent I/O together and await it with Promise.all. Use cache() for repeated server reads within a request. Never store mutable request or user state in module globals.
Practical tips
- Pair
generateStaticParamswithdynamicParams = falsefor deterministic 404s. - Generate canonical and
alternates.languagesfrom the same document ID. - Load search code and indexes only when search opens.
- Do not serialize entire source documents into Client Component props.
Failure cases
Making the whole layout a Client Component
A root provider does not require every descendant to become client code. Isolate a small ClientShell and pass server-rendered children through a slot.
Runtime file-system discovery
Scanning content on every request makes deployment tracing and caching brittle. Generate a manifest and search indexes before build, then load content from static module paths.