Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 119 additions & 0 deletions apps/docs/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
# Geistdocs agent instructions

This app uses the packaged Geistdocs architecture. The `@vercel/geistdocs` package owns shared runtime behavior; this app owns local content, configuration, adapters, and site-specific routes.

Use these instructions when an AI coding agent edits this project.

## Architecture

- Runtime features come from `@vercel/geistdocs`, including the docs page renderer, layout helpers, MDX components, search, Ask AI, markdown routes, proxy helpers, and source helpers.
- `@vercel/geistdocs` owns the Ask AI client, server route behavior, and AI SDK v6 runtime dependencies. Do not fork package internals to fit an older app-level `ai` version.
- Local files are user-owned adapters. They should stay thin and call public package exports from `@vercel/geistdocs/*`.
- Do not copy package internals into the app to make a customization. Prefer configuring an adapter file or upgrading `@vercel/geistdocs`.
- Do not deep import from `@vercel/geistdocs/dist` or edit files in `node_modules/@vercel/geistdocs`.
- Do not edit generated directories such as `.source/`, `.next/`, `node_modules/`, or build output.

## Package Docs For Agents

- When package API behavior is unclear, read the installed package docs in `node_modules/@vercel/geistdocs/docs` before guessing.
- Start with `node_modules/@vercel/geistdocs/docs/agents.md` and `node_modules/@vercel/geistdocs/docs/sitemap.md` to identify the relevant focused page.
- Use `node_modules/@vercel/geistdocs/docs/pages/*.md` for task-specific guidance and `node_modules/@vercel/geistdocs/docs/llms.txt` only when you need broad package context.
- These package docs are read-only generated artifacts. Do not edit files under `node_modules/@vercel/geistdocs`; change local adapter files or update the package instead.

## Common edit targets

| Task | Edit |
| --- | --- |
| Configure site title, logo, nav, GitHub links, AI prompt, suggestions, translations, `basePath`, or `siteId` | `geistdocs.tsx` |
| Add or update documentation pages | `content/docs/**/*.mdx` |
| Control sidebar order, groups, and folder labels | `content/docs/meta.json` |
| Give a page a shorter navigation label | Set `navTitle` in the page's frontmatter |
| Override MDX components | `components/geistdocs/mdx-components.tsx` |
| Wrap the site provider, analytics, or global client behavior | `components/geistdocs/provider.tsx` |
| Customize the docs layout shell | `components/geistdocs/docs-layout.tsx` |
| Configure the Fumadocs source adapter or versioned docs | `lib/geistdocs/source.ts` |
| Configure Fumadocs collections and source-safe MDX processing | `source.config.ts` |
| Configure the docs page renderer | `app/[lang]/docs/[[...slug]]/page.tsx` |
| Configure AI-readable markdown output | `app/[lang]/agents.md/route.ts`, `app/[lang]/.well-known/mcp.json/route.ts`, `app/[lang]/llms.txt/route.ts`, `app/[lang]/llms.mdx/[[...slug]]/route.ts`, `app/[lang]/sitemap.md/route.ts` |
| Configure chat or search APIs | `app/api/chat/route.ts`, `app/api/search/route.ts` |
| Add request handling before or after Geistdocs routing | `proxy.ts` |
| Edit the flag-driven home page | `app/[lang]/home/[code]/**` |
| Edit shared styles | `app/global.css`, `app/styles/geistdocs.css` |

## Content guidelines

- Put docs in `content/docs` unless the project has added another source in `lib/geistdocs/source.ts`.
- Add each new page to `content/docs/meta.json` so it appears in the sidebar.
- Use MDX frontmatter with at least `title` and `description` for documentation pages. Set `navTitle` only when the navigation label should differ from the page heading.
- Keep slugs stable unless the task explicitly includes redirects or link updates.
- When adding translated content, follow the existing locale suffix pattern, such as `page.cn.mdx`.
- Use `CopyPrompt` when a page should give readers a prompt they can copy into a coding agent.

## Routing and proxy guidelines

