# Geistdocs Package Documentation These docs are bundled with `@vercel/geistdocs` 2.4.1. They describe how to build package-backed Geistdocs sites and how to use the package APIs from generated project adapters. ## Documentation Index - [Overview](pages/index.md): Geistdocs is a packaged documentation system for creating Next.js and Fumadocs sites with shared runtime features. - [Getting Started](pages/getting-started.md): Create a Geistdocs project, run it locally, and edit your first documentation page. - [Environment Variables](pages/env.md): Environment variables required by Geistdocs for AI chat, Mixedbread retrieval, proxy mode, and production URL configuration. - [Deploy to Vercel](pages/deployment.md): Deploy a Geistdocs site to Vercel with the required project settings and environment variables. - [Migrate to Geistdocs](pages/migration.md): Move an existing documentation site to @vercel/geistdocs while keeping local content, routing, middleware behavior, and AI-readable surfaces intact. - [Choose a Geistdocs package](pages/packages.md): Use the public package by default and the private package only where restricted Vercel npm dependencies are already available. - [Configuration](pages/configuration.md): Configure site metadata, navigation, AI features, page actions, translations, and local adapters. - [Syntax](pages/syntax.md): Supported MDX components and syntax including tabs, text formatting, code blocks, line highlighting, and Mermaid diagrams. - [Geistdocs Provider](pages/provider.md): The root provider component that wraps your application to handle toast notifications, search, and analytics. - [Versioned docs](pages/versioned-docs.md): Use createVersionedSources to configure stable, pre-release, or host-based documentation versions. - [Proxy and markdown routes](pages/proxy.md): Configure createProxy with generated route discovery, request hooks, markdown mappings, and a static Next.js matcher. - [Add a changelog](pages/changelog.md): Connect collection or paged ChangelogSource data to five thin HTML, Markdown, and JSON adapters, preserving existing docs mappings. - [.md Extension](pages/md.md): Access any documentation page as raw Markdown by appending .md or .mdx to the URL for AI tool consumption. - [Agent readiness](pages/agent-readiness.md): Generate /agents.md and /.well-known/mcp.json from Geistdocs config so agents can find docs, Markdown surfaces, API specs, and MCP endpoints. - [Ask AI](pages/ask-ai.md): An AI-powered chat assistant with context-aware search, persistent history, and suggested prompts for your documentation. - [Configure sidebar navigation](pages/guides/nested-navigation.md): Configure sidebar order, section labels, folder landing pages, and deeply nested page trees with meta.json files. - [Edit on GitHub](pages/edit-on-github.md): A direct link on every documentation page that lets readers propose changes through GitHub pull requests. - [Feedback](pages/feedback.md): An interactive feedback widget that collects user sentiment and creates structured GitHub Issues automatically. - [Improve Ask AI answers with Mixedbread](pages/mixedbread-retrieval.md): Provision a Mixedbread Store for one Geistdocs consumer site, enable semantic retrieval, and sync documentation during production builds. - [Internationalization](pages/internationalization.md): Serve documentation in multiple languages with automatic translation via the Geistdocs CLI and Fumadocs routing. - [llms.txt](pages/llms-txt.md): A single endpoint that returns all documentation as plain Markdown text following the llms.txt standard. - [Open in Chat](pages/open-in-chat.md): Transfer documentation context to AI chat platforms like ChatGPT, Claude, Cursor, and v0 with a single click. - [RSS](pages/rss.md): An automatically generated RSS 2.0 feed that keeps users informed when documentation is published or updated. - [Steps](pages/components/steps.md): Write consecutive headings that start with a number, such as "### 1. Install", to render a numbered step list. The Steps and Step components cover cases where a heading must be wrapped in another component. - [Table of contents](pages/table-of-contents.md): Geistdocs builds the "On this page" outline from your headings, indents sub-headings under their parent, and tracks the active section along a straight vertical guide rail. - [WebMCP](pages/webmcp.md): Expose documentation search and page reading to browser agents. --- --- title: Overview description: Learn how Geistdocs packages the CLI, runtime, and template for documentation sites type: overview summary: Geistdocs is a packaged documentation system for creating Next.js and Fumadocs sites with shared runtime features. url: /docs source: apps/template/content/docs/index.mdx related: - /docs/getting-started - /docs/migration - /docs/configuration --- # Overview Geistdocs is a packaged documentation system built with Next.js, Fumadocs, and shared Vercel documentation patterns. Use it to create a docs site with local content files and package-managed runtime features. Help me understand this Geistdocs project. Explain how the `@vercel/geistdocs` package, local adapter files, and `content/docs` folder work together, then suggest the first files I should edit for my documentation site. ## How Geistdocs works Geistdocs has three main parts: - `@vercel/geistdocs`: The npm package that provides the CLI, runtime components, route helpers, MDX components, page actions, and bundled template. - Local adapter files: The files in your generated project that connect your configuration, content source, routes, and UI customization to the package. - Platform services: Hosted endpoints at `geistdocs.com` for optional translation, feedback, and markdown tracking features. ```mermaid flowchart TB subgraph Package["@vercel/geistdocs package"] cli["CLI: init, update, translate, search sync"] runtime["Runtime: layout, routes, MDX, controls"] bundledTemplate["Bundled template snapshot"] end subgraph Site["Generated docs site"] config["geistdocs.tsx"] adapters["app/, components/geistdocs/, lib/geistdocs/"] content["content/docs/*.mdx"] end subgraph Platform["Geistdocs platform"] translate["/translate"] feedback["/feedback"] tracking["/md-tracking"] end cli --> bundledTemplate bundledTemplate --> Site adapters --> runtime content --> adapters cli --> translate runtime --> feedback runtime --> tracking ``` ## What you edit Most projects start by editing these files: - `content/docs`: Write and organize your docs. - `content/docs/meta.json`: Control sidebar order and groups. - `geistdocs.tsx`: Configure the logo, nav, GitHub repo, title, AI prompt, translations, and feature flags. - `components/geistdocs/mdx-components.tsx`: Add or override MDX components. - `app/[lang]/docs/[[...slug]]/page.tsx`: Configure docs page behavior, such as custom render hooks. - `proxy.ts`: Add site-specific request logic before or after package-managed markdown negotiation. ## What the package owns The package owns shared behavior such as docs rendering, page actions, search, Ask AI, markdown routes, `llms.txt`, and reusable MDX components. Updating `@vercel/geistdocs` gives your project package-level fixes and features without overwriting user-owned adapter files. Ask AI uses AI SDK v6 through package-managed client and server code. Generated projects include `ai` v6 and `@ai-sdk/react` v3 so the package chat UI and `createChatRoute` use the supported AI SDK APIs. Advanced projects can use package APIs for versioned docs, multiple content sources, custom page metadata, and custom proxy hooks while keeping runtime behavior in `@vercel/geistdocs`. ## Included features - MDX documentation with custom components - Local content in `content/docs` - Search and Ask AI - Optional Mixedbread semantic retrieval for Ask AI - Page actions, including feedback, copy page, open in chat, and edit on GitHub - Agent-readiness metadata with `/agents.md` and `/.well-known/mcp.json` - Raw Markdown routes for AI tools - `llms.txt` - Versioned docs and multiple content sources - Proxy hooks for custom request logic - RSS feed - Theme-aware images - Internationalized routes - CLI commands for init, update, translation, and Mixedbread search sync ## Next steps - Follow [Getting Started](/docs/getting-started) to create and run a project. - Read [Migration guide](/docs/migration) to move an existing docs site to package-backed Geistdocs. - Read [Configuration](/docs/configuration) to customize the generated site. - Read [Versioned docs](/docs/versioned-docs) to configure multiple docs versions. - Read [Proxy and markdown routes](/docs/proxy) to add custom request logic. - Read [Syntax](/docs/syntax) to write content with MDX components. --- --- title: Getting Started description: Create a Geistdocs project and start writing documentation type: guide summary: Create a Geistdocs project, run it locally, and edit your first documentation page. url: /docs/getting-started source: apps/template/content/docs/getting-started.mdx related: - /docs/configuration - /docs/migration - /docs/syntax - /docs/deployment --- # Getting Started Create a new Geistdocs site with the package CLI, then customize its identity, content, and navigation. If you already have a documentation site, use [Migrate to Geistdocs](/docs/migration). Read [Choose a Geistdocs package](/docs/packages) before using the private package. > Note: Use `@vercel/geistdocs-private` only for Vercel-internal applications that are not open source and already restrict npm access to authorized Vercel users, such as `next-site` or `vercel-docs`. Vercel-owned OSS repositories, repositories with external contributors, and all other projects must use the public `@vercel/geistdocs` package. See [Choose a Geistdocs package](/docs/packages) for the full comparison. Create a new Geistdocs project with `pnpm dlx @vercel/geistdocs@latest init --name my-docs`. Confirm the prerequisites, run `pnpm dev`, and help me customize `geistdocs.tsx`, `content/docs/index.mdx`, and `content/docs/meta.json`. Keep the generated integrations package-backed. ## Prerequisites Before you begin, install: - Node.js 20.9 or later - pnpm - Git The CLI always uses pnpm to install the generated project's dependencies. ## Create a project Run the Geistdocs CLI from an empty parent folder. For stable Geistdocs, run: ```bash title="Terminal" pnpm dlx @vercel/geistdocs@latest init --name my-docs ``` The CLI copies the template bundled in the package, rewrites the generated app to depend on the same `@vercel/geistdocs` version, installs dependencies, copies `.env.example` to `.env.local`, and initializes Git. ### Create a Labs starter To reuse the same template with [Vercel Labs](https://vercel.com/labs) navbar branding, run: ```bash title="Terminal" pnpm dlx @vercel/geistdocs@latest init --name my-labs-docs --brand labs ``` `--brand` accepts `vercel` (default) or `labs`. Omitting it or passing `--brand vercel` preserves the starter defaults. Invalid values fail before prompts or writes. Add `--disable-git` to skip Git initialization, not dependency installation. Labs changes only `navbarBrand` in `geistdocs.tsx`, independently of the `navbarVariant` layout, project `Logo`, and navigation. `@vercel/geistdocs-private` accepts the same flag. Its public-repository prompt still selects the public or private package; either choice preserves the selected brand. Read [Choose a navbar brand](/docs/configuration#choose-a-navbar-brand) to change the brand in an existing project. ## Understand generated dependencies Generated projects pin the resolved package version and include AI SDK v6 and `@ai-sdk/react` v3. The public package does not require private npm access. The template already configures styles, fonts, Cache Components, Partial Prefetching, source adapters, search, Ask AI, and agent-readable routes. Keep those integrations package-backed. Update Geistdocs instead of copying package internals into the app. ## Start the development server Open the generated project and start Next.js: ```bash title="Terminal" cd my-docs pnpm dev ``` Open `http://localhost:3000` in your browser. Start with these files: - `geistdocs.tsx` for the site title, logo, navigation, repository, and AI prompt. - `content/docs/index.mdx` for the documentation overview. - `content/docs/meta.json` for sidebar order and groups. Only customize `components/geistdocs/*`, `lib/geistdocs/*`, or route adapters when the generated defaults do not fit your site. ## Edit your first page Docs live in `content/docs`. Edit the overview page: ```mdx title="content/docs/index.mdx" --- title: Overview description: Learn how to use my project. --- My project helps developers build with clear documentation. ## Start here Add the first workflow you want readers to complete. ``` Add more pages by creating new `.mdx` files in `content/docs`. ## Update the sidebar Use `content/docs/meta.json` to control page order and groups: ```json title="content/docs/meta.json" { "title": "Documentation", "root": true, "pages": ["index", "getting-started", "---Guides---", "deploy"] } ``` Page and folder entries use names without extensions. A value such as `---Guides---` creates a non-clickable separator label. Read [Configure sidebar navigation](/docs/guides/nested-navigation) to add folder landing pages, deeply nested folders, custom links, and `defaultOpen` behavior. ## Configure the site Use `geistdocs.tsx` to customize the site shell: ```tsx title="geistdocs.tsx" export const title = "My Documentation"; export const nav = [ { label: "Docs", href: "/docs" }, { label: "GitHub", href: "https://github.com/my-org/my-repo" }, ]; ``` Read [Configuration](/docs/configuration) for the full configuration surface. ## Update Geistdocs For package-based projects, `geistdocs update` updates the `@vercel/geistdocs` package version. It does not overwrite your local content or adapter files. Projects already using a canary version continue to follow the `canary` dist-tag; stable projects follow `latest`. ```bash title="Terminal" pnpm exec geistdocs update ``` Review and test dependency changes before committing. ## Verify and deploy Run a production build before publishing: ```bash title="Terminal" pnpm build ``` Then follow [Deploy to Vercel](/docs/deployment), including environment-variable setup. Private-package access is required only for `@vercel/geistdocs-private`. --- --- title: Environment Variables description: Learn about the environment variables used in Geistdocs type: reference summary: Environment variables required by Geistdocs for AI chat, Mixedbread retrieval, proxy mode, and production URL configuration. url: /docs/env source: apps/template/content/docs/env.mdx prerequisites: - /docs/getting-started related: - /docs/configuration - /docs/deployment --- # Environment Variables Geistdocs uses environment variables to configure Ask AI, optional Mixedbread retrieval, optional Vertex-backed proxy mode, and production URL behavior. Review this Geistdocs project and help me configure the required environment variables. Check whether `AI_GATEWAY_API_KEY`, `MXBAI_API_KEY`, `MXBAI_STORE_ID`, `GEISTDOCS_CHAT_PROXY_URL`, and `NEXT_PUBLIC_VERCEL_PROJECT_PRODUCTION_URL` are needed for the features I enabled, then tell me where to set them locally and on Vercel. ## `AI_GATEWAY_API_KEY` The API key for the AI Gateway. This is used to power the default local AI chat functionality when `GEISTDOCS_CHAT_PROXY_URL` is not set. > This is automatically set when deploying to Vercel. Leave `GEISTDOCS_CHAT_PROXY_URL` unset to use AI Gateway mode. ## `MXBAI_API_KEY` The server-only API key for Mixedbread retrieval. The Mixedbread Vercel Marketplace integration injects this value when you provision a Store and connect it to the consumer site's Vercel project. ## `MXBAI_STORE_ID` The Mixedbread Store used by one consumer site. Set `ai.retrieval` to `"mixedbread"` and configure a unique `siteId` before using this value. Provision the Store by running `vercel integration add mixedbread` from the root of the consumer site's repository, where Vercel CLI is linked to that site's project. Do not run the provisioning command from the Geistdocs package repository. Read [Improve Ask AI answers with Mixedbread](/docs/mixedbread-retrieval) for the complete setup. ## `GEISTDOCS_CHAT_PROXY_URL` An optional URL for a chat proxy. When set, Geistdocs searches the local documentation on the first user message, injects the current page and related docs as context, and forwards the request to this proxy. Use this when routing Ask AI through a central Vertex-backed service. The value should point at the Geistdocs platform proxy and include `/vertex`: ```txt https:///vertex ``` When this variable is set, the site does not need `AI_GATEWAY_API_KEY` for Ask AI requests. The platform proxy calls Vertex and forwards a Vercel OIDC token in `x-vercel-trusted-oidc-idp-token` so the Vertex deployment can validate the caller through Deployment Protection Trusted Sources. ## `GEISTDOCS_CHAT_PROXY_TOKEN` An optional bearer token for the chat proxy. Only set this if your proxy requires an `Authorization` header. The default Geistdocs platform `/vertex` proxy uses Vercel OIDC and Trusted Sources, so it does not require this token. ## Vertex-backed proxy setup To route Ask AI through Vertex: 1. Use or deploy the central Geistdocs platform proxy with its `/vertex` route. 2. In the Vertex deployment's Deployment Protection settings, add the Geistdocs platform Vercel project as a Trusted Source. 3. In each Geistdocs site that should use Vertex, set `GEISTDOCS_CHAT_PROXY_URL` to the platform proxy URL, including `/vertex`. 4. Leave `GEISTDOCS_CHAT_PROXY_TOKEN` unset unless you replace the platform proxy with a custom bearer-authenticated proxy. The Vertex deployment does not need a Geistdocs-specific environment variable. Access is controlled by Trusted Sources and the OIDC token forwarded by the platform proxy. ## `NEXT_PUBLIC_VERCEL_PROJECT_PRODUCTION_URL` The production URL of the Geistdocs site. Geistdocs uses it for canonical and Open Graph metadata, product JSON-LD, `sitemap.xml`, `robots.txt`, and RSS links. Vercel sets this variable automatically. Set this variable explicitly when deploying outside Vercel. Use a hostname such as `docs.example.com` or a full `http` or `https` URL. Local development falls back to `http://localhost:3000`. A production build with a missing or malformed value emits a warning and omits default canonical and product JSON-LD URLs. Its `sitemap.xml`, `robots.txt` sitemap directive, RSS links, and generated Open Graph image URLs continue to use the localhost fallback, so treat the warning as a deployment blocker. Geistdocs does not use the deployment-specific `VERCEL_URL` for canonical surfaces. --- --- title: Deploy to Vercel description: Learn how to deploy your Geistdocs site to Vercel type: guide summary: Deploy a Geistdocs site to Vercel with the required project settings and environment variables. url: /docs/deployment source: apps/template/content/docs/deployment.mdx prerequisites: - /docs/getting-started - /docs/env related: - /docs/configuration --- # Deploy to Vercel Deploy a Geistdocs site to Vercel as a Next.js app. Configure environment variables and the production URL before shipping. Public `@vercel/geistdocs` projects require no private npm access. Help me deploy this Geistdocs project to Vercel. Review the selected package, scripts, environment variables, GitHub Actions authentication, and Vercel project settings. Verify private npm access only for `@vercel/geistdocs-private`. > Note: `@vercel/geistdocs-private` depends on the restricted `@vercel/geistcn` and `@vercel/geistcn-assets` packages. Use it only in internal environments such as `front`, where installs and deployments already have access. Public repositories, including Vercel-owned OSS projects, should use `@vercel/geistdocs`. ## Prerequisites Before deploying, you need: - A [GitHub account](https://github.com) with your Geistdocs repository - A [Vercel account](https://vercel.com) (sign up at [vercel.com](https://vercel.com)) - Your Vercel account connected to your GitHub account - Environment variables ready (see [Environment Variables](/docs/env)) - Access to restricted `@vercel` npm packages if the project uses the Geistcn canary ## Configure private package access The Geistcn canary depends on restricted `@vercel/geistcn` and `@vercel/geistcn-assets` packages. Package installation fails without npm authentication. For a Vercel deployment, ask `#help-core-platform` to add your Vercel project to the shared `NPM_TOKEN` environment variable list. Include the project link and the environments that need access. When `NPM_TOKEN` is available, Vercel creates the npm authentication configuration before installing dependencies. Do not copy the shared token into your repository or project documentation. For GitHub Actions, ask `#help-it` to add your repository to the allowlist for the `NPM_TOKEN` organization secret. Pass that secret to npm authentication during the install job: ```yaml title=".github/workflows/check.yml" - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v4 with: node-version: 20 cache: pnpm registry-url: https://registry.npmjs.org - run: pnpm install env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} ``` Read [How do I use private dependencies with Vercel?](https://vercel.com/kb/guide/using-private-dependencies-with-vercel) for the platform's npm authentication behavior. ## Deploy to Vercel To deploy from the Vercel dashboard: 1. Go to [vercel.com/new](https://vercel.com/new) 2. Select your Geistdocs repository 3. Configure your project settings: - **Framework Preset**: Next.js (automatically detected) - **Build Command**: `pnpm build` - **Output Directory**: `.next` (default) - **Install Command**: `pnpm install` 4. Add your environment variables (see [Environment Variables](/docs/env)) 5. Click "Deploy" ## Deploy with Vertex-backed Ask AI If your site routes Ask AI through the central Vertex-backed proxy, configure both the Geistdocs site and the Vertex deployment before testing production chat. 1. Set `GEISTDOCS_CHAT_PROXY_URL` on the Geistdocs site to the platform proxy URL, including `/vertex`. 2. Leave `GEISTDOCS_CHAT_PROXY_TOKEN` unset unless your proxy requires bearer authentication. 3. Add the Geistdocs platform Vercel project as a Trusted Source in the Vertex deployment's Deployment Protection settings. 4. Confirm the platform proxy can forward the Vercel OIDC token to Vertex in `x-vercel-trusted-oidc-idp-token`. The Geistdocs site does not need Vertex credentials. The platform proxy authenticates to Vertex with Vercel OIDC and Trusted Sources. --- --- title: Migrate to Geistdocs description: Migrate a Fumadocs or custom Geist docs site to package-backed Geistdocs type: guide summary: Move an existing documentation site to @vercel/geistdocs while keeping local content, routing, middleware behavior, and AI-readable surfaces intact. url: /docs/migration source: apps/template/content/docs/migration.mdx prerequisites: - /docs/configuration - /docs/proxy related: - /docs/versioned-docs - /docs/llms-txt - /docs/agent-readiness --- # Migrate to 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. > Note: Use `@vercel/geistdocs` by default, including in Vercel-owned OSS repositories. Use `@vercel/geistdocs-private` only in an internal environment that already has private npm access. See [Choose a Geistdocs package](/docs/packages). Help me migrate this existing docs site to package-backed Geistdocs. Inspect `source.config.ts`, `lib/geistdocs/source.ts`, app route files, `proxy.ts` or `middleware.ts`, `public/llms.txt`, Open Graph routes, Tailwind CSS setup, `package.json` AI SDK dependencies, and environment variables. Then produce a step-by-step migration plan using `@vercel/geistdocs` package APIs without copying package internals. ## 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.ts` or `proxy.ts` behavior. - 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. - Imports from `@vercel/geistdocs/components/*` or `@vercel/geistdocs/assets/*` that the Geistcn canary removes. - Direct app usage of `ai` or `@ai-sdk/react` outside Geistdocs. - Environment variables required by the homepage, docs routes, or API routes. Run the current project before changing it: ```bash title="Terminal" pnpm install pnpm build ``` Fix 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. Geistdocs requires Next.js 16.3.3 or later within 16.x. Earlier 16.3 releases can retain an optional catch-all route's index content after a client navigation to a child page when Cache Components and Partial Prefetching are enabled. For an existing project, use your package manager directly: ```bash title="Terminal" pnpm add --save-exact @vercel/geistdocs@latest ``` Geistdocs requires Node.js 20.9 or later and Next.js 16.3.3 or later within 16.x. Earlier 16.3 releases can retain an optional catch-all route's index content after a client navigation to a child page when Cache Components and Partial Prefetching are enabled. If you are starting from a generated stable Geistdocs project and moving content into it, run the CLI from an empty parent folder: ```bash title="Terminal" pnpm dlx @vercel/geistdocs@latest init --name my-docs ``` ### Install the Geistcn canary The Geistcn canary requires npm access to restricted `@vercel` packages. Configure your user-level npm authentication before installation: ```ini title="~/.npmrc" @vercel:registry=https://registry.npmjs.org/ //registry.npmjs.org/:_authToken=${NPM_TOKEN} always-auth=true ``` Set `NPM_TOKEN` from an approved internal source and keep its value out of files and Git. Read [Deploy to Vercel](/docs/deployment#configure-private-package-access) for Vercel and GitHub Actions access. For an existing standalone project, install the canary and the direct Geistcn dependencies used by application files. The current canary template uses `@vercel/geistcn` 1.0.3 and `@vercel/geistcn-assets` 2.0.1: ```bash title="Terminal" pnpm add --save-exact \ @vercel/geistdocs@canary \ @vercel/geistcn@1.0.3 \ @vercel/geistcn-assets@2.0.1 ``` `--save-exact` records the canary resolved at install time instead of a range. Before upgrading later, check the next canary and test that exact version: ```bash title="Terminal" npm view @vercel/geistdocs dist-tags.canary ``` In a monorepo that owns the Geistcn packages, such as `vercel/front`, keep their existing `workspace:*` dependency specifications and pin only `@vercel/geistdocs` to the tested canary. If you are starting from a generated Geistdocs project and moving content into it, run the canary CLI from an empty parent folder: ```bash title="Terminal" pnpm dlx @vercel/geistdocs@canary init --name my-docs ``` ## Replace imports for the Geistcn canary The Geistcn canary removes generic design-system components and assets from the Geistdocs public API. Search for affected imports before upgrading: ```bash title="Terminal" rg '@vercel/geistdocs/(components|assets)' . ``` Replace imports according to this table: | Previous Geistdocs import | Replacement | | --- | --- | | `components/badge`, `components/button`, or `components/theme-switcher` | Keep the import package-backed. Geistdocs resolves these template primitives through the selected public or private provider. | | `components/dialog`, `components/drawer`, `components/input`, `components/kbd`, `components/separator`, `components/sheet`, `components/spinner`, `components/textarea`, or `components/tooltip` | Import the matching component directly from `@vercel/geistcn/components/` in approved private projects, or keep an application-owned public equivalent. | | `components/switch` | Import toggle controls from `@vercel/geistcn/components/toggle`. | | `components/sonner` | Use `Toasts` and `useToasts` from `@vercel/geistcn/components/toasts`. | | `components/theme-aware-image` | Use `Image` from `@vercel/geistcn/components/image` with `srcLight` and `srcDark`. | | `assets/icons`, `assets/icons/*`, `assets/logos`, or `assets/logos/*` | Import from `@vercel/geistcn-assets/icons`, `@vercel/geistcn-assets/icons/*`, `@vercel/geistcn-assets/logos`, or `@vercel/geistcn-assets/logos/*`. | | `components/button-group`, `components/card`, or `components/input-group` | Use an appropriate Geistcn component or keep an application-owned composition. There is no direct Geistcn replacement with the same API. | Keep documentation-specific composites such as `Callout`, `CodeBlock`, `CodeBlockTabs`, `CommandPrompt`, `CopyPrompt`, and `Mermaid` imported from `@vercel/geistdocs/components/*`. ## Configure Next.js The Geistdocs Next.js integration composes Fumadocs MDX, enables automatic app-route discovery for agent-readable 404s, and preserves your Next.js configuration. Enable Cache Components in the same file: ```ts title="next.config.ts" import { createGeistdocs } from "@vercel/geistdocs/next"; import type { NextConfig } from "next"; const withGeistdocs = createGeistdocs(); const config: NextConfig = { cacheComponents: true, partialPrefetching: true, }; export default withGeistdocs(config); ``` ### Configure Next.js for the Geistcn canary Geistcn publishes TypeScript source from supporting packages. A project using the Geistcn canary also needs the matching transpilation and modular import settings: ```ts title="next.config.ts" import { createGeistdocs } from "@vercel/geistdocs/next"; import type { NextConfig } from "next"; const withGeistdocs = createGeistdocs(); const config: NextConfig = { modularizeImports: { "@vercel/geistcn/components": { skipDefaultConversion: true, transform: "@vercel/geistcn/components/{{ kebabCase member }}", }, "@vercel/geistcn/core": { skipDefaultConversion: true, transform: "@vercel/geistcn/core", }, }, transpilePackages: [ "@vercel/geistcn", "@vercel/geist-test-utils", "@vercel/next-themes", ], cacheComponents: true, partialPrefetching: true, }; export default withGeistdocs(config); ``` Restart `next dev` after adding, deleting, or renaming an App Router page or route. Production builds always regenerate the route manifest. Every root dynamic parameter needs at least one value from `generateStaticParams`. For a site with a `[lang]` root segment, return every configured language from the root layout: ```tsx title="app/[lang]/layout.tsx" export const generateStaticParams = () => [{ lang: "en" }]; ``` Server Components can read the active language without passing `params` through each layout: ```tsx title="app/[lang]/docs/layout.tsx" import * as root from "next/root-params"; export default async function DocsLayout({ children }) { const language = await root.lang(); // Use language to select the page tree. return children; } ``` Keep using route context `params` in Route Handlers and Server Actions. Next.js does not support `next/root-params` in those contexts. Remove `dynamic`, `revalidate`, and `fetchCache` exports from App Router pages and route handlers. Cache Components replaces those route segment options with `use cache` and `cacheLife`. Keep `partialPrefetching` enabled so links reuse each route's static and cached content instead of prefetching every destination page separately. For unknown HTML pages, browsers can receive the docs loading shell with a `200` response before the page resolves to the not-found UI. Crawlers wait for the complete response and receive `404`. Machine-readable Markdown routes keep their configured status behavior. ## Configure the root layout for the Geistcn canary Geistcn provides the fonts, theme classes, and Tailwind preflight used by the package UI. Apply them to the root `` element: ```tsx title="app/[lang]/layout.tsx" import { geistFontClasses } from "@vercel/geistcn/core"; import { cn } from "@vercel/geistcn/utils"; import type { ReactNode } from "react"; export default function RootLayout({ children }: { children: ReactNode }) { return ( {children} ); } ``` Keep the site's existing providers and metadata inside this structure. If the route has a `[lang]` parameter, use the active language instead of the hardcoded `en` value. ## Align 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. ```ts title="source.config.ts" 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`. ```tsx title="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: , 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`. ```ts title="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`: ```ts title="lib/geistdocs/source.ts" export const geistdocsSource = createSource({ docs, config, baseUrl: "/", }); ``` If the Next.js application also uses `basePath`, pass the same value through `defineConfig` but do not add it to `baseUrl`, `content.route`, `getPageUrl`, or `markdownRoutes`. Those values remain app-local. Geistdocs adds the public prefix to page actions, metadata, generated Markdown, discovery links, chat citations, and proxy destinations. ## Add route adapters Keep App Router files thin. Route files should call package helpers instead of copying package internals. ```tsx title="app/[lang]/docs/[[...slug]]/page.tsx" 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. ```ts title="app/[lang]/llms.txt/route.ts" import { createLlmsRoute } from "@vercel/geistdocs/routes/llms"; import { geistdocsSource } from "@/lib/geistdocs/source"; export const { GET } = createLlmsRoute({ sources: [geistdocsSource], }); ``` ```ts title="app/[lang]/llms.mdx/[[...slug]]/route.ts" import { createDocsMarkdownRoute } from "@vercel/geistdocs/routes/llms"; import { geistdocsSource } from "@/lib/geistdocs/source"; export const { GET, generateStaticParams } = createDocsMarkdownRoute({ notFound: {}, source: geistdocsSource, }); ``` ```ts title="app/[lang]/sitemap.md/route.ts" import { createSitemapMarkdownRoute } from "@vercel/geistdocs/routes/sitemap"; import { config } from "@/lib/geistdocs/config"; import { geistdocsSource } from "@/lib/geistdocs/source"; export const { GET, generateStaticParams } = createSitemapMarkdownRoute({ config, sources: [{ source: geistdocsSource.source }], }); ``` ```ts title="app/[lang]/agents.md/route.ts" import { createAgentsRoute } from "@vercel/geistdocs/routes/agents"; import { config } from "@/lib/geistdocs/config"; export const { GET, generateStaticParams } = createAgentsRoute({ config, }); ``` ```ts title="app/[lang]/.well-known/mcp.json/route.ts" import { createMcpManifestRoute } from "@vercel/geistdocs/routes/mcp"; import { config } from "@/lib/geistdocs/config"; export const { GET, generateStaticParams } = 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. ```ts title="app/api/search/route.ts" 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] }); ``` ```ts title="app/api/chat/route.ts" 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. ```ts title="proxy.ts" 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: ```ts title="proxy.ts" 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: ```ts title="proxy.ts" 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" }, ], }); ``` For a base-path application whose root page is documentation, add `"/"` to the static proxy matcher and verify `/index.md`. Keep normal application and API paths out of a broad root Markdown mapping with matcher exclusions or a `before` hook. ## Configure Tailwind CSS Stable Geistdocs uses Tailwind CSS v4 with source entries for package components and related dependencies: ```css title="app/styles/geistdocs.css" @import "@vercel/geistdocs/styles.css"; ``` The package stylesheet owns the provider foundation, Geist tokens, typography, Fumadocs integration, and compiled runtime source discovery. ### Configure Tailwind CSS for the Geistcn canary For the Geistcn canary, use the import order and narrow Geistcn source scan from the canary template: ```css title="app/styles/geistdocs.css" @import "@vercel/geistcn/tailwind.css"; @import "fumadocs-ui/css/preset.css"; @import "@vercel/geistdocs/theme.css"; @import "@vercel/geistcn/styles.css"; @import "@vercel/geistcn/marketing-typography.css"; @source "../../node_modules/@vercel/geistcn/src/components/**/*.{ts,tsx}"; ``` Geistcn owns the Geist tokens, theme, typography, preflight, and animation utilities. The Geistdocs theme entry point contains documentation and Fumadocs integration rules and sources the compiled Geistdocs runtime. Do not scan all of `@vercel/geistcn/src`; font internals in the published package can reference paths outside the application root and cause Turbopack to fail. ## 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. ```ts title="lib/example-secret.ts" export const flagsSecret = process.env.FLAGS_SECRET ?? (process.env.NODE_ENV === "development" ? "local-development-secret" : undefined); ``` Keep real application secrets in `.env.local` and out of Git. `NPM_TOKEN` is an install-time credential, not an application environment variable. Keep it in your shell, Vercel project settings, or GitHub Actions secrets, and never prefix it with `NEXT_PUBLIC_`. ## Verify the migration Run the same checks after each routing or source change: ```bash title="Terminal" pnpm postinstall pnpm list @vercel/geistdocs pnpm build pnpm dev ``` For the Geistcn canary, also run `pnpm list @vercel/geistcn @vercel/geistcn-assets` and confirm the versions match the selected canary template. Check these URLs locally: - `/docs` or the migrated docs root. - `/llms.txt`. - `/sitemap.md`. - `/agents.md`. - `/.well-known/mcp.json` if `agent.mcp.servers` is 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](/docs/configuration) to review package config options. - Read [Proxy and markdown routes](/docs/proxy) to tune request handling. - Read [Agent readiness](/docs/agent-readiness) to configure `/agents.md` and `/.well-known/mcp.json` for AI agents. --- --- title: Choose a Geistdocs package description: Choose between the public Geistdocs package and Vercel's private Geistcn-backed package type: guide summary: Use the public package by default and the private package only where restricted Vercel npm dependencies are already available. url: /docs/packages source: apps/template/content/docs/packages.mdx related: - /docs/getting-started - /docs/deployment - /docs/migration --- # Choose a Geistdocs package Geistdocs ships as two packages built from the same runtime and template. They expose the same API and intentionally share the same visual design, layout, interactions, and product behavior. A site should look and work the same with either package; only the underlying primitive and asset provider differs. > Note: `@vercel/geistdocs-private` uses restricted Geistcn components and assets intended only for internal, non-OSS applications. Vercel-owned OSS repositories and projects with external contributors must use `@vercel/geistdocs`. Do not expose the private package, its dependencies, credentials, or downloaded assets outside authorized internal environments. | Package | Access | UI provider | Recommended use | | --- | --- | --- | --- | | `@vercel/geistdocs` | Public | Package-owned public primitives and vendored assets | Open-source repositories, external contributors, and all projects without private Vercel npm access | | `@vercel/geistdocs-private` | Private; restricted access | Restricted `@vercel/geistcn` components and `@vercel/geistcn-assets`; requires authenticated access to Vercel's private npm registry | Internal environments such as `front`; never public repositories or projects requiring external contributor access | ## Use the public package by default Create a public Geistdocs project with: ```bash title="Terminal" pnpm dlx @vercel/geistdocs@latest init --name my-docs ``` The generated project has no dependency or peer dependency on `@vercel/geistcn` or `@vercel/geistcn-assets`. Contributors can install it using normal public npm access. ## Use the private package inside Vercel Create the private variant with: ```bash title="Terminal" pnpm dlx @vercel/geistdocs-private@latest init --name my-docs ``` The private initializer asks whether the generated template will be used in a public repository. Answer **Yes** to generate the public `@vercel/geistdocs` variant without restricted dependencies. Answer **No** to keep `@vercel/geistdocs-private`. The generated private template installs `@vercel/geistcn` and `@vercel/geistcn-assets` directly. Both local development and deployment must be authenticated to Vercel's private npm registry. This makes it suitable only for internal environments such as `front`, where that access already exists and remains limited to authorized Vercel users. Do not use the private package in a public repository or provide its registry access to external contributors. Use the public package whenever anyone outside Vercel needs to install, build, or contribute to the project—even if the site itself is deployed on Vercel. ## Keep package identities consistent The CLI writes the package that initialized the project into every generated import and dependency. Keep one Geistdocs package throughout the app and update it with that package's CLI. Do not install both variants or combine imports from them. Switching variants should require only changing package identity and provider-specific dependency metadata. Application content, configuration, route adapters, and Geistdocs APIs remain the same. --- --- title: Configuration description: Configure a Geistdocs site through local adapter files and package APIs type: reference summary: Configure site metadata, navigation, AI features, page actions, translations, and local adapters. url: /docs/configuration source: apps/template/content/docs/configuration.mdx prerequisites: - /docs/getting-started related: - /docs/migration - /docs/provider - /docs/env - /docs/guides/nested-navigation - /docs/versioned-docs - /docs/proxy --- # Configuration Configure Geistdocs through user-owned files in the generated project. Runtime features come from `@vercel/geistdocs`, while your local files control content, branding, routing adapters, and opt-in customization. Review this Geistdocs project configuration. Explain what each export in `geistdocs.tsx` does, identify which features are enabled, and suggest safe customizations that do not require editing package internals. ## Configuration files | File | Purpose | | --- | --- | | `geistdocs.tsx` | Site-level settings, including logo, nav, GitHub repo, AI prompt, agent-readiness metadata, translations, and feature flags. | | `next.config.ts` | Composes Fumadocs MDX, discovers App Router routes, and configures Next.js. | | `source.config.ts` | Configures Fumadocs collections with the package-provided schema, Markdown plugins, and syntax theme. | | `lib/geistdocs/config.tsx` | Converts `geistdocs.tsx` exports into the package config using `defineConfig`. | | `source.config.ts` | Configures Fumadocs collections with package-safe schemas and Markdown processing. | | `lib/geistdocs/source.ts` | Connects Fumadocs content collections to the package source adapter. | | `components/geistdocs/mdx-components.tsx` | Adds or overrides MDX components. | | `components/geistdocs/docs-layout.tsx` | Customizes the package-backed docs layout, including optional sidebar content. | | `app/[lang]/docs/[[...slug]]/page.tsx` | Configures the package docs page renderer. | | `app/[lang]/agents.md/route.ts` | Configures the agent-readiness entry point. | | `app/[lang]/.well-known/mcp.json/route.ts` | Configures MCP server discovery. | | `app/[lang]/llms.txt/route.ts` | Configures the all-docs Markdown endpoint. | | `app/[lang]/llms.mdx/[[...slug]]/route.ts` | Configures single-page Markdown responses. | | `proxy.ts` | Configures markdown negotiation, AI-agent rewrites, request tracking, and custom request hooks. | | `content/docs/meta.json` | Controls sidebar order and section labels. | Read [Configure sidebar navigation](/docs/guides/nested-navigation) for page ordering, titled separators, folder indexes, nested folders, and custom navigation links. ## `geistdocs.tsx` The root `geistdocs.tsx` file is the primary configuration surface: ```tsx title="geistdocs.tsx" import type { GeistdocsNavbarBrand } from "@vercel/geistdocs/config"; export const navbarBrand: GeistdocsNavbarBrand = "vercel"; export const Logo = () => My Docs; export const github = { owner: "my-org", repo: "my-repo", branch: "main", editPath: "content/docs/{path}", }; export const nav = [ { label: "Docs", href: "/docs" }, { label: "GitHub", href: "https://github.com/my-org/my-repo" }, ]; export const title = "My Documentation"; export const prompt = "You are a helpful assistant that answers questions about My Documentation."; export const suggestions = [ "How do I get started?", "How do I deploy?", ]; export const agent = { product: { name: "My Product", description: "My Product helps teams build and deploy applications.", }, }; export const translations = { en: { displayName: "English" }, }; export const basePath: string | undefined = undefined; export const siteId: string | undefined = undefined; ``` ### Choose a navbar brand Set the optional `navbarBrand` export to `"vercel"` (default) or `"labs"`: ```tsx title="geistdocs.tsx" import type { GeistdocsNavbarBrand } from "@vercel/geistdocs/config"; export const navbarBrand: GeistdocsNavbarBrand = "labs"; ``` The generated `lib/geistdocs/config.tsx` forwards this export to `defineConfig`. Existing sites can forward it in their adapter or set `navbarBrand: "labs"` directly in `defineConfig`. Private projects use the type from `@vercel/geistdocs-private/config`. Labs links to [vercel.com/labs](https://vercel.com/labs) and adapts to light and dark themes. Brand selection preserves your project's `Logo`, `logoHref`, and navigation. It is independent of `navbarVariant`, which selects the `"oss"` (default) or `"standard"` layout; either supports both brands. For new projects, both CLIs accept `--brand labs`; omitting it or using `--brand vercel` preserves the defaults. See [Create a Labs starter](/docs/getting-started#create-a-labs-starter). ### Group navbar links into a dropdown A `nav` entry can group related links into a dropdown. Provide `items` instead of `href`. Each item needs a `label` and an `href`, and takes an optional `section`: ```tsx title="geistdocs.tsx" export const nav = [ { label: "Docs", href: "/docs" }, { label: "Resources", items: [ { label: "Cookbook", href: "/cookbook", section: "Learn" }, { label: "Templates", href: "/templates", section: "Build" }, { label: "GitHub", href: "https://github.com/my-org/my-repo" }, ], }, ]; ``` On desktop, the group opens as a full-width panel below the navbar, following the vercel.com header pattern and matching the OSS products flyout. `section` values become column headings, preserving first-seen order; items without a `section` group into a column headed by the dropdown's own label. In the mobile menu, the group renders as a collapsible section. External URLs open in a new tab and show an external-link arrow. ## `defineConfig` `lib/geistdocs/config.tsx` converts your root exports into a package config. Most projects only need the generated defaults: ```tsx title="lib/geistdocs/config.tsx" import { defineConfig } from "@vercel/geistdocs/config"; import { agent, ai, basePath, github, Logo, nav, navbarBrand, prompt, siteId, suggestions, title, translations, } from "@/geistdocs"; export const config = defineConfig({ title, agent, defaultLanguage: "en", logo: , github, nav, navbarBrand, basePath, siteId, translations, ai: { prompt, suggestions, ...ai, }, }); ``` The navbar wordmark (`logo`) links to the site root by default. Set `logoHref` to keep visitors on the docs when the root redirects elsewhere — for example `logoHref: "/docs/getting-started"`. Like other navbar links, the href is prefixed with the active non-default language. Advanced sites can add `content` and `versions` metadata so local adapters and custom UI share one configuration object: ```tsx title="lib/geistdocs/config.tsx" export const config = defineConfig({ // ... content: [ { id: "docs", label: "Docs", dir: "content/docs", route: "/docs" }, { id: "cookbook", label: "Cookbook", dir: "content/cookbook", route: "/cookbook", }, ], versions: { current: "v6", items: [ { id: "v6", label: "v6 latest" }, { id: "v5", label: "v5", href: "https://v5.example.com/:path*" }, ], }, }); ``` The `ai` config accepts `retrieval: "mixedbread"` to use semantic documentation retrieval and `eveAgent` to answer Ask AI requests with a hosted eve framework agent: ```tsx title="geistdocs.tsx" export const ai = { retrieval: "mixedbread", eveAgent: { url: "https://help-eve.example.dev" }, }; ``` See [Ask AI](/docs/ask-ai) for the full eve agent mode behavior, including authentication. Read [Improve Ask AI answers with Mixedbread](/docs/mixedbread-retrieval) before enabling semantic retrieval. Use [Versioned docs](/docs/versioned-docs) for versioned source setup and [Proxy and markdown routes](/docs/proxy) for route mappings. ## `source.config.ts` Use the source-config-safe package export in `source.config.ts`. This file is evaluated by `fumadocs-mdx` during install and build, so it should avoid imports from runtime component entry points such as `@vercel/geistdocs/mdx`. ```ts title="source.config.ts" 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(); ``` To serve docs from the site root, use `route: "/"` in `content` and `baseUrl: "/"` in `createSource`. Root-mounted docs need explicit `markdownRoutes`; Geistdocs does not infer a broad `/*path` mapping because it can capture homepages and non-docs app routes. ## Next.js base paths When Next.js mounts the application below a path, set the same value in `next.config.ts` and Geistdocs config: ```ts title="next.config.ts" import { createGeistdocs } from "@vercel/geistdocs/next"; import type { NextConfig } from "next"; const withGeistdocs = createGeistdocs(); const config: NextConfig = { basePath: "/docs", cacheComponents: true, partialPrefetching: true, }; export default withGeistdocs(config); ``` ```tsx title="geistdocs.tsx" export const basePath = "/docs"; ``` Keep `content.route`, source `baseUrl`, navigation links, `getPageUrl`, search results, and proxy `markdownRoutes` app-local. Next.js applies the mount prefix to navigation. Geistdocs applies `basePath` separately to public page actions, Markdown metadata, generated Markdown and sitemap links, Ask AI citations, RSS discovery, and proxy rewrites. For example, a root-mounted source with `baseUrl: "/"` still uses `page.url === "/guide"`. The public page is `/docs/guide`, and its Markdown URL is `/docs/guide.md`. The root page uses `/docs/index.md`. ## Page actions Content actions appear in the **Copy page** menu beside the page title. **Scroll to top** and **Give feedback** remain below the table of contents so they stay available while you read. These actions are enabled by default and can be disabled through `defineConfig` in `lib/geistdocs/config.tsx`: ```tsx title="lib/geistdocs/config.tsx" export const config = defineConfig({ // ... pageActions: { editSource: false, scrollTop: true, copyPage: true, askAI: true, openInChat: false, }, feedback: { enabled: false, }, }); ``` `github.editPath` controls the file path used by the "Edit this page on GitHub" action. Use `{path}` where the page path should be inserted. For monorepos, include the app directory, such as `apps/docs/content/docs/{path}`. ## Page visibility Use frontmatter to control whether a page appears in package-owned machine-readable surfaces: ```mdx title="content/docs/internal-note.mdx" --- title: Internal note description: Hidden from public indexes internal: true --- ``` `internal: true` excludes a page from `llms.txt`, `sitemap.md`, search, and chat. `noindex: true` adds `robots: noindex` metadata and excludes the page from `sitemap.md`. Use `excludeFrom` for per-surface control: ```mdx --- title: Draft guide excludeFrom: - chat - search --- ``` Optional `tags`, `keywords`, and `canonical` frontmatter are also recognized by the package runtime. ### Gate page access per request Use `createDocsPage({ canViewPage })` when route state, authentication, or another request-specific policy controls access to HTML pages. A denied page returns the standard Next.js not-found response. The same policy filters page metadata, breadcrumbs, previous and next links, and the tree returned by `getPageTree`. Keep the page factory in a shared app-owned module so the page and layout use the same policy: ```tsx title="lib/geistdocs/docs-page.tsx" import { createDocsPage, type DocsPageParams, } from "@vercel/geistdocs/pages/docs"; import { config } from "./config"; import { geistdocsSource } from "./source"; interface PageParams extends DocsPageParams { audience: string; } export const docsPage = createDocsPage({ config, source: geistdocsSource, canViewPage: (page, { params }) => page.data.preview !== true || params.audience === "preview", }); ``` Use `docsPage.getPageTree(params)` for the layout tree instead of reading the source tree directly: ```tsx title="app/[lang]/[audience]/docs/layout.tsx" import { DocsLayout } from "@/components/geistdocs/docs-layout"; import { docsPage } from "@/lib/geistdocs/docs-page"; const Layout = async ({ children, params, }: LayoutProps<"/[lang]/[audience]/docs">) => ( {children} ); export default Layout; ``` `canViewPage` controls request-specific HTML access. Machine-readable indexes and Ask AI need one stable, cacheable public corpus. Define a synchronous `filterPublicPage` function. Pass it as `canViewPage` to `createDocsMarkdownRoute`, and as `filterPage` to `createLlmsRoute`, `createSitemapMarkdownRoute`, `createSearchRoute`, `createSearchExportRoute`, and `createChatRoute`: ```ts title="lib/geistdocs/page-access.ts" export const filterPublicPage = (page: { data: { preview?: boolean } }) => page.data.preview !== true; ``` Consumers choose the frontmatter field and access provider. Geistdocs does not require a specific authentication or feature flag system. ## Last modified dates `sitemap.md` entries include a `Lastmod` field and RSS items carry a publish date when a page has a last-modified date. Geistdocs resolves the date for each page in this order: 1. The page's `lastModified` frontmatter value. 2. The file's most recent git commit date. 3. No date. The sitemap entry omits `Lastmod` and the RSS item falls back to the build date. Set `lastModified` in frontmatter when a page needs a stable, editor-controlled date: ```mdx title="content/docs/example.mdx" --- title: Example page description: An example documentation page lastModified: 2026-07-01 --- ``` Git-based dates require full git history at build time. Vercel builds use a shallow clone by default, so pages that were not changed in the cloned history have no reachable commit date and their entries omit `Lastmod`. To make git-based dates work on Vercel, set the `VERCEL_DEEP_CLONE=true` environment variable on your project so builds clone the full history. ## User-owned adapters Adapter files are safe to edit. Package updates do not overwrite them. For example, add a custom MDX component: ```tsx title="components/geistdocs/mdx-components.tsx" import { createMdxComponents } from "@vercel/geistdocs/mdx"; import type { MDXComponents } from "mdx/types"; const ProductCard = ({ name }: { name: string }) =>
{name}
; export const getMDXComponents = (components?: MDXComponents): MDXComponents => createMdxComponents({ ProductCard, ...components, }); ``` Then use it in MDX: ```mdx ``` ## Docs page adapter `createDocsPage` accepts hooks for route-specific behavior: ```tsx title="app/[lang]/docs/[[...slug]]/page.tsx" const docsPage = createDocsPage({ config, source: geistdocsSource, getPageUrl: ({ page }) => page.url, openGraph: { images: true, }, metadata: ({ metadata }) => metadata, mdx: ({ link }) => getMDXComponents({ a: link }), tableOfContent: { header:
Above the table of contents
, footer:
Below the table of contents
, }, }); ``` Use `getPageUrl` when the app-local route differs from the source URL, such as `/v5/docs`. Geistdocs keeps that value app-local for previous and next navigation, then derives the public action URL with `config.basePath`. For unusual deployments, `getPublicPageUrl` can override the public HTML URL and `getMarkdownUrl` can override the Copy Page, View as Markdown, and metadata URL. Most sites should use the package defaults so these surfaces cannot drift apart. Set `openGraph.images` to `true` only when your site includes the Geistdocs OG route. Use `metadata` to set canonical URLs, `robots`, or custom Open Graph fields for a specific route. The `tableOfContent.header` and `tableOfContent.footer` slots render immediately above and below the table of contents. Section landing pages that use `title: Overview` produce a distinct document `` automatically: Geistdocs substitutes the parent section label (for example, `Channels`) so browser tabs and search results stay meaningful instead of repeating "Overview". The visible page heading and breadcrumb are unchanged, and other pages keep their own title. Use the `metadata` callback to override the resolved title for a specific route. Geistdocs wraps resolved documentation pages in a Next.js error boundary. Unexpected rendering failures show a generic **Try again** action while the navbar and sidebar remain available. Framework signals such as `notFound()` and `redirect()` continue to use their standard Next.js behavior. Package-owned links fully prefetch documentation destinations. Pages returned by `generateStaticParams` therefore navigate directly to their complete static content without showing transient fallback UI. ## Package-owned runtime Do not edit files inside `node_modules/@vercel/geistdocs`. Update the package instead: ```bash title="Terminal" pnpm exec geistdocs update ``` If a release adds an optional prop or config field, opt into it by editing the relevant local adapter file. Existing adapters should keep compiling unless a release includes a breaking API change. --- --- title: Syntax description: Learn about the supported markdown syntax and formatting options type: reference summary: Supported MDX components and syntax including tabs, text formatting, code blocks, line highlighting, and Mermaid diagrams. url: /docs/syntax source: apps/template/content/docs/syntax.mdx prerequisites: - /docs/getting-started related: - /docs/configuration --- # Syntax Geistdocs supports MDX, a superset of Markdown that allows you to use JSX components within your documentation. This guide covers all the formatting options available. Help me write a Geistdocs MDX page. Use this page as a syntax reference and create an example page with frontmatter, headings, links, a code block, a `Callout`, and a `CopyPrompt` component. ## Frontmatter Every MDX file should include frontmatter at the top: ```yaml --- title: Page Title description: A brief description of the page content navTitle: Short page title badge: Beta --- ``` The `title` property is required and is used for the page heading, metadata, and default navigation label. Set the optional `navTitle` when navigation needs a shorter label without changing the page heading. The `description` is used for SEO and page previews. The optional `badge` appears beside the page label in desktop and mobile sidebar navigation. Badge labels that match an uppercase HTTP method (`GET`, `POST`, `PUT`, `PATCH`, `DEL`, or `DELETE`) render with a method-specific color. See [Configure sidebar navigation](/docs/guides/nested-navigation) for the full color mapping. ## Basic Markdown ### Text Formatting You can style text using standard Markdown syntax: - **Bold text** with `**double asterisks**` - _Italic text_ with `*single asterisks*` - ~~Strikethrough~~ with `~~double tildes~~` - `Inline code` with `` `backticks` `` ### Headings Use `#` symbols to create headings: ```text # Heading 1 ## Heading 2 ### Heading 3 #### Heading 4 ##### Heading 5 ###### Heading 6 ``` Headings automatically generate anchor links for navigation and deep linking. ### Lists Create unordered lists with `-`, `*`, or `+`: ```md - Install the required packages - Configure your project - Start the development server ``` This renders as: - Install the required packages - Configure your project - Start the development server Create ordered lists with numbers: 1. First step 2. Second step 3. Third step ### Steps Headings that start with a number, such as `### 1. Install the CLI`, render as a numbered step list with a counter beside each heading. See [Steps](/docs/components/steps) for the grouping rules and the `Steps` and `Step` components. ### Blockquotes Use `>` to create blockquotes: > **Watch:** Learn more about how to use `next/image` → [YouTube (9 minutes)](https://youtu.be/IU_qq_c_lKA). ### Callouts Use `<Callout>` for a highlighted informational note: ```mdx <Callout>This is automatically set when deploying to Vercel.</Callout> ``` This renders as: > This is automatically set when deploying to Vercel. Callouts can also contain a title and lists: > Note: - The `App Router` uses [React canary releases](https://react.dev/blog/2023/05/03/react-canaries) built-in, which include all the stable React 19 changes, as well as newer features being validated in frameworks, but you should still declare react and react-dom in package.json for tooling and ecosystem compatibility. - The `Pages Router` uses the React version from your `package.json`. Callouts support rich inline Markdown, including code, emphasis, and links: ```mdx <Callout title="Good to know"> Global styles can be imported into any layout, page, or component inside the `app` directory. However, since Next.js uses React's built-in support for stylesheets to integrate with Suspense, this currently does not remove stylesheets as you navigate between routes which can lead to conflicts. We recommend using global styles for _truly global_ CSS (like Tailwind's base styles), [Tailwind CSS](https://tailwindcss.com) for component styling, and [CSS Modules](https://nextjs.org/docs/app/getting-started/css#css-modules) for custom scoped CSS when needed. </Callout> ``` This renders as: > Note: Global styles can be imported into any layout, page, or component inside the `app` directory. However, since Next.js uses React's built-in support for stylesheets to integrate with Suspense, this currently does not remove stylesheets as you navigate between routes which can lead to conflicts. We recommend using global styles for _truly global_ CSS (like Tailwind's base styles), [Tailwind CSS](https://tailwindcss.com) for component styling, and [CSS Modules](https://nextjs.org/docs/app/getting-started/css#css-modules) for custom scoped CSS when needed. ### Disclosures Use `<Details>` and `<Summary>` to hide supporting information until the reader chooses to expand it: ```mdx <Details> <Summary>Graph algorithm overview</Summary> The graph starts with the CSS files imported by each route. It groups files when sharing them costs less than another request. It splits files when unused CSS becomes more expensive than that request. </Details> ``` This renders as: <Details> <Summary>Graph algorithm overview</Summary> The graph starts with the CSS files imported by each route. It groups files when sharing them costs less than another request. It splits files when unused CSS becomes more expensive than that request. </Details> ### Copy Prompts Short prompts render in full without an expansion control: ```mdx <CopyPrompt text="Summarize this documentation page and list the key concepts."> Summarize this documentation page and list the key concepts. </CopyPrompt> ``` This renders as: Summarize this documentation page and list the key concepts. Long prompts initially show a preview with a **Show more** control: ```mdx <CopyPrompt collapsible text="Write a detailed migration plan for this project."> Write a detailed migration plan for this project. Include prerequisites, implementation steps, validation, rollback guidance, and common pitfalls. </CopyPrompt> ``` Set the boolean `collapsible` prop when the prompt should initially render as a two-line preview. This renders as: Review this Geistdocs project in detail. Explain how the `@vercel/geistdocs` package, application layouts, local adapter files, source configuration, `content/docs` directory, navigation metadata, MDX components, syntax highlighting, search routes, and deployment configuration work together. Then identify the safest first files to edit, describe the effect of each change, call out common mistakes, and propose a step-by-step plan for customizing the documentation site without breaking routing, generated Markdown, or accessibility behavior. ### Links Create links with `[text](url)`: - [External link](https://vercel.com) - [Internal link](/docs/getting-started) Use an absolute `https://` or `http://` URL for an external destination. Use a root-relative path beginning with `/` for an internal link. Both use the same visual styling: ```md [External link](https://vercel.com) [Internal link](/docs/getting-started) ``` Links can also contain inline code: ```md Run independent requests in parallel with [`Promise.all`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise/all). ``` This renders as: Run independent requests in parallel with [`Promise.all`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise/all). #### Force browser-native navigation Normal internal links use Next.js navigation, while external links disable prefetching automatically. A custom `createDocsPage` link resolver also receives `nativeLink` for URLs that must always use a regular `<a>`, such as a raw Markdown file or a production URL that should not be localized into a preview deployment. It keeps the same Geistdocs link styling: ```tsx const docsPage = createDocsPage({ config, source, resolveLink: ({ link: Link, nativeLink: NativeLink }) => function ResolvedLink(props) { const native = props.href?.endsWith(".md") || props.href?.startsWith("https://example.com"); return native ? <NativeLink {...props} /> : <Link {...props} />; }, }); ``` `NativeDocsLink` is also exported from the selected package's `components/link` subpath for use outside `createDocsPage`. ### Images Add images with `![alt text](url)`: ```md ![Description of the image](/path/to/image.png) ``` Use `<Image>` when an image needs the standard documentation presentation and an optional description. It uses Geistcn's image component without a lightbox or other interaction: ```mdx <Image alt="Deployment graph" caption="Requests move from the edge to the selected region." height={480} src="/deployment-graph.png" width={800} /> ``` This renders as: <Image alt="Navigation Inspector showing a loading shell for a page load" caption="The Navigation Inspector paused on a page load." height={389} srcDark="https://h8dxkfmaphn8o0p3.public.blob.vercel-storage.com/docs/dark/inspector-load.png" srcLight="https://h8dxkfmaphn8o0p3.public.blob.vercel-storage.com/docs/light/inspector-load.png" unoptimized width={472} /> For theme-aware images, provide both sources: ```mdx <Image alt="Deployment graph" caption="Requests move from the edge to the selected region." height={480} srcDark="/deployment-graph-dark.png" srcLight="/deployment-graph-light.png" width={800} /> ``` ### Tabs Use `TabsWithChildren` and `TabContent` to switch between related instructions, code examples, or other MDX components. Both are available without imports. The `tabs` array defines the labels, and each panel's `order` starts at `1`. ```mdx <TabsWithChildren tabs={["Dashboard", "CLI"]} ariaLabel="Setup method"> <TabContent order={1}> Open your project's settings in the dashboard. </TabContent> <TabContent order={2}> Connect your local project using the CLI. </TabContent> </TabsWithChildren> ``` This renders as: <TabsWithChildren tabs={["Dashboard", "CLI"]} ariaLabel="Setup method"> <TabContent order={1}> Open your project's settings in the dashboard. </TabContent> <TabContent order={2}> Connect your local project using the CLI. </TabContent> </TabsWithChildren> Use the arrow keys, Home, or End to select a tab. Each tab is linked to its panel for assistive technology. Inactive panels stay mounted and hidden, preserving local state when you return to a tab. Nested tab groups manage their selection independently. In React, use `defaultValue="tab2"` to start on the second tab, or pass `value` and `onValueChange` to control selection. Values follow the panel order (`tab1`, `tab2`, and so on). ## GitHub Flavored Markdown Geistdocs supports GFM extensions including tables, task lists, and autolinks. ### Tables Create tables using pipes and dashes: ```md | Feature | Description | Status | | -------- | ----------------- | --------- | | Markdown | Basic formatting | Supported | | GFM | GitHub extensions | Supported | | MDX | JSX in Markdown | Supported | ``` This renders as: | Feature | Description | Status | | -------- | ----------------- | --------- | | Markdown | Basic formatting | Supported | | GFM | GitHub extensions | Supported | | MDX | JSX in Markdown | Supported | Tables can also include inline code, as commonly used for file references: | File | Description | | -------------------- | ------------------------------------------------ | | `next.config.js` | Configuration file for Next.js | | `package.json` | Project dependencies and scripts | | `instrumentation.ts` | OpenTelemetry and instrumentation file | | `.env.local` | Local environment variables | | `tsconfig.json` | Configuration file for TypeScript | ### Task Lists Create interactive task lists: ```md - [x] Completed task - [ ] Incomplete task - [ ] Another task to do ``` This renders as: - [x] Completed task - [ ] Incomplete task - [ ] Another task to do ### Autolinks URLs are automatically converted to clickable links: ```md https://vercel.com ``` This renders as: https://vercel.com ## Code Blocks Fenced code blocks support syntax highlighting for many languages. ### Basic Syntax Highlighting Specify the language after the opening backticks: ````md ```typescript const greet = (name: string): string => { return `Hello, ${name}!`; }; ``` ```` This renders as: ```typescript const greet = (name: string): string => { return `Hello, ${name}!`; }; ``` ### Titles Add a title to your code block with the `title` attribute: ````md ```tsx title="components/button.tsx" export const Button = ({ children }) => { return <button type="button">{children}</button>; }; ``` ```` This renders as: ```tsx title="components/button.tsx" export const Button = ({ children }) => { return <button type="button">{children}</button>; }; ``` ### Tabs and switchers Code blocks support tabs and compact switchers for selecting between code examples. Use consecutive fences with `tab` metadata to show all code tab labels at once: ````md ```ts tab="TypeScript" title="example.ts" export const greeting: string = "Hello from TypeScript"; ``` ```js tab="JavaScript" title="example.js" export const greeting = "Hello from JavaScript"; ``` ```` This renders as: ```ts tab="TypeScript" title="example.ts" export const greeting: string = "Hello from TypeScript"; ``` ```js tab="JavaScript" title="example.js" export const greeting = "Hello from JavaScript"; ``` Wrap the same fences in `<CodeBlockTabs compact>` to move the selector into the code-block header: ````mdx <CompactCodeBlockTabs defaultValue="TypeScript"> <CodeBlockTabsList> <CodeBlockTabsTrigger value="TypeScript">TypeScript</CodeBlockTabsTrigger> <CodeBlockTabsTrigger value="JavaScript">JavaScript</CodeBlockTabsTrigger> </CodeBlockTabsList> <CodeBlockTab value="TypeScript"> ```ts title="example.ts" export const greeting: string = "Hello from TypeScript"; ``` </CodeBlockTab> <CodeBlockTab value="JavaScript"> ```js title="example.js" export const greeting = "Hello from JavaScript"; ``` </CodeBlockTab> </CompactCodeBlockTabs> ```` This renders as: <CompactCodeBlockTabs defaultValue="TypeScript"> <CodeBlockTabsList> <CodeBlockTabsTrigger value="TypeScript">TypeScript</CodeBlockTabsTrigger> <CodeBlockTabsTrigger value="JavaScript">JavaScript</CodeBlockTabsTrigger> </CodeBlockTabsList> <CodeBlockTab value="TypeScript"> ```ts title="example.ts" export const greeting: string = "Hello from TypeScript"; ``` </CodeBlockTab> <CodeBlockTab value="JavaScript"> ```js title="example.js" export const greeting = "Hello from JavaScript"; ``` </CodeBlockTab> </CompactCodeBlockTabs> When migrating an existing site such as next-site, translate adjacent `switcher` fences to `tab="TypeScript"` and `tab="JavaScript"`, then wrap the pair with `<CodeBlockTabs compact>`. Import `@vercel/geistdocs/styles.css` (or `theme.css` with the documented Geist dependencies) so Tailwind scans the package's compiled components and emits the tab layout utilities. ### Framework icons Append `#<framework>` to the title to show a framework logo instead of the language icon. The suffix is stripped from the displayed title. Supported values are `#next` / `#nextjs` and `#svelte` / `#sveltekit`: ````md ```ts title="flags.ts#next" import { flag } from "flags/next"; ``` ```ts title="flags.ts#svelte" import { flag } from "flags/sveltekit"; ``` ```` This renders as: ```ts title="flags.ts#next" import { flag } from "flags/next"; ``` ```ts title="flags.ts#svelte" import { flag } from "flags/sveltekit"; ``` ### Line Numbers Display line numbers by adding the `lineNumbers` attribute: ````md ```typescript lineNumbers const numbers = [1, 2, 3, 4, 5]; const doubled = numbers.map((n) => n * 2); console.log(doubled); ``` ```` This renders as: ```typescript lineNumbers const numbers = [1, 2, 3, 4, 5]; const doubled = numbers.map((n) => n * 2); console.log(doubled); ``` ### Line Highlighting Highlight specific lines using the `[!code highlight]` comment: ````md ```typescript const config = { theme: "dark", // [ !code highlight] language: "en", }; ``` ```` Remove the space before `!` when using this syntax. This renders as: ```typescript const config = { theme: "dark", // [!code highlight] language: "en", }; ``` ### Word Highlighting Highlight specific words with `[!code word:term]`: ````md ```typescript // [ !code word:config] const config = { theme: "dark", }; console.log(config); ``` ```` Remove the space before `!` when using this syntax. This renders as: ```typescript // [!code word:config] const config = { theme: "dark", }; console.log(config); ``` ### Diff Syntax Show additions and deletions with `[!code ++]` and `[!code --]`: ````md ```typescript const config = { theme: "light", // [ !code --] theme: "dark", // [ !code ++] language: "en", }; ``` ```` Remove the space before `!` when using this syntax. This renders as: ```typescript const config = { theme: "light", // [!code --] theme: "dark", // [!code ++] language: "en", }; ``` ### Focus Lines Draw attention to specific lines with `[!code focus]`: ````md ```typescript const numbers = [1, 2, 3, 4, 5]; const sum = numbers.reduce((a, b) => a + b, 0); // [ !code focus] console.log(sum); ``` ```` Remove the space before `!` when using this syntax. This renders as: ```typescript const numbers = [1, 2, 3, 4, 5]; const sum = numbers.reduce((a, b) => a + b, 0); // [!code focus] console.log(sum); ``` ## Mermaid Diagrams Create diagrams and flowcharts using Mermaid syntax. ### Flowcharts ````md ```mermaid graph TD; A[Start] --> B{Is it working?}; B -->|Yes| C[Great!]; B -->|No| D[Debug]; D --> B; ``` ```` This renders as: ```mermaid graph TD; A[Start] --> B{Is it working?}; B -->|Yes| C[Great!]; B -->|No| D[Debug]; D --> B; ``` ### Sequence Diagrams ````md ```mermaid sequenceDiagram participant User participant API participant Database User->>API: Request data API->>Database: Query Database-->>API: Results API-->>User: Response ``` ```` This renders as: ```mermaid sequenceDiagram participant User participant API participant Database User->>API: Request data API->>Database: Query Database-->>API: Results API-->>User: Response ``` ### Architecture Diagrams ````md ```mermaid graph TD; subgraph Frontend A[Web App] B[Mobile App] end subgraph Backend C[API Gateway] D[Auth Service] E[Data Service] end subgraph Storage F[(Database)] G[(Cache)] end A --> C B --> C C --> D C --> E E --> F E --> G ``` ```` This renders as: ```mermaid graph TD; subgraph Frontend A[Web App] B[Mobile App] end subgraph Backend C[API Gateway] D[Auth Service] E[Data Service] end subgraph Storage F[(Database)] G[(Cache)] end A --> C B --> C C --> D C --> E E --> F E --> G ``` --- --- title: Geistdocs Provider description: The root provider component that handles notifications, search and analytics type: reference summary: The root provider component that wraps your application to handle toast notifications, search, and analytics. url: /docs/provider source: apps/template/content/docs/provider.mdx prerequisites: - /docs/getting-started related: - /docs/configuration --- # Geistdocs Provider The `GeistdocsProvider` wraps the root of your application with package-managed UI state. It connects theme, search, AI chat, notifications, and optional analytics to the rest of your Geistdocs site. Review my Geistdocs provider setup. Check that the root layout wraps the app with `GeistdocsProvider`, that config is passed correctly, and that analytics, search, theme, and Ask AI behavior are wired safely. ## What it does The provider handles these behaviors: 1. **Toast notifications**: Provides a global notification system for user feedback. 2. **Analytics**: Integrates Vercel Analytics and Speed Insights when the local adapter includes them. 3. **Search**: Configures the search dialog and connects it to your search API. 4. **Ask AI**: Provides the client state used by the chat sidebar and page actions. ## Usage Ensure your application is wrapped with the provider in your root layout: ```tsx title="app/layout.tsx" import { GeistdocsProvider } from "@/components/geistdocs/provider"; const Layout = ({ children }: LayoutProps) => ( <html lang="en"> <body> <GeistdocsProvider> <Navbar /> {children} </GeistdocsProvider> </body> </html> ); ``` The generated local adapter imports the package provider and passes your site config to it. ## Search scope A search index partitioned by version, section, or product usually wants the dialog to answer from the partition the reader is in. Pass that scope as `search.options.tag`: ```tsx title="app/layout.tsx" <GeistdocsProvider config={config} search={{ options: { tag: "v5" } }}> ``` The dialog sends it to your search route as `?tag=v5`, alongside the query. Read it from the request URL and filter results with it. Leave the option unset and the parameter is omitted, so the request is unchanged. Pass the scope here rather than reading a header or a cookie in the route. The search client caches results per request URL for the lifetime of the page, so two scopes that produce the same URL share one cache entry, and the second reader is served the first reader's results. > Note: `createSearchRoute` indexes pages without a `tag` field, and tagged queries match with `containsAll`, so a tag passed to the built-in route matches nothing. ## AI Sidebar When users open the AI chat on desktop, the provider automatically adds padding to prevent content from being hidden behind the sidebar. On mobile, the chat opens as a drawer instead, so no padding is needed. The provider detects the screen size and chat state. If Ask AI fails while rendering, its panel shows **Try again** and **Close** actions. The error remains scoped to the chat panel, so the documentation page and navigation stay available. ## Toast Notifications The provider includes a global toast notification system. Use it anywhere in your app: ```tsx title="page.tsx" import { toast } from "sonner"; toast.success("Changes saved"); toast.error("Something went wrong"); ``` ## Analytics Vercel Analytics is automatically included and tracks: - Page views - Web Vitals - User interactions - Performance metrics No configuration needed - it works automatically when deployed on Vercel. --- --- title: Versioned docs description: Configure multiple documentation versions with package-backed source helpers type: guide summary: Use createVersionedSources to configure stable, pre-release, or host-based documentation versions. url: /docs/versioned-docs source: apps/template/content/docs/versioned-docs.mdx prerequisites: - /docs/configuration related: - /docs/proxy - /docs/llms-txt --- # Versioned docs Versioned docs let you serve multiple documentation sets from one Geistdocs project. Use them for stable and pre-release docs, previous major versions, or host-based version switching. Help me add versioned docs to this Geistdocs site. Inspect `source.config.ts`, `lib/geistdocs/source.ts`, app route files, and the docs layout, then propose a versioned source setup using `createVersionedSources`. ## Configure source collections Create one Fumadocs collection for each version in `source.config.ts`: ```ts title="source.config.ts" import { defineGeistdocsSourceConfig, geistdocsFrontmatterSchema, } from "@vercel/geistdocs/source-config"; import { defineDocs } from "fumadocs-mdx/config"; export const v4docs = defineDocs({ dir: "content/docs/v4", docs: { schema: geistdocsFrontmatterSchema, postprocess: { includeProcessedMarkdown: true, }, }, }); export const v5docs = defineDocs({ dir: "content/docs/v5", docs: { schema: geistdocsFrontmatterSchema, postprocess: { includeProcessedMarkdown: true, }, }, }); export default defineGeistdocsSourceConfig(); ``` ## Create versioned sources Use `createVersionedSources` in `lib/geistdocs/source.ts`: ```ts title="lib/geistdocs/source.ts" import { createVersionedSources } from "@vercel/geistdocs/source"; import { v4docs, v5docs } from "@/.source/server"; import { config } from "./config"; export const versions = createVersionedSources({ config, current: "v4", versions: [ { id: "v4", label: "v4", docs: v4docs, baseUrl: "/docs", }, { id: "v5", label: "v5 pre-release", docs: v5docs, baseUrl: "/docs", routePrefix: "/v5", }, ], }); export const geistdocsSource = versions.current; export const source = geistdocsSource.source; ``` The returned object includes: | Property | Type | Description | | --- | --- | --- | | `current` | `GeistdocsSourceBundle` | The source matching the `current` version ID. | | `byId` | `Record<string, GeistdocsSourceBundle>` | A map of version IDs to source bundles. | | `all` | `Array` | Version metadata and source bundles for rendering UI. | | `currentVersion` | `string` | The active version ID. | ## Render versioned pages Use the current version for `/docs` and a specific version for version-prefixed routes: ```tsx title="app/[lang]/docs/[[...slug]]/page.tsx" import { createDocsPage } from "@vercel/geistdocs/pages/docs"; import { config } from "@/lib/geistdocs/config"; import { versions } from "@/lib/geistdocs/source"; const docsPage = createDocsPage({ config, source: versions.current, }); export default docsPage.Page; export const generateStaticParams = docsPage.generateStaticParams; export const generateMetadata = docsPage.generateMetadata; ``` ```tsx title="app/[lang]/v5/docs/[[...slug]]/page.tsx" import { createDocsPage } from "@vercel/geistdocs/pages/docs"; import { config } from "@/lib/geistdocs/config"; import { versions } from "@/lib/geistdocs/source"; const docsPage = createDocsPage({ config, source: versions.byId.v5, getPageUrl: ({ page }) => `/v5${page.url}`, metadata: ({ metadata }) => ({ ...metadata, robots: { index: false, follow: true, }, }), }); export default docsPage.Page; export const generateStaticParams = docsPage.generateStaticParams; export const generateMetadata = docsPage.generateMetadata; ``` Use `getPageUrl` when the route path differs from the source `baseUrl`. This keeps page actions and markdown links pointed at the public URL. ## Add a version selector Use `GeistdocsVersionSelect` when readers need to move between versions: ```tsx title="components/geistdocs/docs-layout.tsx" import { GeistdocsDocsLayout } from "@vercel/geistdocs/layout"; import { GeistdocsVersionSelect } from "@vercel/geistdocs/versions"; import { config } from "@/lib/geistdocs/config"; export const DocsLayout = ({ tree, children }: DocsLayoutProps) => ( <GeistdocsDocsLayout config={config} sidebarTop={ <GeistdocsVersionSelect current="v4" versions={[ { id: "v4", label: "v4" }, { id: "v5", label: "v5 pre-release", routePrefix: "/v5" }, ]} /> } tree={tree} > {children} </GeistdocsDocsLayout> ); ``` Each version can define an `icon` to override the default current/previous version icons: ```tsx <GeistdocsVersionSelect current="v6" versions={[ { id: "v6", label: "v6 latest", icon: <span>6</span> }, { id: "v5", label: "v5", icon: <span>5</span>, routePrefix: "/v5" }, ]} /> ``` Use `GeistdocsRouteSelect` for framework, product, or route switchers that should not use version-specific labels: ```tsx import { GeistdocsRouteSelect } from "@vercel/geistdocs/versions"; <GeistdocsRouteSelect ariaLabel="Select framework" current="nextjs" items={[ { id: "nextjs", label: "Next.js", description: "React framework", icon: <span aria-hidden>N</span>, href: "/:path*", }, { id: "sveltekit", label: "SvelteKit", description: "Svelte framework", icon: <span aria-hidden>S</span>, routePrefix: "/sveltekit", href: "/sveltekit/:path*", }, ]} /> ``` For host-based versions, use `href` instead of `routePrefix`: ```tsx <GeistdocsVersionSelect current="v6" versions={[ { id: "v6", label: "v6 latest" }, { id: "v5", label: "v5", href: "https://v5.example.com/:path*" }, { id: "v4", label: "v4", href: "https://v4.example.com/:path*" }, ]} /> ``` ## Avoid 404s when switching versions By default the selector swaps the route prefix without checking that the page exists in the target version, so pages that exist in only one version 404 on switch. Pass `paths` — computed server-side with `collectVersionSwitchPaths` — and the selector falls back to the nearest existing ancestor (or the version's docs root) instead: ```tsx title="app/[lang]/docs/layout.tsx" import { collectVersionSwitchPaths } from "@vercel/geistdocs/source"; import { GeistdocsVersionSelect } from "@vercel/geistdocs/versions"; import * as root from "next/root-params"; import { config } from "@/lib/geistdocs/config"; import { versions } from "@/lib/geistdocs/source"; const DocsLayout = async ({ children }) => { const lang = await root.lang(); return ( <GeistdocsDocsLayout config={config} sidebarTop={ <GeistdocsVersionSelect current={versions.currentVersion} paths={collectVersionSwitchPaths({ lang, versions })} versions={config.versions} /> } tree={versions.current.source.pageTree[lang]} > {children} </GeistdocsDocsLayout> ); }; ``` `collectVersionSwitchPaths` enumerates every page of every version from the `createVersionedSources` output and returns paths relative to each version's `routePrefix`, plus each version's docs root as the fallback landing page. When an app rewrites loader URLs at render time (for example serving a `baseUrl: "/docs"` source under `/v5` with a custom `getPageUrl`), pass the same mapping so the collected paths match the public URLs. For sources that span several loaders per version (for example separate docs and cookbook sources), use the lower-level `collectVersionPaths` per version instead: ```tsx import { collectVersionPaths } from "@vercel/geistdocs/source"; const paths = { v4: { fallbackPath: "/docs", paths: collectVersionPaths({ lang, sources: [v4Docs, v4Cookbook] }), }, v5: { fallbackPath: "/docs", paths: collectVersionPaths({ lang, routePrefix: "/v5", sources: [v5Docs, v5Cookbook], }), }, }; ``` Fallback resolution only applies to `routePrefix` versions. Host-based versions (`href`) always keep the plain path swap, since the target host's pages can't be enumerated locally. ## Configure markdown routes Each public route family needs a matching markdown route in `proxy.ts`. Read [Proxy and markdown routes](/docs/proxy) for the proxy configuration. ## Scope search to a version The search dialog does not know which version a reader is on. Pass the current version to the provider as `search.options.tag` and filter on it in your search route, so a query answers from the version the reader is reading. Read [Search scope](/docs/provider#search-scope) for the setup. ## Next steps - Read [Proxy and markdown routes](/docs/proxy) to connect versioned pages to `.md`, `.mdx`, and AI-agent rewrites. - Read [llms.txt](/docs/llms-txt) to configure all-docs markdown output for multiple sources. --- --- title: Proxy and markdown routes description: Add custom request logic while keeping Geistdocs markdown negotiation in the package type: reference summary: Configure createProxy with generated route discovery, request hooks, markdown mappings, and a static Next.js matcher. url: /docs/proxy source: apps/template/content/docs/proxy.mdx prerequisites: - /docs/configuration related: - /docs/migration - /docs/md - /docs/llms-txt - /docs/versioned-docs --- # Proxy and markdown routes The Geistdocs proxy handles markdown negotiation, AI-agent rewrites, request tracking, and i18n fallback. Add site-specific request logic with hooks instead of copying the package proxy implementation. Review this Geistdocs `proxy.ts` file. Check that `export const config` is static, custom logic is inside `createProxy` `before` or `after` hooks, and `markdownRoutes` cover every public docs route family. ## Basic proxy Generated projects use `createProxy` with the default `/docs` markdown route: ```ts title="proxy.ts" import { createProxy } from "@vercel/geistdocs/proxy"; import { config as geistdocsConfig } from "@/lib/geistdocs/config"; import { trackMdRequest } from "@/lib/geistdocs/md-tracking"; const proxy = createProxy({ config: geistdocsConfig, trackMarkdownRequest: trackMdRequest, }); export const config = { matcher: [ "/((?!api(?:/|$)|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)", ], }; export default proxy; ``` Keep `export const config` as a static object. Next.js reads proxy matchers at build time and does not support calling a helper for this export. ## Custom request logic Use `before` for logic that should run before Geistdocs markdown handling. Return a `Response` to stop processing. ```ts title="proxy.ts" 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, defaultLanguage }) => { if (request.nextUrl.pathname === "/") { return NextResponse.rewrite( new URL(`/${defaultLanguage}/home`, request.url) ); } return null; }, }); ``` Use `after` for logic that should run after markdown handling but before the i18n fallback: ```ts title="proxy.ts" const proxy = createProxy({ config: geistdocsConfig, after: async ({ request }) => { const session = await getSessionFromReq(request); if (!session) { return null; } return refreshSession(request, session); }, }); ``` Use `transformRewrite` when every package-owned HTML and Markdown destination needs an internal route segment. Geistdocs resolves Markdown negotiation and the locale first, then passes the app-local destination to the callback. Geistdocs continues to apply the configured base path, query string, and representation headers. ```ts title="proxy.ts" const proxy = createProxy({ config: geistdocsConfig, transformRewrite: async (pathname, { request }) => { const segment = await resolveRouteSegment(request); return `/${segment}${pathname}`; }, }); ``` For example, an HTML request for `/docs/functions` passes `/en/docs/functions`, while its Markdown representation passes `/en/llms.mdx/functions`. The callback must return an app-local path that starts with `/`. Avoid network work that does not belong on every matched request. ## Markdown route mappings Define `content` sections in `lib/geistdocs/config.tsx` to infer standard Markdown route mappings from each section route. The package still serves `/llms.txt` and individual page Markdown through route helpers; the proxy uses these mappings for `.md`, `.mdx`, `Accept: text/markdown`, and AI-agent requests. When `agent` metadata is configured, a request to the homepage with `Accept: text/markdown` returns the generated `/agents.md` content. This gives agents a product overview and focused discovery links without treating every application route as documentation. Geistdocs does not infer Markdown mappings for `content` sections with `route: "/"`. A root catch-all such as `/*path` can make a homepage or other application routes look like documentation. Use explicit `markdownRoutes` for root-mounted docs. ```tsx title="lib/geistdocs/config.tsx" export const config = defineConfig({ // ... content: [{ id: "docs", label: "Docs", dir: "content/docs", route: "/docs" }], }); ``` Use `additionalMarkdownRoutes` to add a standalone endpoint, such as a [changelog](/docs/changelog), without replacing docs mappings: ```ts title="proxy.ts" const proxy = createProxy({ config: geistdocsConfig, additionalMarkdownRoutes: [ { from: "/changelog", to: "/[lang]/changelog.md" }, ], }); ``` Additional mappings are prepended to inferred or default mappings, or to explicit `markdownRoutes`. The first match wins, so a standalone route takes precedence over a root `/*path` catch-all. Keep mapping paths app-local and install the destination Route Handler separately. Use `markdownRoutes` when you need to replace the inferred mappings: ```ts title="proxy.ts" const proxy = createProxy({ config: geistdocsConfig, markdownRoutes: [ { from: "/docs/*path", to: "/[lang]/llms.mdx/*path" }, { from: "/cookbook/*path", to: "/[lang]/llms.mdx/cookbook/*path" }, { from: "/v5/docs/*path", to: "/[lang]/v5/llms.mdx/*path" }, ], }); ``` With these mappings: | Request | Rewritten destination | | --- | --- | | `/docs/getting-started.md` | `/en/llms.mdx/getting-started` | | `/cookbook/install.mdx` | `/en/llms.mdx/cookbook/install` | | `/v5/docs/intro` with `Accept: text/markdown` | `/en/v5/llms.mdx/intro` | Use `[lang]` in the destination when your markdown route is nested under `app/[lang]`. Use `*path` or `:path*` to insert the matched wildcard path. `markdownRoutes` are app-local even when `config.basePath` is set. For a Next.js `basePath` of `/docs`, keep a mapping such as `{ from: "/*path", to: "/[lang]/llms.mdx/*path" }`. `createProxy` preserves `/docs` when it rewrites the public request. ## Root-mounted docs For docs served from the site root, set the source `baseUrl` and config route to `/`: ```ts title="lib/geistdocs/source.ts" export const geistdocsSource = createSource({ docs, config, baseUrl: "/", }); ``` ```tsx title="lib/geistdocs/config.tsx" export const config = defineConfig({ // ... content: [{ id: "docs", label: "Docs", dir: "content/docs", route: "/" }], }); ``` If the whole site is documentation and there is no separate homepage, you can map all root paths to page-level Markdown: ```ts title="proxy.ts" const proxy = createProxy({ config: geistdocsConfig, markdownRoutes: [{ from: "/*path", to: "/[lang]/llms.mdx/*path" }], }); ``` If a homepage or app routes also live at `/`, do not use a broad `/*path` mapping. Enumerate each docs route family instead: ```ts title="proxy.ts" const proxy = createProxy({ config: geistdocsConfig, markdownRoutes: [ { from: "/api-reference/*path", to: "/[lang]/llms.mdx/api-reference/*path" }, { from: "/guides/*path", to: "/[lang]/llms.mdx/guides/*path" }, { from: "/concepts/*path", to: "/[lang]/llms.mdx/concepts/*path" }, ], }); ``` When root-mounted docs are the entire base-path application, include `"/"` explicitly in the static matcher. Next.js does not apply a catch-all matcher to the base-path root: ```ts title="proxy.ts" export const config = { matcher: [ "/", "/((?!api(?:/|$)|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)", ], }; ``` The package keeps `llms.txt`, `sitemap.md`, `agents.md`, MCP discovery, and RSS out of root page-Markdown negotiation. Continue to exclude site-specific application and API routes with the matcher or a `before` hook. A root Markdown page is exposed as `/index.md`, or `<basePath>/index.md` publicly. ## Negotiate HTML and Markdown Explicit `Accept` preferences determine the response for extensionless mapped URLs. Geistdocs recognizes `text/markdown`, `text/x-markdown`, and `text/plain` as Markdown media types and compares their quality and specificity with HTML media types. An explicit HTML preference or Markdown with `q=0` continues to the app's normal HTML handling. Agent detection acts as a fallback when `Accept` is missing or does not distinguish HTML from Markdown. Explicit `.md` and `.mdx` URLs always request Markdown, even when the client usually prefers HTML. Geistdocs does not return `406 Not Acceptable` when Markdown is not selected. ## Recover unmatched Markdown requests `createGeistdocs` in `next.config.ts` generates a lightweight manifest from App Router pages and route handlers. When `agent` is enabled, `createProxy` uses this manifest to return a Markdown 404 only when an eligible Markdown `GET` or `HEAD` request matches neither a Markdown mapping nor an application route. Requests that prefer HTML keep the app's normal HTML recovery, including requests from detected agents. The manifest contains route patterns, not page modules or content. Static, dynamic, catch-all, optional catch-all, grouped, and localized routes continue to Next.js. Root catch-all pages are treated as potentially valid, so the app remains responsible for their 404 behavior. The package's own `[...not-found]` route handler is excluded so it does not claim every path. The integration also resolves `next.config.ts` rewrites. Static rewrite sources are matched exactly, while parameterized sources protect their safe static prefix. If a rewrite begins with a dynamic root segment, Geistdocs disables automatic recovery rather than risk returning a Markdown 404 for a real rewritten route. No proxy option is required. Pass `markdownNotFound: false` only when the site needs to disable automatic recovery: ```ts title="proxy.ts" const proxy = createProxy({ config: geistdocsConfig, markdownNotFound: false, }); ``` Existing sites that use `createMDX` directly do not receive a generated manifest and keep the previous opt-in behavior. Restart `next dev` after adding, deleting, or renaming a route; production builds always regenerate the manifest. ## Hook context Both `before` and `after` receive the same context: | Property | Type | Description | | --- | --- | --- | | `request` | `NextRequest` | The incoming request. | | `context` | `NextFetchEvent` | The proxy event, including `waitUntil`. | | `defaultLanguage` | `string` | The configured default language. | | `languages` | `string[]` | All configured languages. | ## Next steps - Read [.md extension](/docs/md) to understand single-page Markdown responses. - Read [Migration guide](/docs/migration) to move existing middleware behavior into `createProxy` hooks. - Read [Versioned docs](/docs/versioned-docs) to map multiple public route families. --- --- title: Add a changelog description: Add a paginated changelog from Changesets, a CMS, or an API, with Show more, numbered archives, and Markdown output. type: guide summary: Connect collection or paged ChangelogSource data to five thin HTML, Markdown, and JSON adapters, preserving existing docs mappings. url: /docs/changelog source: apps/template/content/docs/changelog.mdx prerequisites: - /docs/configuration related: - /docs/proxy - /docs/agent-readiness --- # Add a changelog Publish release notes in batches with **Show more**, numbered archive pages, and matching Markdown output. Geistdocs provides the page UI; your app owns the source, caching, and route adapters. In the Geistdocs repository, `/changelog` reads the package's released `CHANGELOG.md` with 10 entries per page. The standalone `apps/example-basic` app uses one entry per page to demonstrate pagination in English and Chinese. These examples are not enabled in newly scaffolded sites. The template's `(changelog)` route group and repo-only proxy mappings are excluded from the scaffold bundle. ## How consumers connect a changelog Your app chooses where release data comes from. Geistdocs validates the entries and renders the shared page design, metadata, and Markdown output. 1. Create a server-side source that returns a collection or a requested page of releases, newest first. 2. Share the source, paths, and `pageSize` across the route factories. 3. Add the five thin adapters below for root HTML, numbered HTML, root Markdown, numbered Markdown, and JSON data. 4. Add Markdown proxy mappings and a navigation link. Adding the page is the opt-in. There is no global enable flag, and the package does not create App Router files in an existing site. ## Read a Changesets changelog Use `createChangesetsChangelogSource` from `@vercel/geistdocs/changelog` to read the durable `CHANGELOG.md` produced by Changesets. It does not read pending `.changeset/*.md` files or run the release process. Create a server-side file that shares the source and options across all five adapters: ```ts title="lib/geistdocs/changelog.ts" import { readFile } from "node:fs/promises"; import path from "node:path"; import { createChangesetsChangelogSource } from "@vercel/geistdocs/changelog"; import { config } from "@/lib/geistdocs/config"; const readChangelog = async () => { "use cache"; return await readFile(path.join(process.cwd(), "CHANGELOG.md"), "utf8"); }; export const changelogOptions = { config, source: createChangesetsChangelogSource({ read: readChangelog }), path: "/changelog", markdownPath: "/changelog.md", pageSize: 10, title: "Changelog", description: "The latest releases, improvements, and fixes.", }; ``` This path assumes `CHANGELOG.md` is in the app root, where `process.cwd()` points when Next.js runs. Use a known, statically analyzable path so deployment file tracing can include the file. For a monorepo file outside the app root, use the correct relative path and configure deployment tracing bounds, including `outputFileTracingRoot`, if the deployed server needs to read it at runtime. A cache miss or refresh can still require the file. The reader accepts a string or a promise of a string. The adapter recognizes level-two semantic version headings such as `## 1.2.0`, preserves file order and release Markdown, and skips `## Unreleased`. It returns version-only entries, which display as `v1.2.0`. The version becomes the entry ID, with `+` replaced by `_` for safe anchors. Changesets entries have no publication date; Geistdocs does not infer dates from Git history or file timestamps. ## Add the HTML page Use the template's `getRootLang` helper with Cache Components: ```tsx title="app/[lang]/changelog/page.tsx" import { createChangelogPage } from "@vercel/geistdocs/pages/changelog"; import { changelogOptions } from "@/lib/geistdocs/changelog"; import { getRootLang } from "@/lib/geistdocs/root-params"; const changelogPage = createChangelogPage({ ...changelogOptions, getLang: getRootLang, }); export const generateMetadata = changelogPage.generateMetadata; export const generateStaticParams = changelogPage.generateStaticParams; export default changelogPage.Page; ``` The root adapter uses `Page`, `generateMetadata`, and `generateStaticParams`. The page includes its own `<main>` element, so render it without another main wrapper. Without `getLang`, the helper reads `params.lang`; apps using root parameters should pass their server-side language getter. `config` and `source` are required. `path` defaults to `/changelog`, `title` to `Changelog`, and `description` to `The latest releases, improvements, and fixes.` Set `pageSize` in the shared options; it defaults to `10` and accepts integers from `1` to `100`. Set `markdownPath` only when both Markdown adapters are installed. It enables the shared **Copy page** control, including **View as Markdown**, and alternate metadata; it does not create an endpoint. These actions use the current numbered page's Markdown endpoint and honor `config.pageActions`, matching the docs controls. Keep `path`, `markdownPath`, and `dataPath` app-local, without a locale or deployment `basePath`. Geistdocs adds those prefixes when generating public links and canonical URLs; it does not strip prefixes you supply. Set `config.siteUrl` for absolute canonical URLs. ## Add numbered HTML pages Use the same factory's archive exports for `/changelog/page/2` and later pages: ```tsx title="app/[lang]/changelog/page/[page]/page.tsx" import { createChangelogPage } from "@vercel/geistdocs/pages/changelog"; import { changelogOptions } from "@/lib/geistdocs/changelog"; import { getRootLang } from "@/lib/geistdocs/root-params"; const changelogPage = createChangelogPage({ ...changelogOptions, getLang: getRootLang, }); export const generateMetadata = changelogPage.generatePaginatedMetadata; export const generateStaticParams = changelogPage.generatePageParams; export default changelogPage.PaginatedPage; ``` `PaginatedPage` renders only the requested batch, with **Show more** for older releases and **Newer releases** for the previous page. Archive metadata uses the numbered HTML canonical URL and its `.md` alternate. `/changelog/page/1` redirects to `/changelog`. ## Add the optional Markdown route Reuse the shared options for the root Markdown endpoint: ```ts title="app/[lang]/changelog.md/route.ts" import { createChangelogMarkdownRoute } from "@vercel/geistdocs/routes/changelog"; import { changelogOptions } from "@/lib/geistdocs/changelog"; const changelogRoute = createChangelogMarkdownRoute(changelogOptions); export const GET = changelogRoute.GET; export const generateStaticParams = changelogRoute.generateStaticParams; ``` The root route returns the first batch with anchor IDs and a **Next** Markdown link when older releases exist. Its HTTP `Link` header points to the root HTML canonical URL. `path` still identifies the HTML page, not the Markdown endpoint. Route Handlers use context `params` to resolve the language. Add the numbered Markdown handler as a sibling route. The same `GET` reads the optional `params.page`: ```ts title="app/[lang]/changelog-pages.mdx/[page]/route.ts" import { createChangelogMarkdownRoute } from "@vercel/geistdocs/routes/changelog"; import { changelogOptions } from "@/lib/geistdocs/changelog"; const changelogRoute = createChangelogMarkdownRoute(changelogOptions); export const GET = changelogRoute.GET; export const generateStaticParams = changelogRoute.generatePageParams; ``` The proxy maps `/changelog/page/2.md` to `/[lang]/changelog-pages.mdx/2` internally. This handler serves the second batch with **Previous** and, when applicable, **Next** links. Its canonical `Link` header points to `/changelog/page/2`, not the internal handler path. Add this mapping to your existing `createProxy` options, preserving its hooks, tracking, and static matcher: ```ts title="proxy.ts" import { createProxy } from "@vercel/geistdocs/proxy"; import { config as geistdocsConfig } from "@/lib/geistdocs/config"; const proxy = createProxy({ config: geistdocsConfig, additionalMarkdownRoutes: [ { from: "/changelog/page/*path", to: "/[lang]/changelog-pages.mdx/*path", }, { from: "/changelog", to: "/[lang]/changelog.md" }, ], }); export const config = { matcher: [ "/((?!api(?:/|$)|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)", ], }; export default proxy; ``` `additionalMarkdownRoutes` is prepended to inferred or default docs mappings, or to explicit `markdownRoutes`. The changelog mapping therefore wins before a root `/*path` catch-all without removing docs negotiation. Explicit `markdownRoutes` still replaces inferred mappings. Keep mapping paths app-local and use `[lang]` in the destination. Put the numbered mapping before the root mapping. These mappings enable `.md`, `.mdx`, `Accept: text/markdown`, and detected-agent requests for root and numbered pages, including localized paths such as `/cn/changelog/page/2.md`. Restart `next dev` after adding routes so the app-route manifest is regenerated. ## Add the Show more data handler **Show more** requires a JSON route that serves the next batch using the same source and `pageSize`: ```ts title="app/[lang]/changelog/data/[page]/route.ts" import { createChangelogDataRoute } from "@vercel/geistdocs/routes/changelog"; import { changelogOptions } from "@/lib/geistdocs/changelog"; const changelogRoute = createChangelogDataRoute(changelogOptions); export const GET = changelogRoute.GET; export const generateStaticParams = changelogRoute.generateStaticParams; ``` The default `dataPath` is `${path}/data`, so this setup fetches `/changelog/data/2` on the first **Show more** activation. To use another endpoint, set `dataPath` in the shared options and place this handler at the matching route. Configuring `dataPath` alone does not create a handler. The root is pre-rendered with only the first 10 entries by default. The browser receives one batch initially, not the entire history. With JavaScript, **Show more** fetches and appends the next JSON batch without navigating. It shows a loading state and allows retry after a failed request. Without JavaScript, the same normal `href` opens the next numbered HTML page. Readers can also open the link in another tab. ## Use a custom source Replace the Changesets source with an object implementing either form of `ChangelogSource`. A source can read local files, a CMS, a database, GitHub releases, or an HTTP API. `getEntries` is a convenience for a full collection; use `getPage` for a provider that fetches one batch at a time. Keep the shared options and route adapters unchanged: ```ts title="lib/geistdocs/changelog.ts" import type { ChangelogEntry, ChangelogSource, } from "@vercel/geistdocs/changelog"; import { config } from "@/lib/geistdocs/config"; // biome-ignore lint/suspicious/useAwait: Cache Components requires an async reader, including for this local data. const readEntries = async (lang: string): Promise<ChangelogEntry[]> => { "use cache"; return [ { id: "1.2.0", title: lang === "cn" ? "保存搜索筛选条件" : "Saved search filters", body: lang === "cn" ? "保存**搜索筛选条件**,下次访问时继续使用。" : "Save **search filters** for your next visit.", version: "1.2.0", publishedAt: "2026-09-08", }, ]; }; const source: ChangelogSource = { getEntries: ({ lang }) => readEntries(lang), }; export const changelogOptions = { config, source, path: "/changelog", markdownPath: "/changelog.md", pageSize: 10, title: "Changelog", description: "Updates to search and navigation.", }; ``` `getEntries({ lang })` accepts a configured language and returns `ChangelogEntry[]` or a promise of that array. Geistdocs reads and validates the full collection on the server, then selects the requested batch. Only that batch reaches the browser. The source owns translation and fallback policy; this example falls back to English. The Changesets adapter serves the same file for every locale. Unsupported route locales return not found. Keep asynchronous data access in an app-owned `"use cache"` reader with `cacheComponents: true`. Do not add `dynamic`, `revalidate`, or `fetchCache` route exports. Keep source getters and credentials in server-side modules, not the shared `geistdocs.tsx` config, which is available to client components. Each entry follows these constraints: | Field | Requirement | | --- | --- | | `id` | Stable, unique within the source result, and starts with an ASCII letter or digit. Remaining characters can be ASCII letters, digits, `.`, `_`, or `-`. Reuse IDs across translations to preserve release links. | | `title` | Optional text, preferred over `version`. Required when `version` is omitted. A blank title falls back to a provided version. | | `body` | Markdown text, not executable MDX. The UI supports GitHub Flavored Markdown and disables raw HTML. | | `version` | Optional nonempty text. Required when `title` is omitted or blank. | | `publishedAt` | Optional valid ISO date (`2026-09-08`) or timestamp with a timezone. Omit unknown dates. | Return entries newest first. Geistdocs preserves source order without sorting by date or version. Return `[]` when there are no releases; the page shows `No releases published yet.`, and Markdown contains only the page heading and configured description. Source failures and invalid entries propagate as errors rather than appearing as an empty changelog. An empty or title-only Changesets file returns no releases; unrecognized release content throws. ## Choose a title or version Provide a title, a version, or both. When both are present, only the title appears as the release heading. Without a title, the heading uses the version with a `v` prefix; an existing `v` is not duplicated. HTML and Markdown use the same label. ```ts title="Release entry examples" import type { ChangelogEntry } from "@vercel/geistdocs/changelog"; const entries = [ { id: "saved-filters", title: "Saved search filters", version: "1.2.0", body: "Save **search filters** for your next visit.", publishedAt: "2026-09-08", }, { id: "keyboard-navigation", title: "Keyboard navigation improvements", body: "Closing search returns focus to the search button.", publishedAt: "2026-09-04", }, { id: "1.1.1", version: "1.1.1", body: "Fix navigation to nested documentation pages.", }, ] satisfies ChangelogEntry[]; ``` These headings are `Saved search filters`, `Keyboard navigation improvements`, and `v1.1.1`. TypeScript requires at least one of `title` or `version`, and runtime validation rejects entries where neither provides a label. The release heading is also its permalink. Entries rendered on the current page use `#id`; an entry appended from page 2 links to `/changelog/page/2#id`, so reloading the link renders that batch. IDs remain stable when titles change, but numbered page membership can move as releases are published. These links are not permanent per-version URLs across publishing. A separate version badge or permalink button is not rendered. ## Clean Changesets release notes The Changesets adapter removes bare generated commit prefixes, such as `f31d616:`, from Major, Minor, and Patch Changes lists. Existing PR and commit links, code examples, and authored prose remain intact. The HTML page, JSON batches, and Markdown routes use the same cleaned notes. ## Display publication dates Set `publishedAt` when the source provides an authoritative publication date. Dates appear alongside entries on desktop and above their headings on mobile. Timestamps are formatted in UTC; date-only values retain their calendar date. Entries without a date omit it, and a wholly undated changelog has no date column. To add dates to Changesets entries, wrap the Changesets source in your own `getEntries` implementation and join publication dates by version. For example, a consumer could combine local `CHANGELOG.md` bodies with GitHub release publication metadata. Omit dates that cannot be matched reliably. ## Read entries from an HTTP API For a paged API, implement `getPage({ lang, page, pageSize })` and return `{ entries, total }` or a promise of that object. Geistdocs requests only the needed batch, without downloading earlier pages. `total` must be the exact number of releases matching the locale and any source filters, not an estimate or the current batch length. Set the server-only `CHANGELOG_API_URL` environment variable to the endpoint. Set `CHANGELOG_API_TOKEN` if it requires a bearer token. This example validates an API response matching the paged source contract: ```ts title="lib/geistdocs/changelog.ts" import { type ChangelogSource, validateChangelogEntries, } from "@vercel/geistdocs/changelog"; import { config } from "@/lib/geistdocs/config"; const readPage = async (lang: string, page: number, pageSize: number) => { "use cache"; const endpoint = process.env.CHANGELOG_API_URL; if (!endpoint) { throw new Error("Set CHANGELOG_API_URL to your release notes endpoint."); } const url = new URL(endpoint); url.searchParams.set("lang", lang); url.searchParams.set("page", String(page)); url.searchParams.set("pageSize", String(pageSize)); const token = process.env.CHANGELOG_API_TOKEN; const response = await fetch(url, { headers: token ? { Authorization: `Bearer ${token}` } : undefined, }); if (!response.ok) { throw new Error(`Release notes request failed: ${response.status}`); } const result: unknown = await response.json(); if ( !result || typeof result !== "object" || !("entries" in result) || !Array.isArray(result.entries) || !("total" in result) || typeof result.total !== "number" || !Number.isSafeInteger(result.total) || result.total < 0 ) { throw new Error("Invalid release page response."); } const entries = validateChangelogEntries(result.entries); const remaining = Math.max(0, result.total - (page - 1) * pageSize); if (entries.length !== Math.min(pageSize, remaining)) { throw new Error("Release page length does not match its total."); } return { entries, total: result.total }; }; const source: ChangelogSource = { getPage: ({ lang, page, pageSize }) => readPage(lang, page, pageSize), }; export const changelogOptions = { config, source, path: "/changelog", markdownPath: "/changelog.md", pageSize: 10, title: "Changelog", description: "The latest releases, improvements, and fixes.", }; ``` The query parameters are an example API convention, not a Geistdocs requirement. Adapt the request and response mapping to your provider. Each in-range page must contain exactly `Math.min(pageSize, total - (page - 1) * pageSize)` entries. Return `{ entries: [], total: 0 }` for an empty history. Use a consistent newest-first order across pages, with unique IDs across the history. Consumers choose the source and cache policy. This cached reader keys data by locale, page, and page size; configure cache lifetime or invalidation to match your publishing workflow. Only normalized release data reaches the public HTML and JSON endpoints. Keep credentials in server-side modules, not entry bodies or client-visible configuration. ## Make the changelog discoverable Add `{ label: "Changelog", href: "/changelog" }` to `nav` in `geistdocs.tsx`. For agent discovery, you can also add a resource link through [`agent.links`](/docs/agent-readiness). Standalone changelog entries are not automatically included in docs search, `/llms.txt`, or sitemaps. The Markdown endpoint and discovery links expose the changelog separately from the documentation corpus. ## Configure archive generation and errors The HTML and Markdown factories expose `generatePageParams` for numbered archives. Export it as Next.js `generateStaticParams` to prebuild archive pages, as shown above. It reads page 1's total for each configured locale to generate `{ lang, page }` values starting at page 2. Rendering those archives then reads their respective batches. Prebuilding is optional; consumers can choose on-demand archive rendering instead. The data factory's `generateStaticParams` also includes page 1. Page parameters are positive decimal integers without leading zeroes. Invalid values such as `0`, `02`, or `abc`, unsupported locales, and pages beyond the total return not found. Markdown and JSON handlers return HTTP `404`; HTML uses Next.js not-found handling. Source failures, invalid totals, invalid entries, and mismatched in-range batch lengths propagate as errors rather than appearing as empty releases or a missing page. Individual release routes and a changelog RSS feed are not included. ## Check the routes locally Open the template's `/changelog` and confirm it initially renders 10 entries. Select **Show more** and confirm it renders 20 entries without leaving the page. Reload a newly appended entry's title link and confirm the entry appears on its numbered archive. Open `/changelog/page/2` and `/changelog/page/2.md`. Both should contain only the second batch; HTML metadata and the Markdown canonical `Link` header should identify `/changelog/page/2`. Root `/changelog.md` should contain only the first batch and a **Next** link, while the second Markdown page has **Previous** and **Next** links when applicable. Disable JavaScript and confirm **Show more** navigates to page 2. The basic example uses `pageSize: 1`, so its root shows the custom title and **Show more** appends `v1.1.1`. Repeat these checks under `/cn/changelog` for Chinese body text and localized canonical URLs. `/docs/changelog` remains the separate setup guide, and existing `/docs` Markdown negotiation should continue to work. --- --- title: .md Extension description: Access documentation pages as raw Markdown by appending .md or .mdx to any URL type: conceptual summary: Access any documentation page as raw Markdown by appending .md or .mdx to the URL for AI tool consumption. url: /docs/md source: apps/template/content/docs/md.mdx related: - /docs/agent-readiness - /docs/llms-txt - /docs/proxy --- # .md Extension Geistdocs lets AI tools and language models access documentation pages as plain Markdown. Append `.md` or `.mdx` to a documentation URL to get the page content with presentation-only MDX components removed. Help me test raw Markdown routes in this Geistdocs project. Fetch a docs page with `.mdx` appended, compare it to the rendered page, and explain how AI tools can use it. ## How it works When you add `.md` or `.mdx` to a documentation URL, Geistdocs returns the page content as Markdown instead of rendered HTML. AI tools can use that response as focused context for one page. ### Example ``` # Normal page https://yourdomain.com/docs/getting-started # Markdown version https://yourdomain.com/docs/getting-started.mdx ``` The `.mdx` version returns: - Frontmatter with the canonical page URL, docs index, and last-updated date when available - The full page content as plain Markdown - No HTML, styling, or navigation For a docs section mounted at `/docs`, the section root remains `/docs.md`. For a source mounted at the app root with `baseUrl: "/"`, Geistdocs uses `/index.md` for the root page. If Next.js has `basePath: "/docs"`, that public root Markdown URL is `/docs/index.md`. Copy Page, View as Markdown, and the `text/markdown` metadata alternate all use the same package resolver. View as Markdown opens the resolved `.md` URL in a new tab from a button, which keeps the URL shareable without adding an anchor to the page. Use `createDocsPage({ getMarkdownUrl })` only when the deployment needs a different public contract. ## Use cases This feature is particularly useful for: - **AI Chat Tools** - Tools like ChatGPT, Claude, and Cursor can fetch and read your docs - **LLM Context** - Provides clean text for language model prompts - **Documentation Analysis** - Extract content for processing or analysis - **Content Migration** - Export documentation in a clean format ## Implementation The package proxy routes `.md` and `.mdx` requests to the package-backed markdown route handler: ```ts title="proxy.ts" const proxy = createProxy({ config: geistdocsConfig, markdownRoutes: [ { from: "/docs/*path", to: "/[lang]/llms.mdx/*path" }, ], }); ``` The destination route uses `createDocsMarkdownRoute` to process the page and return Markdown with a `text/markdown` content type: ```ts title="app/[lang]/llms.mdx/[[...slug]]/route.ts" import { createDocsMarkdownRoute } from "@vercel/geistdocs/routes/llms"; import { filterPublicPage } from "@/lib/geistdocs/page-access"; import { geistdocsSource } from "@/lib/geistdocs/source"; export const { GET, generateStaticParams } = createDocsMarkdownRoute({ sources: [geistdocsSource], canViewPage: filterPublicPage, }); ``` Pass the same stable public filter to `createDocsMarkdownRoute` that you use for `llms.txt`, sitemap, search, search export, and Ask AI. When `canViewPage` denies an existing page, Geistdocs returns the same `404` response as a missing page, omits its canonical header and content, and removes it from suggested pages. Configuring `canViewPage` also keeps missing responses at `404`, even when `notFound.status` is `410`, so response status cannot reveal whether a denied page exists. Read [Proxy and markdown routes](/docs/proxy) to configure additional route families such as `/cookbook` or `/v5/docs`. ## Response Format The response includes: - Content-Type: `text/markdown` - `Link: <canonical page URL>; rel="canonical"` for resolved pages - Frontmatter with page metadata and discovery links - Page content with presentation-only MDX components removed All MDX components are processed and converted to plain Markdown equivalents. ### Customize frontmatter Geistdocs generates `title`, `description`, `url`, `docs_index`, and `lastUpdated` when their values are available, along with supported page frontmatter such as `prerequisites` and `related`. Add product-specific fields or override defaults with the source `markdown.frontmatter` hook: ```ts title="lib/geistdocs/source.ts" export const geistdocsSource = createSource({ config, docs, markdown: { frontmatter: (defaults) => ({ docs_index: "/docs/llms.txt", version: "16.3.4", }), }, }); ``` The callback returns only additions and overrides; Geistdocs keeps the remaining generated fields from `defaults`. ## Missing documentation pages When a Markdown request does not match a page, Geistdocs returns an agent-readable `Page Not Found` response. It compares the requested path with visible page paths and titles, then links up to five likely matches with their descriptions. If no match is useful, the response links to `/llms.txt` instead. For example, a request for `/docs/env-vars` can suggest `/docs/environment-variables.md`. The response uses a short cache lifetime and returns a real `404` status. It also sends `X-Robots-Tag: noindex` and no canonical link because the requested page does not exist. Regular HTML requests keep the app's standard not-found behavior. The suggestion response is enabled by default. Pass `notFound: { status: 410 }` for permanently removed pages, or `notFound: false` to use the standard Next.js not-found response. Legacy `status: 200` values are treated as `404` so missing pages always return a real error status. If a custom `markdownRoutes` mapping changes the public path shape, set `notFound.getRequestedPath` to reconstruct the app-local public pathname. This also keeps page lookup and suggestions within the right source when a flattened route serves multiple sources: ```ts notFound: { getRequestedPath: ({ slug }) => slug[0] === "integrations" ? `/${slug.join("/")}` : ["/docs", ...slug].join("/"), }, ``` Set `notFound.getPageMarkdownUrl` as well when suggested pages use a custom app-local Markdown URL. ### Unmatched application paths When `agent` is enabled, `createGeistdocs` scans App Router pages and route handlers during `next dev` and `next build`. `createProxy` uses that generated manifest to distinguish valid application routes from unknown paths. Valid routes continue to the app's HTML or route-handler response. Unknown `GET` and `HEAD` requests receive a concise Markdown 404 when the client prefers Markdown or when agent detection applies as a fallback. An explicit HTML preference keeps the app's normal HTML recovery. Markdown mappings take precedence over generated app routes, so a route that has both HTML and Markdown continues to negotiate Markdown. Dynamic app routes are treated as potentially valid; the app remains responsible for returning its own 404 when a specific slug does not exist. Pass `markdownNotFound: false` to disable automatic unmatched-path recovery: ```ts title="proxy.ts" const proxy = createProxy({ config: geistdocsConfig, markdownNotFound: false, }); ``` Sites that do not use `createGeistdocs` keep the previous opt-in behavior. Existing boolean and predicate forms remain supported for compatibility. `createNotFoundRoute` remains available for apps that prefer an explicit catch-all Route Handler with Geistdocs' minimal HTML response. --- --- title: Agent readiness description: Expose agents.md and MCP discovery files that help AI agents discover and use your docs site type: guide summary: Generate /agents.md and /.well-known/mcp.json from Geistdocs config so agents can find docs, Markdown surfaces, API specs, and MCP endpoints. url: /docs/agent-readiness source: apps/template/content/docs/agent-readiness.mdx related: - /docs/llms-txt - /docs/md - /docs/proxy --- # Agent readiness Geistdocs can generate `/agents.md` and `/.well-known/mcp.json` discovery files from your site config. These files tell AI agents where your docs live and which machine-readable integration surfaces your product declares. Review this Geistdocs site's `/agents.md` and `/.well-known/mcp.json` files. Check whether they list the product description, documentation links, `llms.txt`, `sitemap.md`, OpenAPI specs, and MCP servers accurately. ## What it provides The generated `/agents.md` file gives agents a stable starting point before they crawl the rest of your documentation. Its product section states when to use the documented product, and the homepage serves the same guidance when an agent requests `text/markdown`. The optional `/.well-known/mcp.json` route advertises configured MCP servers without running an MCP server inside Geistdocs. It can include: - Product name, description, category, audience, and use cases - Human-readable docs entry points - `/llms.txt` for full documentation context - `/sitemap.md` for semantic navigation - Page-level Markdown guidance for `.md` and `.mdx` URLs - OpenAPI, authentication, scopes, rate limit, and error docs when configured - MCP manifests and server URLs when configured - Additional links and agent instructions ## Add the route Generated projects include a thin App Router adapter: ```ts title="app/[lang]/agents.md/route.ts" import { createAgentsRoute } from "@vercel/geistdocs/routes/agents"; import { config } from "@/lib/geistdocs/config"; export const { GET, generateStaticParams } = createAgentsRoute({ config, }); ``` Because the route is under `[lang]`, the Geistdocs proxy can serve `/agents.md` through the default language route. Generated projects also include a thin adapter for MCP discovery: ```ts title="app/[lang]/.well-known/mcp.json/route.ts" import { createMcpManifestRoute } from "@vercel/geistdocs/routes/mcp"; import { config } from "@/lib/geistdocs/config"; export const { GET, generateStaticParams } = createMcpManifestRoute({ config, }); ``` The MCP discovery route returns `404` until you configure `agent.mcp.servers`, so generated sites do not claim MCP support by default. When configured, `/.well-known/mcp.json` returns the declared MCP servers: ```json { "name": "My Product", "description": "My Product helps teams build and deploy applications.", "servers": [ { "name": "My Product MCP", "url": "https://example.com/api/mcp", "description": "Use product tools through MCP." } ] } ``` ## Configure agent metadata Add agent-readiness metadata in `geistdocs.tsx`, then pass it through `defineConfig` in `lib/geistdocs/config.tsx`. ```tsx title="geistdocs.tsx" import type { GeistdocsAgentReadinessConfig } from "@vercel/geistdocs/config"; export const agent = { product: { name: "My Product", description: "My Product helps teams build and deploy applications.", category: "Developer tools", audience: ["Frontend developers", "Platform teams"], useCases: ["Deploy applications", "Manage domains", "Inspect logs"], }, api: { openApiUrl: "https://api.example.com/openapi.json", openApiSpecs: [ { label: "Admin API OpenAPI specification", url: "https://api.example.com/admin/openapi.json", description: "Administrative API operations.", }, ], authDocsUrl: "/docs/authentication", scopesDocsUrl: "/docs/scopes", rateLimitsUrl: "/docs/rate-limits", errorsUrl: "/docs/errors", }, mcp: { manifestUrl: "/.well-known/mcp.json", servers: [ { name: "My Product MCP", url: "https://example.com/api/mcp", description: "Use product tools through MCP.", }, ], }, links: [ { label: "Support", href: "https://example.com/support", description: "Get help from the product team.", }, ], } satisfies GeistdocsAgentReadinessConfig; ``` ```tsx title="lib/geistdocs/config.tsx" import { agent } from "@/geistdocs"; export const config = defineConfig({ // ... agent, }); ``` ## Keep it honest Only list OpenAPI specs, authentication flows, or MCP servers that actually exist. Geistdocs validates new `openApiSpecs` entries and MCP servers rendered by `/.well-known/mcp.json` as absolute `http` or `https` URLs or root-relative paths. Existing `/agents.md` URL fields keep the same URL handling as previous versions. Geistdocs makes product capabilities discoverable, but it does not create APIs or MCP tools for you. Use `/agents.md` and `/.well-known/mcp.json` with [llms.txt](/docs/llms-txt), [.md extension](/docs/md), and [Proxy and markdown routes](/docs/proxy) to give agents a complete discovery path. --- --- title: Ask AI description: An AI-powered chat assistant that helps users understand your documentation with context-aware responses type: conceptual summary: An AI-powered chat assistant with context-aware search, persistent history, and suggested prompts for your documentation. url: /docs/ask-ai source: apps/template/content/docs/ask-ai.mdx prerequisites: - /docs/env related: - /docs/open-in-chat - /docs/llms-txt - /docs/mixedbread-retrieval - /docs/configuration --- # Ask AI The Ask AI feature provides an intelligent chat assistant built directly into your documentation. Users can ask questions about your docs and get instant, context-aware answers powered by AI. The assistant can search through your documentation, understand context, and provide helpful responses. Help me configure Ask AI for this Geistdocs site. Check my AI prompt, suggestions, environment variables, and search route, then recommend improvements for the assistant experience. ## How it works The AI chat assistant is available in two ways: ### 1. Per-Page Quick Access Each documentation page has an "Ask AI about this page" button in the table of contents sidebar. When clicked, it: 1. Opens the chat interface 2. Pre-fills a prompt asking the AI to read the current page 3. Allows users to ask specific questions about that page ### 2. Global Chat Interface Users can open the chat interface at any time by: - Clicking the "Ask AI" button in the navbar - Using the keyboard shortcut: `⌘I` (Mac) or `Ctrl+I` (Windows/Linux) Once open, users can: - Ask questions about any part of your documentation - Upload files or images for context - View AI-powered search results from your docs - Get step-by-step guidance on complex topics ## Add a chat footer Set `ai.footer` to render content below the chat prompt in the desktop panel and mobile drawer. Use it for attribution, links, or other site-specific calls to action. ```tsx title="geistdocs.tsx" export const ai = { footer: ( <a href="https://example.com"> Powered by your AI provider </a> ), }; ``` ## AI SDK dependency ownership Geistdocs Ask AI is built on AI SDK v6. Generated projects install `ai` v6 and `@ai-sdk/react` v3, and `@vercel/geistdocs` owns the chat client, transport, and `createChatRoute` server behavior. When a consumer app already uses the AI SDK outside Geistdocs, treat that as app code. Upgrade the app code to the installed AI SDK version or let the package manager install separate versions if needed. Do not fork Geistdocs chat internals or downgrade Ask AI to match unrelated app usage. Most projects should not customize the chat transport. If you do customize `DefaultChatTransport.prepareSendMessagesRequest`, preserve `messages` in the returned request body. The AI SDK passes `messages` separately from `body`, and returning a custom `body` replaces the default request body. ```ts prepareSendMessagesRequest: ({ body, messages }) => ({ body: { ...body, messages, currentRoute: pathname, }, }); ``` ## Choose an AI mode Ask AI can run in the default AI Gateway mode, through a Vertex-backed proxy, or answered by a hosted eve framework agent. ### Default AI Gateway mode Use the default mode when the Geistdocs site should call the Vercel AI Gateway directly. 1. Leave `GEISTDOCS_CHAT_PROXY_URL` unset. 2. Set `AI_GATEWAY_API_KEY` for local development. Vercel sets this automatically for deployments that have AI Gateway access. 3. Keep `NEXT_PUBLIC_VERCEL_PROJECT_PRODUCTION_URL` set to the site's production host, such as `docs.example.com` or `localhost:3000` for local development. In this mode, Geistdocs uses the local `search_docs` tool during the AI SDK `streamText` loop. #### Choose a model Pass the `model` option to `createChatRoute` to control which model answers Ask AI requests. It accepts any AI SDK `LanguageModel`, including AI Gateway model strings, and defaults to `openai/gpt-5.6-sol`. ```ts title="app/api/chat/route.ts" import { createChatRoute } from "@vercel/geistdocs/routes/chat"; import { config } from "@/lib/geistdocs/config"; import { geistdocsSource } from "@/lib/geistdocs/source"; export const { POST, maxDuration } = createChatRoute({ config, model: "xai/grok-4.5", sources: [geistdocsSource], }); ``` The `model` option only applies in AI Gateway mode. In proxy mode, the Vertex-backed service selects the model. ### Vertex-backed proxy mode Use proxy mode when Ask AI should route model requests through a central Vertex-backed service instead of calling AI Gateway from each site. 1. Deploy or use the central Geistdocs platform proxy that exposes `POST /vertex`. 2. Configure the Vertex deployment to trust the Geistdocs platform Vercel project with Deployment Protection Trusted Sources. 3. Set `GEISTDOCS_CHAT_PROXY_URL` on the Geistdocs site to the platform proxy URL, including `/vertex`: ```txt https://<geistdocs-platform-deployment>/vertex ``` 4. Leave `GEISTDOCS_CHAT_PROXY_TOKEN` unset unless you are using a custom proxy that requires bearer authentication. When `GEISTDOCS_CHAT_PROXY_URL` is set, Geistdocs searches local docs on the first user message, injects the current page and related docs into the final user message, forwards `{ messages, platform: "vercel" }` to the proxy, and streams the AI SDK UI stream response back to the browser. The platform proxy forwards a Vercel OIDC token to Vertex in the `x-vercel-trusted-oidc-idp-token` header. The Vertex deployment should validate the caller through Trusted Sources; the Geistdocs site does not need a Vertex API key. ### Hosted eve agent mode Use eve agent mode when a hosted [eve framework](https://eve.dev) agent should answer Ask AI requests. Configure it with a single field in `geistdocs.tsx`: ```tsx title="geistdocs.tsx" export const ai = { eveAgent: { url: "https://help-eve.example.dev" }, }; ``` The URL flows to `createChatRoute` through the config object you already pass in `app/api/chat/route.ts` — no route changes are required. Geistdocs speaks eve's session API directly: it starts or resumes a durable eve session, streams the agent's NDJSON events, and translates them into the same UI message stream the chat panel already renders. Conversation continuity is handled automatically by round-tripping the eve session handle through message metadata. Eve agent mode keeps the platform-owned behavior from proxy mode: - Local docs retrieval runs on the first user message, and the current page plus related docs are inlined into the message the agent receives. - Locally computed source citations are injected into the response stream, and `excludeFrom: [chat]` page visibility is enforced before anything reaches the agent. - The agent's reasoning shows as a "Thinking..." indicator, its tool calls show as activity labels, and stopping a response cancels the in-flight eve turn. **Authentication.** Requests carry a Vercel OIDC token minted per request, in two headers for the two layers that may guard the agent: `Authorization: Bearer` for eve's channel auth, and `x-vercel-trusted-oidc-idp-token` for Vercel Deployment Protection. Eve's default channel auth (`vercelOidc()`) accepts deployments from the same Vercel team out of the box; cross-team agents can allow the site with `vercelSubject`. If the agent deployment uses Deployment Protection, add the docs site's project as a Trusted Source. For agents with custom auth, pass server-only headers through the `eveAgent` option: ```ts title="app/api/chat/route.ts" export const { POST, maxDuration } = createChatRoute({ config, sources: [geistdocsSource], eveAgent: { headers: async () => ({ authorization: `Bearer ${await mintToken()}` }), }, }); ``` Never put authentication material in `geistdocs.tsx` — the config ships to the client bundle. The agent URL is public configuration; tokens are not. The `eveAgent` option also accepts a `url` override for pointing staging deployments at a different agent. Configuring both `proxy` and an eve agent throws at route creation. ## Features ### Context-Aware Search By default, the AI assistant includes a built-in `search_docs` tool that: 1. Searches through your documentation content 2. Finds relevant pages based on the user's question 3. Shows source citations with links to the referenced pages 4. Uses the content to provide accurate, contextual answers When `GEISTDOCS_CHAT_PROXY_URL` is set, Geistdocs runs local documentation retrieval on the first user message before forwarding the request to the configured proxy. This gives proxy-backed services access to the current page and related local documentation without requiring them to read the site's source files directly. Set `ai.retrieval` to `"mixedbread"` to replace local keyword retrieval with semantic retrieval for default, proxy, and hosted eve modes. Mixedbread returns the matching content chunk, while the AI model and the visible search dialog remain unchanged. Geistdocs falls back to local Orama retrieval when Mixedbread is unavailable. Read [Improve Ask AI answers with Mixedbread](/docs/mixedbread-retrieval) to provision a Store and configure production sync. ### Persistent Chat History Conversations are automatically saved to the browser's IndexedDB, providing: - Chat history that persists across page reloads - Ability to continue previous conversations - Option to start a new chat or clear history - Offline access to past conversations ### Interactive Features - **Suggestions**: First-time users see suggested questions to get started - **File Upload**: Users can attach files or images for context - **Markdown Support**: Responses are formatted with proper Markdown rendering - **Code Syntax Highlighting**: Code blocks in responses include syntax highlighting - **Reasoning Display**: In development mode, view the AI's reasoning process ### Mobile-Responsive The chat interface adapts to different screen sizes: - **Desktop**: Slides in from the right side as a sidebar panel - **Mobile**: Opens as a bottom drawer. ## Configuration The environment variables `AI_GATEWAY_API_KEY` and `NEXT_PUBLIC_VERCEL_PROJECT_PRODUCTION_URL` must be set for the default AI Gateway mode. Set `GEISTDOCS_CHAT_PROXY_URL` to route Ask AI through a Vertex-backed proxy instead. You can read more in the [Environment Variables](/docs/env) section. In default AI Gateway mode, Geistdocs uses `openai/gpt-5.6-sol` via the Vercel AI Gateway. In Vertex-backed proxy mode, the configured proxy controls the upstream model. ## Suggested Prompts You can customize the initial suggested prompts shown to users. These are configured in the Geistdocs configuration file, which you can read more about in the [Configuration](/docs/configuration) section. --- --- title: Configure sidebar navigation description: Organize Geistdocs pages with ordered groups, separators, folder indexes, and nested navigation type: guide summary: Configure sidebar order, section labels, folder landing pages, and deeply nested page trees with meta.json files. url: /docs/guides/nested-navigation source: apps/template/content/docs/guides/nested-navigation.mdx prerequisites: - /docs/getting-started related: - /docs/configuration - /docs/syntax --- # Configure sidebar navigation Use `meta.json` files to organize the Geistdocs sidebar without changing layout components. Each `meta.json` controls the pages and folders in its own directory, so the same pattern works at every nesting level. ## Start with the content tree Directories become sidebar folders, and `.mdx` files become pages. The examples on this page use the following content tree: ```text content/docs/ ├── index.mdx ├── getting-started.mdx ├── architecture.mdx ├── api-reference.mdx ├── changelog.mdx ├── meta.json └── guides/ ├── index.mdx ├── quickstarts.mdx ├── meta.json └── integrations/ ├── nextjs.mdx ├── sveltekit.mdx ├── meta.json └── advanced/ ├── index.mdx ├── caching.mdx ├── security.mdx └── meta.json ``` Every page needs a frontmatter `title`. Add a `description` to summarize the page. For a regular page, the frontmatter title becomes its navigation label unless you set `navTitle`. Folder landing pages use the folder behavior described below. ## Order top-level pages and add separators Add page and folder names to the root `pages` array without file extensions: ```json title="content/docs/meta.json" { "title": "My Product Documentation", "root": true, "pages": [ "index", "getting-started", "---Guides---", "guides", "---Internals---", "architecture", "api-reference", "---Resources---", "..." ] } ``` This configuration produces three labeled groups after the two introductory pages: - `---Guides---` adds a non-clickable **Guides** separator before the `guides` folder. - `---Internals---` adds an **Internals** separator before the architecture and API reference pages. - `---Resources---` adds a **Resources** separator before pages inserted by `...`. The `...` entry inserts every page or folder that is not named elsewhere in the array. Place it where the remaining items should appear. Without `...`, unnamed items do not appear in that folder's sidebar navigation. By default, top-level folders open as dedicated sidebar sections on desktop. To keep every folder in one expandable tree instead, set `sidebarMode="tree"` on the package layout in `components/geistdocs/docs-layout.tsx`: ```tsx title="components/geistdocs/docs-layout.tsx" <PackageDocsLayout config={config} sidebarMode="tree" tree={tree} > {children} </PackageDocsLayout> ``` Tree mode starts first-level folders expanded unless their `meta.json` explicitly sets `"defaultOpen": false`. Readers can collapse any open folder. Nested folders follow their `defaultOpen` setting and automatically open when they contain the active page. On mobile, Geistdocs always renders the same page tree as expandable nested navigation. Both desktop modes and the mobile sidebar use the same `meta.json` files. ## Add a folder landing page Create `index.mdx` inside a nested directory when the folder needs its own landing page: ```mdx title="content/docs/guides/index.mdx" --- title: Guides description: Follow task-focused guides for My Product. --- Choose a guide based on the workflow you want to complete. ``` Then order the folder's child pages in its `meta.json`: ```json title="content/docs/guides/meta.json" { "title": "Guides", "pages": ["quickstarts", "integrations"] } ``` Leave `index` out of a nested folder's `pages` array to use `index.mdx` as the folder landing page. In the desktop section view, Geistdocs exposes a top-level folder index as **Overview**. For a nested folder, the folder label links to its index while the disclosure control expands its children. If you include `index` explicitly in `pages`, Geistdocs treats it as a regular child page instead of the folder landing page. ## Configure deeply nested folders Add another `meta.json` inside each nested directory. For the `integrations` folder from the example tree, group framework pages separately from advanced topics: ```json title="content/docs/guides/integrations/meta.json" { "title": "Integrations", "defaultOpen": true, "pages": [ "---Frameworks---", "nextjs", "sveltekit", "---Advanced---", "advanced" ] } ``` The `advanced` directory can define another level: ```json title="content/docs/guides/integrations/advanced/meta.json" { "title": "Advanced Integrations", "pages": ["caching", "security"] } ``` Because `advanced/index.mdx` exists and is not listed in `pages`, **Advanced Integrations** links to `/docs/guides/integrations/advanced`. Its child pages resolve to: - `/docs/guides/integrations/advanced/caching` - `/docs/guides/integrations/advanced/security` Set `defaultOpen` to `true` when a folder should start expanded. Geistdocs also opens the folders that contain the active page, unless a folder explicitly sets `defaultOpen` to `false`. ## Use a shorter navigation title Add `navTitle` to page frontmatter when the page heading needs more context than the label used in the sidebar, breadcrumbs, and previous or next links: ```yaml title="content/docs/guides/configuration.mdx" --- title: Configure your project for production description: Prepare your project for a production deployment. navTitle: Configuration --- ``` Geistdocs uses **Configure your project for production** for the page heading and metadata. The sidebar, breadcrumb, and previous or next links display **Configuration**. When `navTitle` is omitted or empty, these navigation surfaces use `title`. ## Add a badge to a page Add a short `badge` value to page frontmatter to show status or availability beside its sidebar label: ```yaml title="content/docs/guides/new-api.mdx" --- title: New API description: Use the new API in your application. badge: Beta --- ``` Geistdocs renders the badge in desktop and mobile navigation. Badges also appear on folder landing pages because their sidebar items use the folder's `index.mdx` frontmatter. Keep badge text short so the page label has enough room to remain readable. ## Show HTTP method badges on API reference pages Badge labels that match an HTTP method render with a method-specific color, so readers can distinguish endpoint types in an API reference sidebar at a glance. Set `badge` to the uppercase method name: ```yaml title="content/docs/api-reference/get-user.mdx" --- title: Get a user description: Retrieve a single user by ID. badge: GET --- ``` Geistdocs applies a color for each method: | Badge label | Color | | ---------------- | ----- | | `GET` | Green | | `POST` | Blue | | `PUT`, `PATCH` | Amber | | `DEL`, `DELETE` | Red | Method matching is case-sensitive, so use uppercase labels. `DEL` and `DELETE` render the same red badge, so prefer `DEL` when you want a narrower badge. `New` renders as a low-contrast blue badge; other labels, such as `Beta`, use the default neutral badge. Badge colors adapt to light and dark themes automatically. ## Add links to the navigation The `pages` array can include internal and external links alongside content files: ```json title="content/docs/meta.json" { "pages": [ "index", "[Status](/status)", "external:[GitHub](https://github.com/my-org/my-repo)" ] } ``` Use `[Label](/path)` for an internal link. Prefix an external link with `external:` so Geistdocs opens it in a new tab and shows the external-link indicator. ## Check breadcrumbs for nested pages Geistdocs builds breadcrumbs from the same page tree. A page at `content/docs/guides/integrations/advanced/caching.mdx` receives the folder path **Guides**, **Integrations**, and **Advanced Integrations** before its navigation title. The final breadcrumb uses `navTitle` when set and falls back to `title`. Breadcrumbs render on the server from the page URL and sidebar tree, so they are present in the initial HTML. Top-level pages have no folder ancestors and do not render a breadcrumb. ## Troubleshoot missing navigation items Check these rules when a page or folder does not appear where expected: - Match each `pages` entry to a file or directory name without its extension. - Add a `meta.json` to every folder that needs custom ordering, separators, `defaultOpen` behavior, or a title override. - Add `...` when unlisted files and folders should remain visible. - Leave `index` out of a nested folder's `pages` array when it should act as the folder landing page. - Keep separator labels between three hyphens on each side, such as `---Internals---`. --- --- title: Edit on GitHub description: Allow readers to contribute directly by editing documentation pages on GitHub type: integration summary: A direct link on every documentation page that lets readers propose changes through GitHub pull requests. url: /docs/edit-on-github source: apps/template/content/docs/edit-on-github.mdx related: - /docs/configuration --- # Edit on GitHub The "Edit on GitHub" feature gives readers a direct link to propose documentation changes. This link appears in the table of contents sidebar on every documentation page. Help me configure Edit on GitHub for this Geistdocs site. Check the `github` owner and repo in `geistdocs.tsx`, verify the generated edit URL for this page, and tell me how to disable the action if needed. ## How it works When users click the "Edit this page on GitHub" link, they are taken directly to the source file in your GitHub repository. From there, they can: 1. Fork the repository (if they haven't already) 2. Make their edits in the GitHub web editor 3. Submit a pull request with their changes This workflow leverages GitHub's built-in editing capabilities, making it straightforward for both technical and non-technical users to contribute. ## Configuration The Edit on GitHub link is automatically generated based on the `github` object in your `geistdocs.tsx` configuration file. Set the `owner` and `repo` properties to enable this feature: ```tsx export const github = { owner: "your-username", repo: "your-repo-name", }; ``` If these values are not set, the link will not appear on your documentation pages. ### Generated URL Format The link follows this pattern: ``` https://github.com/{owner}/{repo}/edit/main/content/docs/{file-path} ``` For example, if your page is located at `content/docs/getting-started.mdx`, the generated link would be: ``` https://github.com/your-username/your-repo-name/edit/main/content/docs/getting-started.mdx ``` --- --- title: Feedback description: Collect user feedback with an interactive emotion-based widget that creates GitHub Issues type: integration summary: An interactive feedback widget that collects user sentiment and creates structured GitHub Issues automatically. url: /docs/feedback source: apps/template/content/docs/feedback.mdx prerequisites: - /docs/getting-started related: - /docs/provider - /docs/configuration --- # Feedback The Feedback feature lets readers share page-level feedback through an interactive widget. Feedback is posted to a centralized GitHub repository as issues so your team can track, label, and respond to user input. Help me configure feedback for this Geistdocs project. Check whether feedback is enabled, what `siteId` label is used, and how feedback is sent to the Geistdocs platform and GitHub issues. ## How it works The feedback widget appears in the table of contents sidebar on every documentation page. When users click "Give feedback", they can: 1. Write their feedback message 2. Select an emotion that represents their experience (🤩, 🙂, 😕, 😭) 3. Optionally provide their name and email 4. Submit their feedback ### Feedback Workflow When feedback is submitted: 1. The feedback is validated against a JSON schema 2. A new GitHub Issue is created in the configured feedback repository 3. Labels are automatically applied based on the emotion, topic, and other metadata 4. User details and metadata are formatted into a structured markdown table Each piece of feedback creates a separate issue, allowing for individual tracking and resolution. ## Feedback Schema The API accepts the following fields: | Field | Type | Required | Max Length | Description | | --------- | ------ | -------- | ---------- | ------------------------------------- | | `note` | string | Yes | 16,384 | The feedback message | | `url` | string | No | 1,024 | Page URL where feedback was submitted | | `emotion` | string | No | 1 | Emoji representing user sentiment | | `topic` | string | No | 1,024 | Feedback topic/category | | `name` | string | No | 1,024 | User's name | | `email` | string | No | 1,024 | User's email | | `ua` | string | No | 256 | User agent string | | `label` | string | No | 64 | Custom label for the issue | | `thumbs` | string | No | 4 | Thumbs up/down indicator | | `reason` | string | No | 10 | Reason for thumbs down | | `plan` | string | No | 10 | User's plan type | Additional fields are captured as metadata and displayed in the issue body. ## Features ### Automatic Labeling Issues are automatically labeled based on: - **Emotion**: `emotion-amazed`, `emotion-happy`, `emotion-sad`, `emotion-cry` - **Topic**: `topic-{topic-name}` - **Plan**: `{plan}-plan` - **Thumbs**: `thumbs-up`, `thumbs-down` - **Reason**: `reason-{reason}` (for thumbs down) - **Custom labels**: Any label passed via the `label` field - **Default labels**: `unresolved`, `vercel-site` ### Structured Issue Body Each issue includes a formatted markdown body with: - The feedback note - URL where feedback was submitted - User details table (email, context, IP, user agent) - Metadata table for any additional fields ### Metadata Support Pass any additional fields in the request body, and they are captured as metadata and displayed in a separate table in the issue. This allows you to include: - User IDs - Feature flags - Experiment groups - Custom tracking data --- --- title: Improve Ask AI answers with Mixedbread description: Configure Mixedbread retrieval for a Geistdocs Ask AI chat and sync documentation to a site-specific Store type: guide summary: Provision a Mixedbread Store for one Geistdocs consumer site, enable semantic retrieval, and sync documentation during production builds. url: /docs/mixedbread-retrieval source: apps/template/content/docs/mixedbread-retrieval.mdx prerequisites: - /docs/ask-ai - /docs/env - /docs/deployment related: - /docs/configuration - /docs/llms-txt --- # Improve Ask AI answers with Mixedbread Mixedbread retrieval gives Ask AI the documentation passages that best match a user's question. The AI model stays the same, while semantic retrieval improves the evidence included with each question. Geistdocs keeps the visible search dialog on its local Orama index. Mixedbread only changes the documentation retrieval used by Ask AI. Help me enable Mixedbread retrieval for this Geistdocs consumer site. Confirm that I am in this site's own repository and linked to the correct Vercel project before provisioning a Store. Then verify `siteId`, `ai.retrieval`, the search export route, the build script, and the `MXBAI` environment variables. ## How Mixedbread retrieval works Ask AI searches documentation before sending a question to the configured AI model. The default local retriever matches keywords with Orama. Mixedbread adds semantic matching and returns the relevant content chunk instead of the start of a matching page. The production build performs an incremental sync: 1. Next.js statically generates `/api/search/export` from the site's configured content sources. 2. `geistdocs search sync` uploads new or changed Markdown documents to the site's Mixedbread Store. 3. Ask AI queries the Store by `siteId` and locale. 4. Geistdocs falls back to the local Orama retriever if Mixedbread is unavailable or its credentials are missing at runtime. Pages with `internal: true` or `excludeFrom: [chat]` are omitted from the export and removed from the Store on the next successful sync. ## Prerequisites Before enabling Mixedbread retrieval, confirm that: - The Mixedbread Marketplace integration is installed on your Vercel team. - The consumer site has its own Vercel project. - The consumer repository uses a Geistdocs release that includes `geistdocs search sync` and `createSearchExportRoute`. - `source.config.ts` enables `includeProcessedMarkdown`, as generated Geistdocs projects do by default. - `next.config.ts` enables `cacheComponents`, as generated Geistdocs projects do by default. Next.js must statically prerender `/api/search/export` before the sync command runs. Use one Store per consumer site. Separate Stores isolate deployments and cleanup while keeping the same configuration in every repository. ## Provision a Store from the consumer repository Run the Vercel integration command from the root of the **consumer site's repository**. Do not run it from the `geistdocs` package repository or from another docs site's repository. The current directory must be linked to the Vercel project that deploys this consumer site. Check `.vercel/project.json`, or run `vercel link` and select the correct project before provisioning the Store: ```bash title="Terminal" cd path/to/consumer-site-repository vercel link vercel integration add mixedbread \ --name docs-my-product \ --environment production ``` If the repository is a monorepo, run the command from the directory where you link the target Vercel project. The Vercel project's configured Root Directory can still point to the Geistdocs application inside that repository. The command provisions one Mixedbread Store, connects it to the currently linked Vercel project, and pulls these environment variables: - `MXBAI_API_KEY`: Authenticates server-side Mixedbread API requests. - `MXBAI_STORE_ID`: Identifies this consumer site's Store. Vercel prints a resource link after provisioning. Confirm the connection with: ```bash title="Terminal" vercel integration list --integration mixedbread ``` ## Configure the consumer site Set a unique `siteId` and enable Mixedbread in the consumer repository's root `geistdocs.tsx` file: ```tsx title="geistdocs.tsx" import type { GeistdocsAIConfig, } from "@vercel/geistdocs/config"; export const siteId = "my-product-docs"; export const ai = { retrieval: "mixedbread", } satisfies GeistdocsAIConfig; ``` The generated `lib/geistdocs/config.tsx` spreads `ai` into the package config. Keep `MXBAI_API_KEY` and `MXBAI_STORE_ID` out of `geistdocs.tsx` because that config is available to client components. ## Add the export route to an existing site New Geistdocs projects include the export route. For an existing package-backed site, create this adapter: ```ts title="app/api/search/export/route.ts" import { createSearchExportRoute } from "@vercel/geistdocs/routes/search-export"; import { config } from "@/lib/geistdocs/config"; import { geistdocsSource } from "@/lib/geistdocs/source"; export const { GET } = createSearchExportRoute({ config, sources: [geistdocsSource], }); ``` Pass every configured source in `sources` when the site has multiple sections or documentation versions. ## Sync after each production build New Geistdocs projects run the sync from their build script. For an existing site, update `package.json`: ```json title="package.json" { "scripts": { "build": "next build && geistdocs search sync" } } ``` Deploy the site to production. The first production build uploads every chat-visible page. Later builds upload changed pages and remove stale pages while leaving unrelated Store files untouched. Local and preview builds skip the remote sync. To test a sync outside a production Vercel build, build the site and run the sync with the linked project's production environment variables: ```bash title="Terminal" pnpm build vercel env run --environment production -- \ pnpm exec geistdocs search sync --allow-non-production ``` The sync command fails when Mixedbread retrieval is enabled for a production build but the Store environment variables are missing. The error prints the provisioning command and reminds you to run it from the correct consumer repository. ## Verify retrieval Open the Mixedbread dashboard through Vercel and confirm that the Store contains one file per chat-visible documentation page: ```bash title="Terminal" vercel integration open mixedbread ``` Ask a question that uses different words from the page title. Ask AI should cite the relevant page and answer from the matching section. If Mixedbread returns an error or exceeds the request timeout, Geistdocs logs a warning and retries the question with local Orama retrieval. --- --- title: Internationalization description: Add multi-language support to your documentation site type: guide summary: Serve documentation in multiple languages with automatic translation via the Geistdocs CLI and Fumadocs routing. url: /docs/internationalization source: apps/template/content/docs/internationalization.mdx prerequisites: - /docs/getting-started related: - /docs/configuration --- # Internationalization Geistdocs supports internationalization (i18n) out of the box using Fumadocs' language-aware routing. You can serve documentation in multiple languages with automatic translation via the Geistdocs CLI. Help me add a new language to this Geistdocs project. Update `translations` in `geistdocs.tsx`, run the Geistdocs translate command for `content/docs`, and explain how localized routes are generated. ## How it works The i18n system uses dynamic routing with a `[lang]` URL segment. The default language (English) doesn't show in URLs, while other languages get a prefix: - `/docs/getting-started` - English (default) - `/cn/docs/getting-started` - Chinese Users can switch languages using the language selector in the navigation bar. ## Translating Content Use the Geistdocs CLI to automatically translate your MDX files: ```bash pnpm translate ``` The CLI reads your target locales from `geistdocs.tsx`. If no locales are found in the config, you'll be prompted to enter them manually. Translations are generated using the Geistdocs platform and saved alongside your original file with the locale suffix (e.g., `introduction.cn.mdx`). ### Command Options You can pass options directly to customize the translation behavior: ```bash # Translate all MDX files (uses default pattern) pnpm translate # Translate files matching a specific glob pattern pnpm translate "content/docs/**/*.mdx" # Use a custom config file pnpm translate "content/**/*.mdx" --config custom-config.tsx # Use a custom translation API URL pnpm translate --url "https://custom-api.example.com/translate" ``` | Option | Description | | ----------- | -------------------------------------------------------- | | `--config` | Path to config file with translations (default: `geistdocs.tsx`) | | `--url` | Custom translation API URL (can also use `GEISTDOCS_TRANSLATE_URL` env var) | Pass the glob pattern as the command argument. If you omit it, Geistdocs uses `content/docs/**/*.mdx`. The CLI automatically excludes files that already have a locale suffix (e.g., `getting-started.cn.mdx`) to avoid translating already-translated content. ### Batch Processing When translating multiple files, the CLI automatically batches them into groups of 10. Progress is displayed as each batch completes, and a 1-second delay is added between batches to avoid rate limiting. ## Configuration Configure supported languages in `geistdocs.tsx`: ```typescript title="geistdocs.tsx" export const translations = { en: { displayName: "English", }, cn: { displayName: "中文", search: "搜尋文檔", }, }; ``` The first locale in the `translations` object is treated as your default/source language and is excluded from translation targets. ### Adding a New Language To add a new language: 1. Add a new key to the `translations` object with the language code 2. Add translations for UI elements like `displayName` and `search` 3. Run `pnpm translate "content/**/*.mdx"` to generate translated content The language selector will automatically include your new language. ## Content Organization Translated files are saved alongside the original with a locale suffix: ``` content/docs/ ├── getting-started.mdx # English (default) ├── getting-started.cn.mdx # Chinese ├── getting-started.fr.mdx # French └── getting-started.es.mdx # Spanish ``` Fumadocs automatically serves the correct file based on the URL path. ## Translating UI Elements The `translations` object in `geistdocs.tsx` controls UI text like search placeholders, navigation labels, and error messages. Add translations for each language you support. If you don't provide a translation for a specific element, it falls back to English. ## Language Detection The language is determined by the URL path. There's no automatic detection based on browser settings - users must explicitly choose their language using the language selector. This approach gives users full control and ensures they see the language they expect. --- --- title: llms.txt description: A single file containing all your documentation in plain text for AI consumption type: conceptual summary: A single endpoint that returns all documentation as plain Markdown text following the llms.txt standard. url: /docs/llms-txt source: apps/template/content/docs/llms-txt.mdx related: - /docs/agent-readiness - /docs/md - /docs/ask-ai - /docs/proxy --- # llms.txt Geistdocs implements the [llms.txt standard](https://llmstxt.org/), a convention for exposing documentation as one AI-readable text file. Language models and coding agents can use `/llms.txt` to retrieve broad documentation context. Help me verify this Geistdocs site's AI-readable docs. Check `/llms.txt` and a few `.mdx` page URLs, then explain whether an AI assistant can retrieve the full documentation context. ## What it is The `/llms.txt` endpoint returns all configured documentation pages as Markdown in a single response. Each page is separated by blank lines so AI tools can parse the complete documentation set. When agent product metadata includes a category, audience, or use case, Geistdocs prepends a `When to use` section. It uses `agent.product.name` when set and otherwise uses the site title. Page-level Markdown also links agents to `/agents.md` for declared integration discovery. ### Access ``` https://yourdomain.com/llms.txt ``` Returns all documentation pages concatenated together as Markdown. ## How it works The package route helper: 1. Reads product guidance from the source bundle's Geistdocs config. 2. Fetches pages from one or more source bundles. 3. Processes each page to extract clean Markdown. 4. Combines the guidance and pages into a single response. 5. Returns the response as `text/markdown`. Pages are joined with double newlines (`\n\n`) for clear separation. ## Use cases This feature enables: - **AI Training** - Provide your docs as context for AI assistants - **Search Indexing** - Feed your entire documentation to search systems - **Content Analysis** - Analyze patterns and content across all docs - **Bulk Processing** - Process all documentation at once - **LLM Context** - Give language models complete documentation context ## Configure sources Generated projects use one documentation source: ```ts title="app/[lang]/llms.txt/route.ts" import { createLlmsRoute } from "@vercel/geistdocs/routes/llms"; import { geistdocsSource } from "@/lib/geistdocs/source"; export const { GET } = createLlmsRoute({ source: geistdocsSource, }); ``` Sites with multiple content sections or versions can pass `sources`: ```ts title="app/[lang]/llms.txt/route.ts" export const { GET } = createLlmsRoute({ sources: [docsSource, cookbookSource], }); ``` Use `filterPage` to exclude pages from `/llms.txt`, such as internal or preview-only docs: ```ts title="app/[lang]/llms.txt/route.ts" export const { GET } = createLlmsRoute({ source: geistdocsSource, filterPage: (page) => !page.url.includes("/internal"), }); ``` `filterPage` receives the page plus its language and source. Use the same synchronous filter function for every public aggregate route so `/llms.txt`, `sitemap.md`, local search, Mixedbread search export, and Ask AI expose the same cacheable page set. Pass that function as `canViewPage` to `createDocsMarkdownRoute`. Request-specific HTML access belongs in `createDocsPage({ canViewPage })`; do not personalize public indexes. ## The llms.txt standard The llms.txt standard is a simple convention that makes documentation more accessible to AI tools. It's similar in spirit to `robots.txt` but designed for language models instead of search crawlers. Learn more at [llmstxt.org](https://llmstxt.org/) --- --- title: Open in Chat description: Allow users to continue conversations in their preferred AI chat platform with a single click type: conceptual summary: Transfer documentation context to AI chat platforms like ChatGPT, Claude, Cursor, and v0 with a single click. url: /docs/open-in-chat source: apps/template/content/docs/open-in-chat.mdx related: - /docs/ask-ai - /docs/env --- # Open in Chat The "Open in Chat" feature enables users to seamlessly transfer documentation context to their preferred AI assistant. With a single click, users can open the current page in external AI chat platforms like ChatGPT, Claude, v0, Cursor, and more. Help me test Open in Chat for this Geistdocs site. Verify that page URLs resolve correctly, markdown endpoints work, and each configured AI platform receives useful context. ## How it works On every documentation page, users will find an "Open in chat" button in the table of contents sidebar. When clicked, a menu appears with options to open the page in various AI platforms: 1. User clicks "Open in chat" 2. A dropdown menu shows available platforms 3. User selects their preferred AI assistant 4. The platform opens with a pre-filled prompt containing the page URL and context The prompt automatically includes: ``` Read this page, I want to ask questions about it. [URL] ``` This allows the AI assistant to fetch and understand the page content before answering user questions. ## Supported Platforms The feature integrates with multiple popular AI platforms: ### Code & Development Tools - **[Cursor](https://cursor.sh/)** - AI-powered code editor - **[v0](https://v0.dev/)** - Vercel's AI interface design tool ### General AI Assistants - **[ChatGPT](https://chat.openai.com/)** - OpenAI's conversational AI - **[Claude](https://claude.ai/)** - Anthropic's AI assistant - **[T3](https://t3.gg/)** - T3 Stack AI assistant - **[Scira](https://scira.app/)** - AI-powered documentation assistant ## Configuration The environment variable `NEXT_PUBLIC_VERCEL_PROJECT_PRODUCTION_URL` must be set, which you can read more about in the [Environment Variables](/docs/env) section. ## User Experience ### Desktop Experience The "Open in chat" button appears in the table of contents sidebar with an external link icon. Clicking it reveals a dropdown menu with all available platforms. ### Platform-Specific Behavior Each platform has its own integration: - **Browser-based platforms** (ChatGPT, Claude, v0): Open in a new browser tab with the prompt pre-filled - **Desktop applications** (Cursor): Open using a custom URL scheme that launches the application - **Specialized tools** (T3, Scira): Open with platform-specific parameters and context ## Outcomes ### For Users 1. **Context transfer**: Users do not need to manually copy URLs or switch contexts. 2. **Consistent starting point**: Each AI platform receives the same page context. 3. **Tool choice**: Users can open the AI platform they prefer. ### For Documentation Authors 1. **More support paths**: Users can ask follow-up questions about a page. 2. **Lower support load**: AI assistants can answer common documentation questions. 3. **Platform flexibility**: Teams can support different AI preferences. --- --- title: RSS description: Keep users updated with an automatically generated RSS feed for your documentation type: conceptual summary: An automatically generated RSS 2.0 feed that keeps users informed when documentation is published or updated. url: /docs/rss source: apps/template/content/docs/rss.mdx related: - /docs/env - /docs/configuration --- # RSS The RSS feature provides an automatically generated RSS 2.0 feed for your documentation. This allows users to subscribe to updates and stay informed when new content is published or existing pages are modified. Help me verify the RSS feed for this Geistdocs site. Check the `/rss.xml` route, confirm the production URL environment variable is set, and explain how page metadata affects feed items. ## How it works Geistdocs automatically generates an RSS feed that includes: - All published documentation pages - Page titles and descriptions - Direct links to each page - Last modified dates - Author information The feed is regenerated on each request, ensuring subscribers always receive the most current information about your documentation. ## Accessing the Feed The RSS feed is available at: ``` https://your-domain.com/rss.xml ``` Users can subscribe to this URL using: - RSS readers (Feedly, Inoreader, NewsBlur) - Email services (Blogtrottr, Kill the Newsletter) - Browser extensions - News aggregators ### RSS Button An RSS button appears in the navigation bar so readers can discover and subscribe to your feed. Clicking the button opens the RSS XML in a new tab. The package Footer applies `config.basePath` to this link. Custom RSS generators must also apply the base path to channel and item URLs; source `page.url` values intentionally remain app-local for Next.js navigation. ## Feed Format The RSS feed follows the RSS 2.0 specification and includes: ### Channel Metadata ```xml <channel> <title>Your Documentation Title https://your-domain.com en https://your-domain.com/banner.png All rights reserved [Year], [Your Organization] ``` ### Item Format Each documentation page appears as an item: ```xml page-url Page Title Page description https://your-domain.com/docs/page-url Thu, 12 Nov 2025 00:00:00 GMT Your Organization ``` ## Configuration The RSS feed requires the `NEXT_PUBLIC_VERCEL_PROJECT_PRODUCTION_URL` environment variable to be set, which you can read more about in the [Environment Variables](/docs/env) section. It also requires a `title` exported from the Geistdocs configuration file, which you can read more about in the [Configuration](/docs/configuration) section. ## Features ### Automatic Updates The feed automatically includes: - Newly created pages - Updated page content (using last modified dates) - Changed titles or descriptions - All pages from your documentation source ### Last Modified Dates Pages use their `lastModified` frontmatter field, then the file's most recent git commit date, and fall back to the current date: ```mdx title="content/docs/example.mdx" --- title: Example Page description: An example documentation page lastModified: 2025-11-12 --- ``` Git commit dates require full git history at build time. On Vercel, set the `VERCEL_DEEP_CLONE=true` environment variable so builds clone the full history. Read [Last modified dates](/docs/configuration#last-modified-dates) for the full resolution order. --- --- title: Steps description: Render numbered step-by-step instructions from Markdown headings or the Steps and Step components type: reference summary: Write consecutive headings that start with a number, such as "### 1. Install", to render a numbered step list. The Steps and Step components cover cases where a heading must be wrapped in another component. url: /docs/components/steps source: apps/template/content/docs/components/steps.mdx prerequisites: - /docs/syntax related: - /docs/syntax - /docs/table-of-contents --- # Steps Steps render sequential instructions as a numbered list with a counter beside each step heading. You write them as plain Markdown headings, so a page needs no imports and stays readable in Markdown output. ## Write steps as numbered headings Start each step heading with a number, a period, and a space. Consecutive headings at the same depth that follow this pattern become one step list. ````md ## Deploy the project ### 1. Install the CLI Install the Vercel CLI globally. ```bash pnpm add -g vercel ``` ### 2. Link the project Run `vercel link` in the project directory and pick the team and project. ### 3. Deploy Run `vercel deploy` to create a preview deployment. ## Next section ```` This renders as: ### 1. Install the CLI Install the Vercel CLI globally. ```bash pnpm add -g vercel ``` ### 2. Link the project Run `vercel link` in the project directory and pick the team and project. ### 3. Deploy Run `vercel deploy` to create a preview deployment. ## How numbering and anchors work Geistdocs removes the `1. ` prefix from the heading text at build time and draws the number with a CSS counter. As a result: - The table of contents shows `Install the CLI`, not `1. Install the CLI`. - The heading anchor is `#install-the-cli`, so renumbering steps does not break links. - Custom anchors written as `### 1. Install the CLI [#install]` keep working. - Markdown routes and `llms.txt` output numbered headings, such as `### 1. Install the CLI`, which read naturally for agents. The numbers you write are only used to detect steps. Geistdocs always counts from 1, so `### 4. Deploy` at the start of a list renders as step 1. ## Rules for grouping steps Geistdocs groups headings into a step list using these rules: | Situation | Result | | --- | --- | | Consecutive numbered headings at the same depth | One step list | | A deeper heading inside a step, such as `#### Options` under `### 2. Configure` | Part of the current step | | A heading at the same or shallower depth without a number prefix | Ends the step list | | A numbered heading where inline code or a link immediately follows the number, such as `` ### 1. `vercel login` `` | Not detected as a step; move a word before the code, such as `` ### 1. Run `vercel login` `` | Paragraphs, lists, code blocks, callouts, and images between two step headings belong to the earlier step. ## Use the Steps and Step components Use the components when a step heading has to be wrapped in another component, for example a framework switcher, and the heading pattern cannot be detected. Both components are in the default MDX component map and are exported from the selected package's `components/steps` subpath. ```mdx ### Install the package Run the install command for your package manager. ### Configure the project Add the configuration file to the project root. ``` This renders as: ### Install the package Run the install command for your package manager. ### Configure the project Add the configuration file to the project root. Do not add a number prefix to headings inside `Step`. The component draws the counter, and a prefix would create a nested step list. ### Props `Steps` and `Step` accept the standard `div` attributes. Both forward `className`, which is merged with the `fd-steps` and `fd-step` classes that carry the counter styling. ## Markdown output In `.md` routes and `llms.txt`, each step list becomes plain numbered headings, such as `### 1. Install the CLI`, followed by the step content. This applies to steps written as headings and to steps written with `Steps` and `Step`, so agents read the same sequence without the wrapper markup. --- --- title: Table of contents description: Show an "On this page" outline that tracks the reader's position along a straight guide rail type: reference summary: Geistdocs builds the "On this page" outline from your headings, indents sub-headings under their parent, and tracks the active section along a straight vertical guide rail. url: /docs/table-of-contents source: apps/template/content/docs/table-of-contents.mdx prerequisites: - /docs/getting-started related: - /docs/syntax - /docs/configuration --- # Table of contents Every documentation page shows an **On this page** outline on the right side of the desktop layout. Geistdocs builds it from your headings, so you get an in-page table of contents without extra configuration. This page nests its own headings so you can watch the outline update as you scroll. ## Read the outline The outline lists your headings in document order and highlights the section you are currently reading. ### Track the active section A vertical guide rail runs down the left edge of the outline. As you scroll, an indicator slides along the rail to mark the active heading, so readers always know where they are in a long page. ### Follow the straight guide rail The guide rail stays straight for every heading level. Sub-headings indent to the right of the rail, and the rail no longer bends outward to meet them, which keeps deep pages readable at a glance. ## Structure your headings The outline mirrors your heading hierarchy, so a clear structure produces a clear outline. ### Start each section with an h2 Use `##` for the top-level sections of a page. These sit closest to the guide rail and anchor the rest of the outline. ### Nest sub-sections with an h3 Use `###` for sub-sections. Geistdocs indents them under their parent `##` heading: ```mdx ## Structure your headings ### Nest sub-sections with an h3 #### Add detail with an h4 ``` #### Add detail with an h4 Use `####` when a sub-section needs its own breakdown. Geistdocs indents each level a little further so the relationship stays clear, while the guide rail itself remains a single straight line. ## Adapt to smaller screens On mobile, Geistdocs replaces the sidebar outline with a compact menu in the page bar. The menu lists the same headings as a flat, indented list without the guide rail, so it stays legible on narrow screens. ## Write scannable headings Keep headings short and descriptive so the outline reads like a summary of the page. Name each heading after the goal of its section, and avoid stacking two headings with no content between them. --- --- title: WebMCP description: Expose documentation search and page reading to browser agents. url: /docs/webmcp source: apps/template/content/docs/webmcp.mdx --- # WebMCP Geistdocs includes experimental WebMCP support. WebMCP is disabled by default. To opt in, add it to the `defineConfig` call in `lib/geistdocs/config.tsx`: ```ts export const config = defineConfig({ // ... webmcp: { enabled: true }, }); ``` The provider exposes two read-only tools: | Tool | Input | Result | | --- | --- | --- | | `search_docs` | `query`, a nonempty string up to 500 characters | Results from the existing search API in the current language | | `read_current_page` | None | The current documentation page as Markdown | Search respects the existing search visibility rules. Page reading uses the existing Markdown route and fails if the current page does not serve Markdown. Neither tool sends data to an external model or modifies documentation. Returned documentation is marked as untrusted content. WebMCP requires a compatible browser with the experimental API enabled. Unsupported browsers continue to work normally. This feature uses browser-local tools, independently of the site's remote MCP discovery manifest. ## Verify locally Enable WebMCP testing as described in the [Chrome documentation](https://developer.chrome.com/docs/ai/webmcp). Open a documentation page and inspect its tools: ```js const tools = await document.modelContext.getTools(); const search = tools.find((tool) => tool.name === "search_docs"); await document.modelContext.executeTool(search, JSON.stringify({ query: "configuration" })); const read = tools.find((tool) => tool.name === "read_current_page"); await document.modelContext.executeTool(read, "{}"); ``` Navigate to another documentation page and read it again. Repeat in a translated route and, when configured, under the site's base path. Disable the feature and verify the provider removes its tools. ## agent-browser With agent-browser 0.36.0 or newer, WebMCP is enabled by default in managed Chrome sessions: ```bash agent-browser --session docs-check open http://localhost:3000/docs agent-browser --session docs-check webmcp list agent-browser --session docs-check webmcp invoke search_docs --params '{"query":"configuration"}' agent-browser --session docs-check webmcp invoke read_current_page --params '{}' agent-browser --session docs-check close ``` Discovery includes each tool's origin, frame, schema, and read-only annotations. Check the invocation's `status`: a successfully delivered command can still contain a failed tool execution. Both tools limit network requests to 15 seconds. When the browser supplies an execution signal, cancellation also aborts the request. Some experimental browser versions, including Chrome 152, omit that signal: canceling an invocation in those versions does not immediately abort its underlying fetch, which remains bounded by the request deadline.