mirror of
https://github.com/vercel/chat.git
synced 2026-09-14 18:32:29 +08:00
b7e4bbfbb0
## Summary
Upgrades the chat-sdk.dev docs app from `@vercel/geistdocs` **1.20.4 →
1.22.0** (published 2026-08-21, Apache-2.0) and `next` **16.2.11 →
16.3.1**, following the bundled 1.22.0 template as the source of truth.
The target release includes all of the behavior-changing PRs for this
round:
- vercel/geistdocs#245 — require Next.js 16.3, scaffold 16.3.1, drop the
dev filesystem-cache flag (shipped in 1.21.1)
- vercel/geistdocs#246 — Cache Components across Geistdocs
- vercel/geistdocs#249 — Partial Prefetching + instant docs navigation
- vercel/geistdocs#250 — stable Next 16.3 APIs, retryable page/Ask AI
error boundaries, full prefetch of package links, no generic page shell
- vercel/geistdocs#251 — tree sidebar preserves scroll position on
folder toggles
## Adapter and configuration changes
- `next.config.ts`: `cacheComponents: true`, `partialPrefetching: true`;
removed `experimental.turbopackFileSystemCacheForDev` (default in 16.3).
Redirects, `/sitemap.xml` rewrite, and image config unchanged.
- New `lib/geistdocs/root-params.ts`; all layouts read `[lang]` via
`next/root-params` instead of `params`. Route handlers keep
route-context `params`.
- Root layout gains `generateStaticParams` returning every configured
language (`en`) — home (`/`), `/adapters`, and `/resources` now
prerender statically (previously dynamic).
- Route adapters no longer re-export `revalidate`/`dynamic` from package
factories (`agents.md`, `sitemap.md`, `llms.mdx`), and custom routes
drop their own `revalidate` exports (`llms.txt`, `llms-full.txt`,
`adapters.mdx`, `rss.xml`, `resources`).
- `llms.mdx` adopts the template form: `sources: [geistdocsSource]` +
`notFound: {}`, enabling smart agent-readable 404/410 responses with
real HTTP statuses.
- `rss.xml` migrated to the template's `"use cache"` +
`cacheLife("max")` form with `getPublicPath` base-path handling.
- `resources` page: `revalidate = 86400` → `"use cache"` +
`cacheLife("days")` (same 1-day lifetime).
- App-owned data fetching moved off `next: { revalidate }` (unsupported
under Cache Components): GitHub README fetches and homepage OSS stats
now use `"use cache"` + `cacheLife("hours")`.
- Homepage Shiki highlighting (`Demo`, `CodePreview`, `highlightCode`)
runs inside `"use cache"` scopes — Shiki reads `Date.now()` internally,
which otherwise fails prerendering.
- App-owned links to statically generated docs pages get
`prefetch={true}` (platform grid, feature matrix, adapter slug list,
"Visit Documentation") per the template's agent guidance; package-owned
sidebar/prev-next links already prefetch fully in 1.22.0.
- `Analytics`/`SpeedInsights` moved into
`components/geistdocs/provider.tsx` per the template.
- CSS: `styles/geistdocs.css` now imports `@vercel/geistdocs/theme.css`
(self-sourcing package dist/streamdown) instead of layering on
`styles.css` from `global.css`; updated the mobile breadcrumb selector
for the new package DOM; kept the site-specific shadcn tokens, dark
background-scale override, prose, TOC, and streamdown fixes.
- Added `apps/docs/AGENTS.md` capturing the packaged-architecture
conventions (cache-components rules, root-params, markdown contract,
proxy mappings).
## PR #251 (tree sidebar scroll) verification
The fix is package-internal (`manualToggleRef` in `SidebarTree`); no
consumer change is needed. This site's sidebars render no collapsible
folder rows (content uses spread folders, `...api` etc.), so I verified
the shipped behavior against the bundled 1.22.0 template with
`sidebarMode="tree"` enabled locally: expanding/collapsing a folder
preserves the exact sidebar scroll position (772 → 772), and a route
change into a collapsed folder still scrolls the active item into view.
8/8 checks pass.
## Static-generation coverage
Production build: 290/290 static pages generated. Every intended
parameter tuple is prerendered with complete content (verified H1/body
in emitted HTML):
- `/en` home, `/en/adapters` listing, `/en/resources` — now fully static
(were `ƒ` on main)
- `/en/docs/*` — 45 pages, complete static HTML + one generic
`[[...slug]]` fallback entry (allowed)
- `/en/adapters/{official,community,vendor-official}/*` — 44 detail
pages + `/en/adapters.mdx/*` markdown for all 44
- `/en/sitemap.md` — SSG
Intentional contract differences (match the 1.22.0 template's own build
output):
- OG image routes (`/og/[...slug]`, adapter `*/og`) render on demand
under Cache Components instead of build-time SSG; Next 16.3 caches the
rendered image per route. URLs and content types verified unchanged.
- `llms.txt`, `llms-full.txt`, `llms.mdx`, `rss.xml`, `agents.md` remain
on-demand route handlers (same as main); `agents.md` reads the request
origin by package design.
- Unknown HTML routes: browsers receive the docs shell with 200 before
not-found UI resolves; crawlers get a real 404. Machine-readable unknown
routes return the new smart 404 body with real 404 status and
`X-Robots-Tag: noindex`.
## Lockfile
`pnpm-lock.yaml` delta: the `docs` importer's `@vercel/geistdocs`
(1.20.4 → 1.22.0) and `next` (16.2.11 → 16.3.1) bumps, their
peer-context re-resolutions, and one mechanical re-keying of `@swc/core`
peer contexts to include `@swc/helpers` across existing entries (no
version changes outside the docs app). Verified with `pnpm install
--frozen-lockfile`.
## Test results
- `pnpm install --frozen-lockfile` ✓
- `pnpm check` ✓ (1 pre-existing warning in untouched
`lib/read-more.ts`)
- `pnpm typecheck` — 43/43 ✓
- `pnpm knip` ✓, `pnpm konsistent` ✓
- `pnpm test` — 47/47 turbo tasks ✓
- Clean production build (removed `.next`/`.source`) ✓ 290/290
- Production-server (`next start`) contract checks: HTML docs,
`.md`/`.mdx`, `Accept: text/markdown`, agent-UA negotiation, `llms.txt`,
`llms-full.txt`, `sitemap.md`, `agents.md`, `/.well-known/mcp.json`
(intentional 404), `rss.xml`, `robots.txt`, `/sitemap.xml` rewrite, OG
images, `AGENTS.md`, all redirects (308s), `/api/search` JSON and not
rewritten as markdown, `/docs.md` section root ✓
- Browser checks (Playwright, Chrome, `next start`) — 16/16: instant
sidebar + prev/next client navigation with complete content and no
loading shell, single visible H1 (Activity-preserved routes stay
hidden), Copy Page, page actions menu, search → result navigation, Ask
AI panel with suggestions, theme switch (dark applies the site's
background scale), mobile navbar menu and docs sheet navigation, unknown
page shows not-found UI, zero console/page errors and failed requests
- Ask AI scoped failure: with no local AI Gateway credentials the chat
surfaces the error, the surrounding page stays intact, and resubmission
retries cleanly
- Visual parity screenshots vs production (home/docs/adapters, light +
dark) match
## Preview
- Preview:
https://chat-git-richardhaines-geistdocs-122-upgrade.vercel.sh —
deployment **Ready**, build passed on Vercel.
- The preview sits behind Vercel SSO (Fork Protection), so automated
route checks aren't possible without a bypass token; please spot-check
through SSO: `/docs/getting-started`, `/docs/getting-started.md`,
`/adapters`, `/llms.txt`, and a client navigation between docs pages.
Signed-off-by: molebox <rich@vercel.com>
125 lines
3.8 KiB
TypeScript
125 lines
3.8 KiB
TypeScript
import { readFile } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
import { cacheLife } from "next/cache";
|
|
import adaptersJson from "@/adapters.json";
|
|
|
|
const LOCAL_PACKAGE_PATTERN = /github\.com\/vercel\/chat\/tree\/[^/]+\/(.+)/;
|
|
const GITHUB_SUBPATH_PATTERN =
|
|
/github\.com\/([^/]+)\/([^/]+)\/tree\/([^/]+)\/(.+)/;
|
|
const GITHUB_REPO_REF_PATTERN =
|
|
/^https:\/\/github\.com\/([^/]+)\/([^/]+)\/tree\/([^/]+)\/?$/;
|
|
const GITHUB_REPO_PATTERN = /github\.com\/([^/]+)\/([^/]+)/;
|
|
const GITHUB_REPO_ROOT_PATTERN = /^(https:\/\/github\.com\/[^/]+\/[^/]+)/;
|
|
const UNPINNED_REF_PATTERN = /^(main|master|head|dev|develop|trunk|default)$/i;
|
|
|
|
const MAX_README_BYTES = 500_000;
|
|
|
|
export type Adapter = (typeof adaptersJson)[number];
|
|
|
|
export const getAdapter = (slug: string): Adapter | undefined =>
|
|
adaptersJson.find((a) => a.slug === slug);
|
|
|
|
export const getAuthor = (adapter: Adapter): string | undefined =>
|
|
"author" in adapter ? adapter.author : undefined;
|
|
|
|
export const getIssuesUrl = (
|
|
readmeUrl: string | undefined
|
|
): string | undefined => {
|
|
if (!readmeUrl) {
|
|
return;
|
|
}
|
|
const match = readmeUrl.match(GITHUB_REPO_ROOT_PATTERN);
|
|
return match ? `${match[1]}/issues` : undefined;
|
|
};
|
|
|
|
const warnUnpinned = (adapter: Adapter, ref: string | undefined) => {
|
|
if (ref && !UNPINNED_REF_PATTERN.test(ref)) {
|
|
return;
|
|
}
|
|
console.warn(
|
|
`[adapters] Community adapter "${adapter.name}" uses an unpinned README ref "${
|
|
ref ?? "<default branch>"
|
|
}". Pin to a commit SHA or tag in adapters.json to freeze content at review time.`
|
|
);
|
|
};
|
|
|
|
const truncate = (content: string): string =>
|
|
content.length <= MAX_README_BYTES
|
|
? content
|
|
: `${content.slice(0, MAX_README_BYTES)}\n\n> _README truncated — view the full version on GitHub._`;
|
|
|
|
const fetchGitHubReadme = async (url: string): Promise<string | undefined> => {
|
|
"use cache";
|
|
cacheLife("hours");
|
|
|
|
const response = await fetch(url, {
|
|
headers: { Accept: "application/vnd.github.raw+json" },
|
|
});
|
|
if (response.ok) {
|
|
return response.text();
|
|
}
|
|
};
|
|
|
|
interface GetReadmeOptions {
|
|
/** Emit a build-time warning when the README ref is not pinned to a SHA/tag. */
|
|
warnOnUnpinnedRef?: boolean;
|
|
}
|
|
|
|
export const getReadme = async (
|
|
adapter: Adapter,
|
|
options: GetReadmeOptions = {}
|
|
): Promise<string | undefined> => {
|
|
if (!adapter.readme) {
|
|
return;
|
|
}
|
|
const repoUrl = adapter.readme;
|
|
const warn = options.warnOnUnpinnedRef ?? false;
|
|
|
|
const localMatch = repoUrl.match(LOCAL_PACKAGE_PATTERN);
|
|
if (localMatch) {
|
|
const [, pkgPath] = localMatch;
|
|
const filePath = join(process.cwd(), "..", "..", pkgPath, "README.md");
|
|
try {
|
|
return truncate(await readFile(filePath, "utf-8"));
|
|
} catch {
|
|
return;
|
|
}
|
|
}
|
|
|
|
const subpathMatch = repoUrl.match(GITHUB_SUBPATH_PATTERN);
|
|
if (subpathMatch) {
|
|
const [, owner, repo, ref, path] = subpathMatch;
|
|
if (warn) {
|
|
warnUnpinned(adapter, ref);
|
|
}
|
|
const content = await fetchGitHubReadme(
|
|
`https://api.github.com/repos/${owner}/${repo}/readme/${path}?ref=${ref}`
|
|
);
|
|
return content ? truncate(content) : undefined;
|
|
}
|
|
|
|
const repoRefMatch = repoUrl.match(GITHUB_REPO_REF_PATTERN);
|
|
if (repoRefMatch) {
|
|
const [, owner, repo, ref] = repoRefMatch;
|
|
if (warn) {
|
|
warnUnpinned(adapter, ref);
|
|
}
|
|
const content = await fetchGitHubReadme(
|
|
`https://api.github.com/repos/${owner}/${repo}/readme?ref=${ref}`
|
|
);
|
|
return content ? truncate(content) : undefined;
|
|
}
|
|
|
|
const repoMatch = repoUrl.match(GITHUB_REPO_PATTERN);
|
|
if (repoMatch) {
|
|
const [, owner, repo] = repoMatch;
|
|
if (warn) {
|
|
warnUnpinned(adapter, undefined);
|
|
}
|
|
const content = await fetchGitHubReadme(
|
|
`https://api.github.com/repos/${owner}/${repo}/readme`
|
|
);
|
|
return content ? truncate(content) : undefined;
|
|
}
|
|
};
|