- Keep App Router route files as thin adapters around package helpers such as `createDocsPage`, `createChatRoute`, `createLlmsRoute`, and `createProxy`.
- Keep `cacheComponents: true` and `partialPrefetching: true` in `next.config.ts`. Do not export `dynamic`, `revalidate`, or `fetchCache` from App Router pages or route handlers.
- Read `[lang]` from `next/root-params` in Server Components (via `lib/geistdocs/root-params.ts`). Keep route context `params` in Route Handlers and Server Actions.
- Use `prefetch={true}` for app-owned links to fully static documentation pages so navigation does not stop at the generic route shell.
- Keep `export const config` in `proxy.ts` as a static object. Next.js must parse proxy matchers at build time.
- Use proxy matcher exclusions that only match `/api` and `/api/...`, such as `api(?:/|$)`. Do not exclude broad prefixes like `api`, because that also excludes routes such as `/api-reference`.
- Preserve markdown negotiation unless the task explicitly changes AI-readable output. Geistdocs serves `/agents.md`, `/llms.txt`, `/.well-known/mcp.json`, and per-page Markdown for `.md`, `.mdx`, `Accept: text/markdown`, and AI-agent requests.
- The homepage rewrite (`/` to `/[lang]/home/[code]`) lives in a `createProxy` `after` hook so it never preempts markdown negotiation.
- When adding custom proxy behavior, prefer `before`, `after`, and `markdownRoutes` options on `createProxy` instead of replacing the proxy.
- Use explicit `markdownRoutes` for root-mounted docs or any site where homepage/app routes coexist with docs routes.
- Keep source URLs, navigation links, `getPageUrl`, and `markdownRoutes` app-local when `config.basePath` is set. Geistdocs derives public page-action and Markdown URLs separately.

## Ask AI and Vertex proxy guidelines

- Leave `GEISTDOCS_CHAT_PROXY_URL` unset and `ai.eveAgent` unconfigured to use the default AI Gateway path. In that mode, `app/api/chat/route.ts` calls `createChatRoute` without a `proxy` option and uses the local docs search tool during the AI SDK `streamText` loop.
- Set `ai.eveAgent: { url }` in `geistdocs.tsx` to answer Ask AI with a hosted eve framework agent instead. The URL flows through the config object; the route file needs no changes. Requests authenticate with a per-request Vercel OIDC bearer token by default; pass server-only headers through the `eveAgent` option on `createChatRoute` for custom auth. Never put auth material in `geistdocs.tsx`. Configuring both `proxy` and an eve agent throws at route creation.
- Geistdocs Ask AI targets AI SDK v6: `ai` v6 and `@ai-sdk/react` v3. Keep those dependencies on the generated package versions unless a `@vercel/geistdocs` release changes them.
- If the app uses `ai` or `@ai-sdk/react` for product code outside Geistdocs, migrate that app code separately or let the package manager install separate versions. Do not downgrade Geistdocs Ask AI to match unrelated app code.
- Set `GEISTDOCS_CHAT_PROXY_URL` only when Ask AI should route through the central Vertex-backed proxy. The value must include the `/vertex` route, such as `https://<geistdocs-platform-deployment>/vertex`.
- Do not add Vertex credentials to a Geistdocs site. The central platform proxy forwards the Vercel OIDC token in `x-vercel-trusted-oidc-idp-token`; the Vertex deployment should trust the platform Vercel project through Deployment Protection Trusted Sources.
- Use `GEISTDOCS_CHAT_PROXY_TOKEN` only for a custom proxy that requires bearer authentication. The default Geistdocs platform `/vertex` proxy does not require it.
- Keep `app/api/chat/route.ts` as a thin adapter around `createChatRoute`. Prefer configuring `GEISTDOCS_CHAT_PROXY_URL` and `GEISTDOCS_CHAT_PROXY_TOKEN` over forking the package chat route.

## Migration guidelines

