Files
Rich Haines 1dff4515e2 refactor(docs): migrate chat-sdk.dev to @vercel/geistdocs (#686)
## Summary

Migrates `apps/docs` from locally-copied geistdocs runtime code to the
published
[`@vercel/geistdocs`](https://www.npmjs.com/package/@vercel/geistdocs)
package (1.8.2), following the official [migration
guide](https://preview.geistdocs.com/docs/migration). Net **−8,400
lines**.

### Package-backed now

- Docs page + layouts: `createDocsPage`, `GeistdocsDocsLayout`,
`GeistdocsHomeLayout` (JSON-LD + sr-only markdown hints preserved via
`renderTop`)
- Navbar (OSS product switcher via `navbarOssProducts`), footer,
provider, search dialog, page actions (edit source, feedback, copy page,
Ask AI, open-in-chat, scroll top)
- `/api/search` → `createSearchRoute`, `/api/chat` → `createChatRoute`
(AI SDK v6; AI Gateway default, optional `GEISTDOCS_CHAT_PROXY_URL`)
- `llms.mdx` → `createDocsMarkdownRoute`, `sitemap.md` →
`createSitemapMarkdownRoute` (now includes an **Adapters** section)
- **New**: `/agents.md` via `createAgentsRoute`, backed by a new `agent`
readiness config
- `proxy.ts` → `createProxy` with explicit `markdownRoutes` for `/docs`
→ `llms.mdx` and `/adapters` → `adapters.mdx` (adds AI-agent UA
rewrites)
- CSS: `@vercel/geistdocs/styles.css` + slim local overrides (shadcn
tokens for remaining `components/ui`, body tint, prose inline code,
`#nd-*` tweaks); code blocks now use the geist Shiki theme
- Icons/logos from `@vercel/geistdocs/assets/*`; feedback via the
package action (same geistdocs.com endpoint + `siteId`)

### Kept local by design

- Curated `/llms.txt` index + `/llms-full.txt` corpus — the published
`AGENTS.md`/SKILL.md artifacts and integration tests reference this
exact contract
- The adapters section (README fetching, OG images, JSON-LD, feature
matrices, `adapters.mdx` markdown route) — now rendered inside the
package docs layout
- RSS and OG image routes (app-owned per the migration guide)
- Skipped `/.well-known/mcp.json`: no MCP servers configured, and the
proxy matcher must keep excluding `.well-known` for the served
agent-skills files

### Cleanup

- Deleted local copies: `components/geistdocs/*` chrome,
`components/ai-elements/*`, chat hooks/persistence, feedback server
actions, unused shadcn primitives, geistcn logo/icon fallbacks covered
by package assets
- Removed 13 now-unused deps (`ai@5`, `@ai-sdk/react@2`, `dexie`,
`jotai`, `cmdk`, `vaul`, `mermaid`, `nanoid`, `react-player`,
`use-stick-to-bottom`, `@orama/tokenizers`, `dexie-react-hooks`,
`next-themes`)
- Updated `docs-llms.test.ts` proxy assertions to the `createProxy`
markdown-route shape

### Behavior changes to be aware of

- Code blocks use the geist Shiki theme instead of GitHub light/dark
- Ask AI history is no longer persisted in IndexedDB (package owns the
panel)
- Adapters sidebar uses the standard geistdocs tree rendering instead of
the bespoke grouped sidebar
- Per-page markdown output appends the standard geistdocs footer links
(`/sitemap.md`, `/llms.txt`, `/agents.md`)

## Test plan

- `pnpm validate` green (knip + check + typecheck + test + build)
- Smoke-tested against `next build && next start`: `/`, `/docs`,
`/adapters`, `/agents.md`, `/llms.txt`, `/llms-full.txt`, `/sitemap.md`,
page-level `.md` URLs for both docs and adapters, `Accept:
text/markdown` negotiation, search API, JSON-LD, sr-only markdown hints,
edit-source URLs (`apps/docs/content/docs/{path}`), OSS navbar, page
actions
- Verified compiled CSS chunks contain the home grid, Shiki palette, and
geist utilities (note: stale turbopack dev caches from before this
change can serve incomplete CSS — `rm -rf apps/docs/.next` fixes it)

## Checklist

- [x] All commits are signed and verified
- [x] All commits are signed off for the DCO (`git commit -s`)
- [x] `pnpm validate` passes
- [x] Changeset added (or N/A — docs app + tests only, no package
behavior change)
- [x] Documentation updated (or N/A)

---------

Signed-off-by: molebox <rich@vercel.com>
2026-07-09 15:29:47 +02:00

120 lines
3.0 KiB
TypeScript

import type { TableOfContents } from "fumadocs-core/toc";
const BASE_URL = "https://chat-sdk.dev";
const VERCEL_ORG = {
"@type": "Organization",
name: "Vercel",
url: "https://vercel.com",
};
/** Core docs pages that emit structured data for search and answer engines. */
const JSON_LD_DOC_SLUGS = new Set(["getting-started", "streaming", "cards"]);
interface DocsPage {
data: {
description?: string;
title?: string;
toc?: TableOfContents;
type?: string;
};
slugs: string[];
url: string;
}
const getDocsPageUrl = (pageUrl: string) =>
pageUrl.startsWith("http") ? pageUrl : `${BASE_URL}${pageUrl}`;
const getDocsBreadcrumb = (title: string, pageUrl: string) => ({
"@context": "https://schema.org",
"@type": "BreadcrumbList",
itemListElement: [
{ "@type": "ListItem", position: 1, name: "Chat SDK", item: BASE_URL },
{
"@type": "ListItem",
position: 2,
name: "Documentation",
item: `${BASE_URL}/docs`,
},
{ "@type": "ListItem", position: 3, name: title, item: pageUrl },
],
});
const getStepNameFromTocEntry = (entry: {
title: TableOfContents[number]["title"];
url: string;
}): string => {
if (typeof entry.title === "string" && entry.title.length > 0) {
return entry.title;
}
const hash = entry.url.startsWith("#") ? entry.url.slice(1) : entry.url;
return hash
.split("-")
.map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1))
.join(" ");
};
const getHowToJsonLd = (
title: string,
description: string | undefined,
pageUrl: string,
toc: DocsPage["data"]["toc"]
) => {
const steps = (toc ?? [])
.filter((entry) => entry.depth === 2)
.map((entry, index) => ({
"@type": "HowToStep",
position: index + 1,
name: getStepNameFromTocEntry(entry),
url: `${pageUrl}${entry.url}`,
}));
return {
"@context": "https://schema.org",
"@type": "HowTo",
name: title,
description,
url: pageUrl,
publisher: VERCEL_ORG,
...(steps.length > 0 ? { step: steps } : {}),
};
};
const getTechArticleJsonLd = (
title: string,
description: string | undefined,
pageUrl: string
) => ({
"@context": "https://schema.org",
"@type": "TechArticle",
headline: title,
description,
url: pageUrl,
author: VERCEL_ORG,
publisher: VERCEL_ORG,
});
/**
* Build JSON-LD for selected core documentation pages.
* Guides (`type: guide`) emit `HowTo` with h2 sections as steps; hub pages use
* `TechArticle`. Always includes a `BreadcrumbList`.
*/
export const getDocsJsonLd = (page: DocsPage) => {
const slug = page.slugs.join("/");
if (!JSON_LD_DOC_SLUGS.has(slug)) {
return null;
}
const pageUrl = getDocsPageUrl(page.url);
const { description, type, toc } = page.data;
const title = page.data.title ?? "Chat SDK";
const breadcrumb = getDocsBreadcrumb(title, pageUrl);
if (type === "guide") {
return [getHowToJsonLd(title, description, pageUrl, toc), breadcrumb];
}
return [getTechArticleJsonLd(title, description, pageUrl), breadcrumb];
};