I recently set up Fumadocs in an existing Next.js app and learned a few things along the way. Here are the practical notes I wish I didn’t have to dig out of the official documentation.
1. Add a custom frontmatter key
Fumadocs comes with only a few frontmatter keys, such as:
1
2
3
4
5
|
---
title: My Page
description: Best document ever
icon: HomeIcon
---
|
If you start customizing the implementation, you’ll eventually need to add custom keys. As the documentation states, you can leverage the schema in the source.config.ts
file.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
// source.config.ts
import { defineConfig, defineDocs, frontmatterSchema } from 'fumadocs-mdx/config'
import { z } from 'zod'
export const docs = defineDocs({
dir: 'content/docs',
docs: {
schema: frontmatterSchema.extend({
noIndex: z.boolean().optional(),
}),
},
})
export default defineConfig()
|
You can then use the new property in different places, such as when generating page metadata to avoid being indexed by search engines.
1
2
3
4
5
6
7
|
// app/docs/[[...slug]]/page.tsx
export async function generateMetadata(...) {
// ... get page
return {
robots: page.data.noIndex ? { index: false, follow: true } : undefined,
}
}
|
2. Add Fumadocs pages to Next.js sitemap
Fumadocs doesn’t automatically add its pages to your sitemap, but doing so is straightforward.
Fetch pages with source.getPages()
and map them to absolute URLs.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
// app/sitemap.ts
import { source } from '@/lib/source'
export default async function sitemap() {
const baseUrl = 'https://www.vinzius.com'
const docsUrls = source
// Optional: leverage the noIndex property we previously added.
// .filter(page => page.data.noIndex !== true)
.getPages().map(page => ({
url: `${baseUrl}${page.url}`,
lastModified: new Date(),
changeFrequency: 'weekly' as const,
priority: 0.7,
}))
return [...docsUrls]
}
|
3. Override MDX components (headings, links, callouts)
Wrap or replace default MDX components globally.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
// mdx-components.tsx
import defaultMdxComponents from 'fumadocs-ui/mdx'
import type { MDXComponents } from 'mdx/types'
export function getMDXComponents(components?: MDXComponents): MDXComponents {
return {
...defaultMdxComponents,
h2: (props) => (
<h2 {...props} className="scroll-mt-24 text-xl font-semibold">
{props.children}
</h2>
),
a: (props) => (
<a {...props} className="underline decoration-primary/80 underline-offset-4" />
),
...components,
}
}
|
Then pass them when rendering a page (you can still inject route-specific overrides, like relative links):
1
2
3
4
5
6
|
// app/docs/[[...slug]]/page.tsx (excerpt)
<MDX
components={getMDXComponents({
a: createRelativeLink(source, page),
})}
/>
|
4. Show your site logo in the nav
The Fumadocs installation example shows how to display text as the navigation title. Adding an image is easy because it supports a ReactNode.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
// lib/layout.shared.tsx
import Image from 'next/image'
export function baseOptions() {
return {
nav: {
title: (
<span className="inline-flex items-center gap-2">
<Image src="/assets/logo.svg" alt="My Website" width={80} height={20} className="h-4 w-auto" />
<span>Docs</span>
</span>
),
url: '/docs',
},
}
}
|
5. Ensure the Next.js runtime is used when building with Docker
If your container build fails with a type error like:
1
2
|
Type error: Cannot find type definition file for 'vite/client'.
/// <reference types="vite/client" />
|
This means the Fumadocs generator chose the Vite runtime inside the container. Make sure your Dockerfile copies both source.config.ts
and next.config.ts
before installing dependencies, so the postinstall step generates for Next.js.
1
2
|
# Copy package files first for better caching
COPY --link package.json pnpm-lock.yaml source.config.ts next.config.ts ./
|
Although the documentation mentions source.config.ts
, I found that without adding next.config.ts
it falls back to Vite.