- When migrating from Fumadocs or a custom Geist docs site, inventory `source.config.ts`, route files, `middleware.ts` or `proxy.ts`, `public/llms.txt`, OG routes, Tailwind CSS setup, and required environment variables before editing.
- Inventory direct app usage of `ai` and `@ai-sdk/react`. Package-owned Ask AI uses AI SDK v6; migrate local AI SDK code separately from Geistdocs route adapters.
- Import source-config helpers from `@vercel/geistdocs/source-config` in `source.config.ts`. Do not import runtime component entry points from source config.
- Move existing `middleware.ts` behavior into `createProxy({ before })` or `createProxy({ after })` hooks.
- Set `openGraph.images` in `createDocsPage` only when the app includes the Geistdocs OG route, or override metadata to avoid broken `/og/...` URLs.
- Add Tailwind CSS v4 `@source` entries for `@vercel/geistdocs` and related runtime dependencies when migrating styles.
- Add local fallbacks for production-only environment variables so migration builds do not require production secrets.

## Package updates

- Use `pnpm exec geistdocs update` to update package-based Geistdocs projects.
- `geistdocs update` updates the `@vercel/geistdocs` dependency. It does not overwrite local adapter files.
- Review dependency changes and run the verification commands before committing an update.

## Commands

Run commands from the repository root unless noted:

- Start development: `pnpm dev -F docs`
- Build for production: `pnpm build -F docs`
- Start the built app: `pnpm start -F docs`
- Regenerate Fumadocs output after dependency installation: `pnpm postinstall -F docs`
- Update Geistdocs: `pnpm -F docs exec geistdocs update`
- Run translations if configured: `pnpm translate -F docs`

## Verification

- Run `pnpm build -F docs` after changing routes, config, source setup, MDX components, or package versions.
- Run `pnpm dev -F docs` and open the changed pages when visual layout, navigation, or MDX rendering changes.
- Check both `/docs` and AI-readable routes such as `/agents.md`, `/.well-known/mcp.json`, `/llms.txt`, or a page-level `.md` URL when changing content routing or proxy behavior.
- Confirm no secrets were added to source files. Use `.env.local` for local values and keep it out of Git.

<!-- BEGIN:nextjs-agent-rules -->

# This is NOT the Next.js you know

This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices.

This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean.

<!-- END:nextjs-agent-rules -->
6 changes: 6 additions & 0 deletions apps/docs/app/[lang]/.well-known/mcp.json/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { createMcpManifestRoute } from '@vercel/geistdocs/routes/mcp';
import { config } from '@/lib/geistdocs/config';

export const { GET, generateStaticParams } = createMcpManifestRoute({
config,
});
4 changes: 4 additions & 0 deletions apps/docs/app/[lang]/[...not-found]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { createNotFoundRoute } from '@vercel/geistdocs/routes/not-found';
import { config } from '@/lib/geistdocs/config';

export const { GET } = createNotFoundRoute({ config });
6 changes: 1 addition & 5 deletions apps/docs/app/[lang]/agents.md/route.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import { createAgentsRoute } from '@vercel/geistdocs/routes/agents';
import { config } from '@/lib/geistdocs/config';

