Migration guide
Migrate a Fumadocs or custom Geist docs site to package-backed Geistdocs
Migrate an existing Fumadocs or custom Geist documentation site to @vercel/geistdocs by moving shared behavior into package APIs and keeping site-specific adapters local. The safest migration keeps content, routing decisions, and product-specific UI in your app while replacing copied runtime code with thin package-backed adapters.
Before you migrate
Create a working branch and inventory the current site before replacing code:
- Existing Fumadocs collections in
source.config.ts. - Public route families, such as
/docs,/api-reference,/guides, or/. - Existing
middleware.tsorproxy.tsbehavior. - Static files that overlap package routes, such as
public/llms.txt. - Search, chat,
llms.txt, page-level Markdown,sitemap.md,agents.md,/.well-known/mcp.json, RSS, and Open Graph routes. - Tailwind CSS version and custom plugins.
- Direct app usage of
aior@ai-sdk/reactoutside Geistdocs. - Environment variables required by the homepage, docs routes, or API routes.
Run the current project before changing it:
pnpm install
pnpm buildFix unrelated build failures first. A migration is safer when the starting point is reproducible.
Install Geistdocs
Add @vercel/geistdocs and align the supported Fumadocs and Next.js dependencies for the package version you are adopting.
For an existing project, use your package manager directly:
pnpm add @vercel/geistdocsIf you are starting from a generated Geistdocs project and moving content into it, use the CLI:
pnpm dlx @vercel/geistdocs init --name my-docsAlign Ask AI dependencies
Geistdocs Ask AI uses AI SDK v6. Generated projects install ai v6 and @ai-sdk/react v3 so package-owned chat components and route helpers use the supported AI SDK APIs.
If the existing app imports ai or @ai-sdk/react for product-specific features, migrate that code separately. Keep Geistdocs route adapters package-backed, and do not copy package chat internals into the app to preserve older AI SDK behavior.
Update source config
Use the source-config-safe export from @vercel/geistdocs/source-config in source.config.ts. This file is evaluated by fumadocs-mdx during dependency installation and builds, so avoid importing runtime component entry points from it.
import {
defineGeistdocsSourceConfig,
geistdocsFrontmatterSchema,
geistdocsMetaSchema,
} from "@vercel/geistdocs/source-config";
import { defineDocs } from "fumadocs-mdx/config";
export const docs = defineDocs({
dir: "content/docs",
docs: {
schema: geistdocsFrontmatterSchema,
postprocess: {
includeProcessedMarkdown: true,
},
},
meta: {
schema: geistdocsMetaSchema,
},
});
export default defineGeistdocsSourceConfig();For multiple docs families, create one collection per directory and reuse geistdocsFrontmatterSchema.
Create the package config
Use @vercel/geistdocs/config to centralize site metadata and route families in lib/geistdocs/config.tsx.
import { defineConfig } from "@vercel/geistdocs/config";
import { Logo, github, nav, prompt, suggestions, title } from "@/geistdocs";
export const config = defineConfig({
title,
defaultLanguage: "en",
logo: <Logo />,
github,
nav,
content: [{ id: "docs", label: "Docs", dir: "content/docs", route: "/docs" }],
ai: {
prompt,
suggestions,
},
});Set content to every public documentation route family. createProxy uses this metadata to infer standard Markdown mappings for non-root sections.
Connect Fumadocs sources
Wrap each Fumadocs collection with createSource in lib/geistdocs/source.ts.
import { createSource } from "@vercel/geistdocs/source";
import { docs } from "@/.source/server";
import { config } from "./config";
export const geistdocsSource = createSource({
docs,
config,
id: "docs",
label: "Docs",
});
export const source = geistdocsSource.source;For root-mounted docs, set baseUrl: "/" and use explicit markdownRoutes in proxy.ts:
export const geistdocsSource = createSource({
docs,
config,
baseUrl: "/",
});Add route adapters
Keep App Router files thin. Route files should call package helpers instead of copying package internals.
import { createDocsPage } from "@vercel/geistdocs/pages/docs";
import { config } from "@/lib/geistdocs/config";
import { geistdocsSource } from "@/lib/geistdocs/source";
const docsPage = createDocsPage({
config,
source: geistdocsSource,
openGraph: {
images: true,
},
});
export default docsPage.Page;
export const generateStaticParams = docsPage.generateStaticParams;
export const generateMetadata = docsPage.generateMetadata;Set openGraph.images to true only when your app includes the Geistdocs OG route. If you do not add the OG route, omit openGraph or override metadata to avoid broken /og/... references.
Add AI-readable routes
Add the package route helpers for machine-readable docs surfaces.
import { createLlmsRoute } from "@vercel/geistdocs/routes/llms";
import { geistdocsSource } from "@/lib/geistdocs/source";
export const { GET, revalidate } = createLlmsRoute({
sources: [geistdocsSource],
});import { createDocsMarkdownRoute } from "@vercel/geistdocs/routes/llms";
import { geistdocsSource } from "@/lib/geistdocs/source";
export const { GET, generateStaticParams, revalidate } =
createDocsMarkdownRoute({
source: geistdocsSource,
});import { createSitemapMarkdownRoute } from "@vercel/geistdocs/routes/sitemap";
import { config } from "@/lib/geistdocs/config";
import { geistdocsSource } from "@/lib/geistdocs/source";
export const { GET, generateStaticParams, revalidate, dynamic } =
createSitemapMarkdownRoute({
config,
sources: [{ source: geistdocsSource.source }],
});import { createAgentsRoute } from "@vercel/geistdocs/routes/agents";
import { config } from "@/lib/geistdocs/config";
export const { GET, generateStaticParams, revalidate } = createAgentsRoute({
config,
});import { createMcpManifestRoute } from "@vercel/geistdocs/routes/mcp";
import { config } from "@/lib/geistdocs/config";
export const { GET, generateStaticParams, revalidate } = createMcpManifestRoute({
config,
});Delete public/llms.txt after adding createLlmsRoute. Static files in public can mask App Router route behavior.
Add search and Ask AI routes
Use package route helpers for search and chat. Keep these files as adapters so @vercel/geistdocs can ship AI SDK compatibility fixes.
import { createSearchRoute } from "@vercel/geistdocs/routes/search";
import { config } from "@/lib/geistdocs/config";
import { geistdocsSource } from "@/lib/geistdocs/source";
export const GET = createSearchRoute({ config, sources: [geistdocsSource] });import { createChatRoute } from "@vercel/geistdocs/routes/chat";
import { config } from "@/lib/geistdocs/config";
import { geistdocsSource } from "@/lib/geistdocs/source";
const chatProxyUrl = process.env.GEISTDOCS_CHAT_PROXY_URL;
const chatProxyToken = process.env.GEISTDOCS_CHAT_PROXY_TOKEN;
export const { POST, maxDuration } = createChatRoute({
config,
proxy: chatProxyUrl
? {
url: chatProxyUrl,
headers: chatProxyToken
? { Authorization: `Bearer ${chatProxyToken}` }
: undefined,
}
: undefined,
sources: [geistdocsSource],
});Leave GEISTDOCS_CHAT_PROXY_URL unset for default AI Gateway mode. Set it to a /vertex proxy URL only when Ask AI should route model requests through the central Vertex-backed service.
Migrate middleware behavior
Use createProxy in proxy.ts. Put existing middleware.ts behavior in before or after hooks instead of replacing Geistdocs markdown negotiation.
import { createProxy } from "@vercel/geistdocs/proxy";
import { NextResponse } from "next/server";
import { config as geistdocsConfig } from "@/lib/geistdocs/config";
const proxy = createProxy({
config: geistdocsConfig,
before: async ({ request }) => {
if (request.nextUrl.pathname === "/legacy-docs") {
return NextResponse.redirect(new URL("/docs", request.url));
}
return null;
},
});
export const config = {
matcher: [
"/((?!api(?:/|$)|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)",
],
};
export default proxy;Use api(?:/|$) in the matcher. A broad api exclusion also excludes routes such as /api-reference.
Configure Markdown route mappings
For standard non-root sections, createProxy can infer Markdown routes from config.content.
Use explicit markdownRoutes when the public docs URL does not map directly to the Markdown route handler:
const proxy = createProxy({
config: geistdocsConfig,
markdownRoutes: [
{ from: "/docs/*path", to: "/[lang]/llms.mdx/*path" },
{ from: "/api-reference/*path", to: "/[lang]/llms.mdx/api-reference/*path" },
],
});Root-mounted docs need explicit mappings. If a homepage or app routes also live at /, do not use a broad /*path mapping. Map each docs family separately:
const proxy = createProxy({
config: geistdocsConfig,
markdownRoutes: [
{ from: "/guides/*path", to: "/[lang]/llms.mdx/guides/*path" },
{ from: "/api-reference/*path", to: "/[lang]/llms.mdx/api-reference/*path" },
],
});Configure Tailwind CSS
Tailwind CSS v4 requires @source entries for package components and related dependencies.
@import "tailwindcss";
@import "fumadocs-ui/css/shadcn.css";
@import "fumadocs-ui/css/preset.css";
@import "tw-animate-css";
@source "../../node_modules/@vercel/geistdocs/dist/**/*.js";
@source "../../node_modules/streamdown/dist/*.js";Move Tailwind CSS v3 plugin utilities into CSS-first v4 utilities or theme variables. Keep custom token names stable while you migrate components.
Handle environment variables
Do not require production secrets for local migration builds. If a homepage or API route depends on a production-only secret, add a local fallback or disable that integration in development.
export const flagsSecret =
process.env.FLAGS_SECRET ??
(process.env.NODE_ENV === "development" ? "local-development-secret" : undefined);Keep real secrets in .env.local and out of Git.
Verify the migration
Run the same checks after each routing or source change:
pnpm postinstall
pnpm build
pnpm devCheck these URLs locally:
/docsor the migrated docs root./llms.txt./sitemap.md./agents.md./.well-known/mcp.jsonifagent.mcp.serversis configured.- A page-level Markdown URL such as
/docs/getting-started.mdx. - A non-docs route that should not be rewritten as Markdown, such as the homepage.
Next steps
- Read Configuration to review package config options.
- Read Proxy and markdown routes to tune request handling.
- Read Agent readiness to configure
/agents.mdand/.well-known/mcp.jsonfor AI agents.