const agentsRoute = createAgentsRoute({
export const { GET, generateStaticParams } = createAgentsRoute({
config,
});

export const GET = agentsRoute.GET;
export const generateStaticParams = agentsRoute.generateStaticParams;
export const revalidate = false;
11 changes: 8 additions & 3 deletions apps/docs/app/[lang]/docs/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import { DocsLayout } from "@/components/geistdocs/docs-layout";
import { getRootLang } from "@/lib/geistdocs/root-params";
import { source } from "@/lib/geistdocs/source";

const Layout = async ({ children, params }: LayoutProps<"/[lang]/docs">) => {
const { lang } = await params;
const Layout = async ({ children }: LayoutProps<"/[lang]/docs">) => {
const lang = await getRootLang();

return <DocsLayout tree={source.pageTree[lang]}>{children}</DocsLayout>;
return (
<div className="bg-background-200">
<DocsLayout tree={source.pageTree[lang]}>{children}</DocsLayout>
</div>
);
};

export default Layout;
6 changes: 6 additions & 0 deletions apps/docs/app/[lang]/home/[code]/highlighted-code.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { CodeBlock } from '@vercel/geistdocs/components/code-block';
import { geistShikiTheme } from '@vercel/geistdocs/shiki-theme';
import { highlight } from 'fumadocs-core/highlight';
import { cacheLife } from 'next/cache';
import type { ComponentProps } from 'react';
import type { BundledLanguage } from 'shiki';

Expand All @@ -17,6 +18,11 @@ export const HighlightedCode = async ({
filename,
caption,
}: HighlightedCodeProps) => {
// Shiki reads Date.now() internally, so Cache Components requires the
// highlight to be cached rather than re-run during prerendering.
'use cache';
cacheLife('max');

// Highlight with the same theme the docs use and render through the
// geistdocs CodeBlock so the home page blocks match the documentation.
const rendered = await highlight(code, {
Expand Down
4 changes: 3 additions & 1 deletion apps/docs/app/[lang]/home/[code]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,21 @@ import { ArrowRight } from 'lucide-react';
import Link from 'next/link';
import { HomeLayout } from '@/components/geistdocs/home-layout';
import { enableBannerFlag, rootFlags } from '@/flags';
import { getRootLang } from '@/lib/geistdocs/root-params';
import { source } from '@/lib/geistdocs/source';

export default async function Layout({
children,
params,
}: LayoutProps<'/[lang]/home/[code]'>) {
const { lang, code } = await params;
const [{ code }, lang] = await Promise.all([params, getRootLang()]);
const bannerFlag = await enableBannerFlag(code, rootFlags);
return (
<>
{bannerFlag ? (
<Link
href="/providers/vercel"
prefetch={true}
className="group flex items-center justify-center gap-x-2 gap-y-1 bg-gray-1000 px-4 py-3 text-center text-background-100 text-xs md:text-sm"
>
<span className="inline-flex shrink-0 items-center rounded-full bg-background-100/15 px-2 py-0.5 font-medium text-[10px] uppercase tracking-wide md:text-[11px]">
Expand Down
37 changes: 32 additions & 5 deletions apps/docs/app/[lang]/home/[code]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
CommandPromptSurface,
CommandPromptViewport,
} from '@vercel/geistdocs/components/command-prompt';
import type { GeistdocsAgentReadinessConfig } from '@vercel/geistdocs/config';
import { generatePermutations } from 'flags/next';
import { FlagValues } from 'flags/react';
import type { Metadata } from 'next';
Expand All @@ -18,6 +19,8 @@ import {
installAudienceFlag,
rootFlags,
} from '@/flags';
import { config } from '@/lib/geistdocs/config';
import { getLocalizedPath } from '@/lib/geistdocs/public-path';
import HeroImage from './components/hero-image';
import { Adaptable, Effortless, Flexible } from './components/illustrations';
import Testimonials from './components/testimonials';
Expand Down Expand Up @@ -63,14 +66,34 @@ export default async function Page() {
return <div>Flag {example ? "on" : "off"}</div>;
}`;

export const dynamicParams = false; // all combinations are known upfront here
// All flag combinations are known upfront and generated statically.
export async function generateStaticParams() {
const codes = await generatePermutations(rootFlags);
return codes.map((code) => ({ code }));
}

export const metadata: Metadata = {
alternates: { canonical: 'https://flags-sdk.dev' },
export const generateMetadata = async ({
params,
}: PageProps<'/[lang]/home/[code]'>): Promise<Metadata> => {
const { lang } = await params;
const agentConfig = config.agent as GeistdocsAgentReadinessConfig | undefined;

return {
alternates: {
...(config.siteUrl
? {
canonical: getLocalizedPath(lang, '/'),
...(agentConfig && agentConfig.enabled !== false
? {
types: {
'text/markdown': getLocalizedPath(lang, '/agents.md'),
},
}
: {}),
}
: {}),
},
};
};

export default async function HomePage({
Expand Down Expand Up @@ -199,7 +222,9 @@ export default async function HomePage({
className="shrink-0 rounded-full"
asChild
>
<Link href="/frameworks/next">Read the Docs</Link>
<Link href="/frameworks/next" prefetch={true}>
Read the Docs
</Link>
</Button>
</div>
<div className="grid grid-cols-1 gap-6 md:gap-20 md:grid-cols-2">
Expand Down Expand Up @@ -237,7 +262,9 @@ export default async function HomePage({
the buttons on the heading's left edge while stacked. */}
<div className="flex flex-col items-start gap-3 md:flex-row md:items-center">
<Button size="lg" asChild className="rounded-full">
<Link href="/frameworks/next">Get Started</Link>
<Link href="/frameworks/next" prefetch={true}>
Get Started
</Link>
</Button>
{/* Root is `w-full items-center` by default, which would centre the
pill against the left-aligned heading while stacked. */}
Expand Down
20 changes: 11 additions & 9 deletions apps/docs/app/[lang]/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,26 @@
import "../global.css";
import "@/lib/geistdocs/site-url-warning";
import { Footer } from "@vercel/geistdocs/footer";
import { Navbar } from "@vercel/geistdocs/navbar";
import { VercelToolbar } from "@vercel/toolbar/next";
import type { Metadata } from "next";
import { GeistdocsProvider } from "@/components/geistdocs/provider";
import { config } from "@/lib/geistdocs/config";
import { mono, sans } from "@/lib/geistdocs/fonts";
import { i18n } from "@/lib/geistdocs/i18n";
import { getRootLang } from "@/lib/geistdocs/root-params";
import { isSiteUrlConfigured, siteUrl } from "@/lib/geistdocs/site-url";
import { cn } from "@/lib/utils";
import type { Metadata } from "next";

export const metadata: Metadata = {
metadataBase: new URL("https://flags-sdk.dev"),
};
export const generateStaticParams = () =>
i18n.languages.map((lang) => ({ lang }));

export const generateStaticParams = async () => {
const langs = Object.keys(config.translations ?? {});
return langs.map((lang) => ({ lang }));
export const metadata: Metadata = {
metadataBase: isSiteUrlConfigured ? siteUrl : undefined,
};

const Layout = async ({ children, params }: LayoutProps<"/[lang]">) => {
const { lang } = await params;
const Layout = async ({ children }: LayoutProps<"/[lang]">) => {
const lang = await getRootLang();
const shouldInjectToolbar = process.env.NODE_ENV === "development";

return (
Expand Down
9 changes: 3 additions & 6 deletions apps/docs/app/[lang]/llms.mdx/[[...slug]]/route.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import { createDocsMarkdownRoute } from "@vercel/geistdocs/routes/llms";
import { geistdocsSource } from "@/lib/geistdocs/source";

const docsMarkdownRoute = createDocsMarkdownRoute({
source: geistdocsSource,
export const { GET, generateStaticParams } = createDocsMarkdownRoute({
notFound: {},
sources: [geistdocsSource],
});

export const GET = docsMarkdownRoute.GET;
export const generateStaticParams = docsMarkdownRoute.generateStaticParams;
export const revalidate = false;
14 changes: 2 additions & 12 deletions apps/docs/app/[lang]/llms.txt/route.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,6 @@
import { createLlmsRoute } from "@vercel/geistdocs/routes/llms";
import { config } from "@/lib/geistdocs/config";
import { geistdocsSource } from "@/lib/geistdocs/source";

const llmsRoute = createLlmsRoute({
source: geistdocsSource,
export const { GET } = createLlmsRoute({
sources: [geistdocsSource],
});

export const dynamicParams = false;
export const generateStaticParams = () => {
const langs = Object.keys(config.translations ?? {});
return langs.map((lang) => ({ lang }));
};

export const GET = llmsRoute.GET;
export const revalidate = false;
Loading
Loading