chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:58:18 +08:00
commit 6d5d58c1a9
18293 changed files with 3502153 additions and 0 deletions
+56
View File
@@ -0,0 +1,56 @@
# shell-docs environment variables.
#
# All values below are read at REQUEST time (not at build time) — a
# single built artifact can serve staging and prod by pointing them at
# different hosts. See src/lib/runtime-config.ts for the authoritative
# resolution rules; the summary below is a quick reference.
#
# URL-FATAL semantics: missing in production logs a `FATAL-CONFIG:`
# error (Sentry-alerted) and falls back to a sentinel URL (visible
# breakage) or — for fields with a working prod default — a non-fatal
# warn and the default. Analytics-KEY semantics: missing is the empty
# string with NO log (consumers no-op on empty).
#
# Severity by field:
# FATAL-CONFIG on miss: NEXT_PUBLIC_BASE_URL, NEXT_PUBLIC_SHELL_URL
# Non-fatal warn on miss (working prod default):
# NEXT_PUBLIC_INTELLIGENCE_SIGNUP_URL,
# NEXT_PUBLIC_POSTHOG_HOST
# Silent ("" empty default, no log):
# NEXT_PUBLIC_POSTHOG_KEY,
# NEXT_PUBLIC_SCARF_PIXEL_ID,
# NEXT_PUBLIC_GOOGLE_ANALYTICS_TRACKING_ID,
# NEXT_PUBLIC_REB2B_KEY,
# NEXT_PUBLIC_REO_KEY
# Canonical base URL used by sitemap.ts, robots.ts, and the per-page
# canonical metadata. In production this points at the docs host so
# crawlers index every framework variant at its self-canonical URL.
# FATAL on miss in prod, falls back to the canonical prod host
# (https://docs.copilotkit.ai).
NEXT_PUBLIC_BASE_URL=https://docs.copilotkit.ai
# URL of the showcase shell host, which owns /integrations and /matrix.
# Used by the top-nav cross-host links and InlineDemo. Replace with your
# deployment's shell host. FATAL on miss in prod, falls back to the
# sentinel `about:blank#shell-url-missing` (visible breakage).
NEXT_PUBLIC_SHELL_URL=https://www.copilotkit.ai
# Intelligence platform signup URL — drives the signup-link CTA and the
# ops-platform-cta. Non-fatal warn on miss in prod; falls back to
# https://dashboard.operations.copilotkit.ai/ which is a real working
# host, so absence is recoverable.
# NEXT_PUBLIC_INTELLIGENCE_SIGNUP_URL=https://dashboard.operations.copilotkit.ai/
# PostHog host — analytics destination for `posthog-js`. Non-fatal warn
# on miss in prod; falls back to https://eu.i.posthog.com (the EU cloud
# default that matches prior middleware behavior).
# NEXT_PUBLIC_POSTHOG_HOST=https://eu.i.posthog.com
# Analytics keys — empty string disables that analytics channel.
# Legitimately absent in non-production envs; NO log on miss.
# NEXT_PUBLIC_POSTHOG_KEY=
# NEXT_PUBLIC_SCARF_PIXEL_ID=
# NEXT_PUBLIC_GOOGLE_ANALYTICS_TRACKING_ID=
# NEXT_PUBLIC_REB2B_KEY=
# NEXT_PUBLIC_REO_KEY=
+6
View File
@@ -0,0 +1,6 @@
.next/
tsconfig.tsbuildinfo
node_modules/
.env*.local
*.log
.vercel
+48
View File
@@ -0,0 +1,48 @@
FROM node:20-slim AS builder
ARG COMMIT_SHA=unknown
ARG BRANCH=unknown
WORKDIR /app
# Copy only what shell-docs + scripts need
COPY showcase/scripts/package.json ./scripts/package.json
COPY showcase/scripts/package-lock.json ./scripts/package-lock.json
COPY showcase/shell-docs/package.json ./shell-docs/package.json
COPY showcase/shell-docs/package-lock.json ./shell-docs/package-lock.json
# Install deps for both (standalone, no workspace)
RUN cd scripts && npm ci --silent && cd ../shell-docs && npm ci --silent
# Copy source
COPY showcase/shared/ ./shared/
COPY showcase/integrations/ ./integrations/
COPY showcase/scripts/ ./scripts/
COPY showcase/shell-docs/ ./shell-docs/
# Bake commit info into Next.js build
ENV NEXT_PUBLIC_COMMIT_SHA=${COMMIT_SHA}
ENV NEXT_PUBLIC_BRANCH=${BRANCH}
# Generate registry + content, then build Next.js
RUN cd scripts && node node_modules/tsx/dist/cli.mjs generate-registry.ts \
&& node node_modules/tsx/dist/cli.mjs bundle-demo-content.ts \
&& node node_modules/tsx/dist/cli.mjs bundle-setup-content.ts \
&& node node_modules/tsx/dist/cli.mjs generate-search-index.ts \
&& cd ../shell-docs && npx next build
FROM node:20-slim AS runner
WORKDIR /app
ARG COMMIT_SHA=unknown
ARG BRANCH=unknown
ENV NODE_ENV=production
ENV PORT=10000
ENV NEXT_PUBLIC_COMMIT_SHA=${COMMIT_SHA}
ENV NEXT_PUBLIC_BRANCH=${BRANCH}
COPY --from=builder /app/shell-docs/.next ./.next
COPY --from=builder /app/shell-docs/node_modules ./node_modules
COPY --from=builder /app/shell-docs/package.json ./
COPY --from=builder /app/shell-docs/public ./public
COPY --from=builder /app/shell-docs/src/content ./src/content
EXPOSE 10000
CMD ["npx", "next", "start", "-p", "10000"]
+116
View File
@@ -0,0 +1,116 @@
# Shell Docs
`showcase/shell-docs` is the Next.js app that builds and serves
`docs.copilotkit.ai`. Author CopilotKit product documentation here, not in the retired
top-level `docs/` app.
## Run Locally
Shell-docs is a standalone npm-based app. You do not need a root install just to run the
docs app locally.
```bash
cd showcase/scripts
npm install
cd ../shell-docs
npm install
npm run dev
```
The local dev server runs on port `3003`.
```text
http://localhost:3003
```
The shell-docs npm lifecycle generates registry, demo-content, setup-content, and search
data before `dev`, `build`, and `typecheck`.
## Validate Changes
Run these from `showcase/shell-docs`:
```bash
npm run build
npm run typecheck
npm run test
```
For repo-level CI parity, prefer Nx when a shell-docs target is available in the current
checkout and root dependencies are installed. For normal shell-docs local development, the
npm commands above are the canonical path.
## Authoring Recipes
### Showcase-Driven Framework Docs
Showcase-driven frameworks use `docs_mode: generated`. The docs are assembled from showcase
registry/generated data, demos, source regions, shared/root MDX, snippets, and sparse
framework overrides.
To update showcase-driven docs:
1. Edit the showcase source of truth: manifests, demos, feature coverage, source regions, or
registry inputs.
2. Edit shared/root MDX only when the change applies across generated frameworks.
3. Add sparse framework overrides only for real framework-specific differences.
4. Do not hand-edit generated files under `src/data/frameworks/`.
5. Validate routes, sidebar state, search results, snippets, and framework switching.
### Authored Framework Docs
Authored frameworks use `docs_mode: authored`. The framework owns an MDX tree under
`src/content/docs/integrations/<docsFolder>/` with a `meta.json` sidebar.
To update authored docs:
1. Check `getDocsFolder()` in `src/lib/registry.ts`; the URL slug and folder name may differ.
2. Edit the MDX page under `src/content/docs/integrations/<docsFolder>/`.
3. Update that folder's `meta.json` when adding, removing, or moving pages.
4. Reuse shared snippets from `src/content/snippets/` when content should stay consistent
across frameworks.
5. Validate the framework route, sidebar, search result, and any shared snippet render.
### Reference Docs
Edit API reference pages under `src/content/reference/`.
The v2 reference does not use `meta.json`; navigation is generated by walking the tree and
reading each page's `title` and `description` frontmatter. Only the legacy `reference/v1/`
tree uses `meta.json`.
### Snippets
Reusable snippets live under `src/content/snippets/`. Snippets may be rendered by root docs,
authored framework pages, and showcase-driven framework pages, so keep them general unless
the path is intentionally framework-specific.
### Frontend Applicability
Frontend routes use page-level applicability metadata, independent from where the content is
authored. A page can be authored MDX, showcase-generated content, mirrored protocol docs, or
reference content and still be universal across frontends.
Use the `frontend` field in page frontmatter or `meta.json` when a root doc should appear in
non-React frontend docs:
- `universal` — render the same page under `/<frontend>/...`.
- `frontend-variant` — render only when a matching page exists under
`src/content/docs/frontends/<frontend>/...`.
- `hide` — omit the page from frontend-scoped docs.
Do not use "showcase-driven" as a proxy for frontend availability. Showcase derivation is an
authoring/source detail; frontend applicability controls routing and sidebar inclusion.
### AG-UI Mirrored Docs
AG-UI protocol docs are authored upstream in `ag-ui-protocol/ag-ui`. The
`src/content/ag-ui/` tree is a downstream mirror rendered on the CopilotKit docs host.
Change AG-UI docs upstream first, then sync the mirror back into shell-docs.
## Top-Level Docs Symlink
The repository's top-level `docs/` path is a symlink to `showcase/shell-docs/` for
contributor muscle memory. It is not a separate docs app. Do not recreate the old
`docs/content/docs/` tree; author CopilotKit docs in `showcase/shell-docs/src/content/`.
+57
View File
@@ -0,0 +1,57 @@
{
"_comment": "Maintained list of valid AI model names for docs. Update when providers release new models. CI validates docs against this list.",
"openai": [
"gpt-5.5",
"gpt-5.5-mini",
"gpt-5.5-nano",
"gpt-5.4",
"gpt-5.4-mini",
"gpt-5.4-nano",
"gpt-5",
"gpt-5-mini",
"gpt-4.1",
"gpt-4.1-mini",
"gpt-4.1-nano",
"gpt-4o",
"gpt-4o-mini",
"gpt-4-turbo",
"gpt-4",
"gpt-3.5-turbo",
"o4-mini",
"o3-mini",
"o1",
"o1-mini",
"o1-preview"
],
"anthropic": [
"claude-opus-4-6",
"claude-sonnet-4-6",
"claude-haiku-4-5",
"claude-opus-4-5",
"claude-sonnet-4-5",
"claude-opus-4-1",
"claude-opus-4",
"claude-sonnet-4",
"claude-sonnet-4-0",
"claude-opus-4-0",
"claude-3-7-sonnet",
"claude-3-5-haiku",
"claude-3-5-sonnet",
"claude-code"
],
"google": [
"gemini-2.5-pro",
"gemini-2.5-flash",
"gemini-2.5-flash-lite",
"gemini-2.0-flash",
"gemini-1.5-pro",
"gemini-1.5-flash"
],
"cohere": ["command-r-plus", "command-r", "command-a"],
"meta": [
"llama-4-scout",
"llama-4-maverick",
"llama-3.3-70b",
"llama-3.1-405b"
]
}
+554
View File
@@ -0,0 +1,554 @@
import type { NextConfig } from "next";
// NEXT_PUBLIC_BASE_URL and NEXT_PUBLIC_SHELL_URL are read at REQUEST
// time by the server `getRuntimeConfig()` reader and injected into the
// client via `window.__SHOWCASE_CONFIG__` from the root layout. They
// are NOT build-time inputs — a single built artifact can serve staging
// and prod by changing the Railway env vars. Any previous build-time
// validation that threw on unset env vars would prevent that exact
// deploy pattern. Missing values are surfaced loudly at runtime via
// `console.error` from `runtime-config.ts` instead.
const nextConfig: NextConfig = {
images: {
// Bypass the Next.js image optimizer (`/_next/image`). The optimizer
// requires `sharp` at runtime, which is missing from the Railway image
// and breaks all `<Image>` rendering site-wide. Our CDN
// (`cdn.copilotkit.ai`, CloudFront/S3) ignores `?fm=webp` and serves
// PNG regardless, so the optimizer added no format-conversion value
// for CDN-hosted images. With `unoptimized`, `<Image>` renders as a
// plain `<img>` pointing at the source URL — visually identical for
// users, no sharp dependency required.
unoptimized: true,
// Asset CDN for framework intro-page media (banner videos, architecture
// diagrams, supported-feature thumbnails, framework icons). Hosts every
// image/video referenced by `src/data/frameworks/*.ts` and any future
// marketing surface that pulls from the shared CDN. Kept here for
// documentation and to remain valid if the optimizer is re-enabled.
remotePatterns: [
{
protocol: "https",
hostname: "cdn.copilotkit.ai",
},
],
},
async rewrites() {
return {
beforeFiles: [
// PostHog reverse proxy — routes analytics through this host so
// requests bypass ad blockers / tracking-protection that target
// the *.i.posthog.com hostname directly. Mirrors docs/.
{
source: "/ingest/static/:path*",
destination: "https://eu-assets.i.posthog.com/static/:path*",
},
{
source: "/ingest/:path*",
destination: "https://eu.i.posthog.com/:path*",
},
// Fumadocs LLM page-actions feature: every docs page is also
// reachable as `<path>.mdx` so LLMCopyButton/ViewOptionsPopover
// (and external crawlers) can fetch the raw MDX source. The
// route handler at `app/llms-mdx/[[...slug]]/route.ts` reuses
// `loadDoc()` to resolve the same content tree the page uses.
{
source: "/:path*.mdx",
destination: "/llms-mdx/:path*",
},
{
source: "/:path*.md",
destination: "/llms-mdx/:path*",
},
],
afterFiles: [],
fallback: [],
};
},
async redirects() {
return [
{
// Built-in agent is the default framework, so its overview page
// is the docs root. Avoid surfacing a redundant "Introduction"
// entry inside the built-in-agent sidebar by canonicalizing the
// bare /built-in-agent URL to the root overview.
source: "/built-in-agent",
destination: "/",
permanent: true,
},
// The BIA index.mdx is reachable through the content fallback as
// `/index`, which would duplicate the home page under a second
// URL. Canonicalize it to the root.
{
source: "/index",
destination: "/",
permanent: true,
},
{
source: "/frontend-actions",
destination: "/frontend-tools",
permanent: true,
},
{
source: "/troubleshooting/migrate-to-v2",
destination: "/migrate/v2",
permanent: true,
},
{
source: "/troubleshooting/migrate-to-1.10.X",
destination: "/migrate/1.10.X",
permanent: true,
},
{
source: "/troubleshooting/migrate-to-1.8.2",
destination: "/migrate/1.8.2",
permanent: true,
},
{
source: "/concepts/oss-vs-cloud",
destination: "/concepts/oss-vs-enterprise",
permanent: true,
},
// /unselected/* tree retired. Files moved to integrations/built-in-agent/
// (BIA replaced the old "unselected" slot as the default integration).
// The Built-in Agent docs are served at the ROOT surface (no
// framework prefix), so per-path entries below map directly onto
// root URLs (direct moves + slug renames from SUBPATH_RENAMES in
// seo-redirects.ts); the catch-all at the bottom routes everything
// else to the root to preserve SEO equity, since these legacy URLs
// historically served BIA content.
{
source: "/unselected",
destination: "/",
permanent: true,
},
{
source: "/unselected/quickstart",
destination: "/quickstart",
permanent: true,
},
{
source: "/unselected/advanced-configuration",
destination: "/advanced-configuration",
permanent: true,
},
{
source: "/unselected/mcp-servers",
destination: "/mcp-servers",
permanent: true,
},
{
source: "/unselected/model-selection",
destination: "/model-selection",
permanent: true,
},
{
source: "/unselected/server-tools",
destination: "/server-tools",
permanent: true,
},
{
source: "/unselected/shared-state",
destination: "/shared-state",
permanent: true,
},
{
source: "/unselected/generative-ui/mcp-apps",
destination: "/generative-ui/mcp-apps",
permanent: true,
},
// Cat C promotions whose canonical home moved off the unselected
// tail (e.g. `unselected/ag-ui` → `backend/ag-ui`).
{
source: "/unselected/ag-ui",
destination: "/backend/ag-ui",
permanent: true,
},
{
source: "/unselected/copilot-runtime",
destination: "/backend/copilot-runtime",
permanent: true,
},
// custom-agent: consolidate two divergent shell-docs copies onto the
// structurally-complete backend/custom-agent.mdx (508 lines, matches
// upstream snippet). The integrations/built-in-agent/custom-agent.mdx
// copy (240 lines, missing 5 sections) was retired; redirect all
// historical paths, including the bare root URL the BIA sidebar's
// Backend section links to.
{
source: "/built-in-agent/custom-agent",
destination: "/backend/custom-agent",
permanent: true,
},
{
source: "/integrations/built-in-agent/custom-agent",
destination: "/backend/custom-agent",
permanent: true,
},
{
source: "/custom-agent",
destination: "/backend/custom-agent",
permanent: true,
},
// ----------------------------------------------------------------
// Built-in Agent served at the root: /built-in-agent/<page> moved
// to /<page>. Specific entries first (they must win over the
// catch-all), then the catch-all that strips the prefix.
// ----------------------------------------------------------------
// BIA's AG-UI backend page lives at /backend/ag-ui at the root —
// the bare /ag-ui segment is owned by the AG-UI protocol docs
// (src/app/ag-ui/), so the page can't keep its old slug.
{
source: "/built-in-agent/ag-ui",
destination: "/backend/ag-ui",
permanent: true,
},
// Tutorials are retired (see /tutorials/:path* below). Preserve the
// old middleware behavior of sending framework-scoped tutorial URLs
// to the quickstart rather than bouncing them through /tutorials → /.
{
source: "/built-in-agent/tutorials/:path*",
destination: "/quickstart",
permanent: true,
},
{
source: "/built-in-agent/:path*",
destination: "/:path*",
permanent: true,
},
// troubleshooting/migrate-to-* in unselected → existing
// /migrate/* canonical (already redirected at the
// /troubleshooting/migrate-to-* level).
{
source: "/unselected/troubleshooting/migrate-to-v2",
destination: "/migrate/v2",
permanent: true,
},
{
source: "/unselected/troubleshooting/migrate-to-1.10.X",
destination: "/migrate/1.10.X",
permanent: true,
},
{
source: "/unselected/troubleshooting/migrate-to-1.8.2",
destination: "/migrate/1.8.2",
permanent: true,
},
// Tutorials and interrupt-based moved out of unselected/ to root.
{
source: "/unselected/tutorials/:path*",
destination: "/tutorials/:path*",
permanent: true,
},
// Interrupt-based was a LangGraph-specific page parked in
// unselected/. Real homes are
// `/<langgraph-slug>/human-in-the-loop/interrupt-flow` (and the
// Mastra equivalent). Send anyone landing on the legacy URL to
// the framework-agnostic HITL page; soft-default routes them
// through to the right framework's interrupt flow if they're
// stored as LangGraph or Mastra.
{
source: "/unselected/generative-ui/your-components/interrupt-based",
destination: "/human-in-the-loop",
permanent: true,
},
// agent-app-context now has a root home: the BIA-authored page is
// served at the bare URL.
{
source: "/unselected/agent-app-context",
destination: "/agent-app-context",
permanent: true,
},
// Slug-rename entries (mirror SUBPATH_RENAMES in seo-redirects.ts).
// These MUST come before the catch-all so the rename wins. Each
// historical slug under /unselected/ has been renamed at the root
// BIA surface; e.g. agentic-chat-ui → prebuilt-components.
{
source: "/unselected/agentic-chat-ui",
destination: "/prebuilt-components",
permanent: true,
},
{
source: "/unselected/use-agent-hook",
destination: "/programmatic-control",
permanent: true,
},
{
source: "/unselected/frontend-actions",
destination: "/frontend-tools",
permanent: true,
},
// No coding-agents page exists any more; match the root-level
// R19 (/vibe-coding-mcp) and R18 (/mcp) rules in seo-redirects.ts.
{
source: "/unselected/vibe-coding-mcp",
destination: "/build-with-agents",
permanent: true,
},
{
source: "/unselected/generative-ui/agentic",
destination: "/generative-ui/your-components/display-only",
permanent: true,
},
{
source: "/unselected/generative-ui/backend-tools",
destination: "/generative-ui/tool-rendering",
permanent: true,
},
{
source: "/unselected/generative-ui/frontend-tools",
destination: "/frontend-tools",
permanent: true,
},
{
source: "/unselected/generative-ui/render-only",
destination: "/generative-ui/your-components/display-only",
permanent: true,
},
{
source: "/unselected/generative-ui/tool-based",
destination: "/generative-ui/tool-rendering",
permanent: true,
},
{
source: "/unselected/custom-look-and-feel/bring-your-own-components",
destination: "/custom-look-and-feel/slots",
permanent: true,
},
{
source:
"/unselected/custom-look-and-feel/customize-built-in-ui-components",
destination: "/custom-look-and-feel/slots",
permanent: true,
},
{
source: "/unselected/custom-look-and-feel/markdown-rendering",
destination: "/custom-look-and-feel/slots",
permanent: true,
},
// The /guides tree no longer exists anywhere (the old destination
// 404'd through the BIA route); send readers home instead.
{
source: "/unselected/guide",
destination: "/",
permanent: true,
},
{
source: "/unselected/mcp",
destination: "/build-with-agents",
permanent: true,
},
// Catch-all: route remaining /unselected/* paths to the root.
// BIA is the canonical owner of the legacy unselected/ content
// tree, and BIA is served at the root; matches P1×unselected in
// seo-redirects.ts.
{
source: "/unselected/:path*",
destination: "/:path*",
permanent: true,
},
// /learn/* tree retired. The seven explanation-tier pages were
// promoted into the Concepts subgroup, the multi-conversation
// tutorial moved to /tutorials/, the open-json-ui page moved to
// /generative-ui/, and the What's New tree became its own
// top-level section. Redirects below funnel old URLs to the
// canonical homes.
{
source: "/learn",
destination: "/concepts/architecture",
permanent: true,
},
{
source: "/learn/architecture",
destination: "/concepts/architecture",
permanent: true,
},
{
source: "/learn/threads",
destination: "/premium/threads-explained",
permanent: true,
},
{
source: "/learn/intelligence-platform",
destination: "/premium/intelligence-platform",
permanent: true,
},
{
source: "/learn/agentic-protocols",
destination: "/agentic-protocols",
permanent: true,
},
{
source: "/learn/ag-ui-protocol",
destination: "/agentic-protocols/ag-ui",
permanent: true,
},
{
source: "/learn/a2a-protocol",
destination: "/agentic-protocols/a2a",
permanent: true,
},
{
source: "/learn/connect-mcp-servers",
destination: "/agentic-protocols/mcp",
permanent: true,
},
{
source: "/learn/generative-ui",
destination: "/concepts/generative-ui-overview",
permanent: true,
},
{
source: "/learn/generative-ui/specs/open-json-ui",
destination: "/generative-ui/open-json-ui",
permanent: true,
},
{
source: "/learn/generative-ui/specs/a2ui",
destination: "/generative-ui/a2ui",
permanent: true,
},
{
source: "/learn/generative-ui/specs/mcp-apps",
destination: "/generative-ui/mcp-apps",
permanent: true,
},
{
source: "/learn/generative-ui/specs",
destination: "/concepts/generative-ui-overview",
permanent: true,
},
{
source: "/learn/tutorials/multi-conversation-chat",
destination: "/",
permanent: true,
},
{
source: "/learn/whats-new/:path*",
destination: "/whats-new/:path*",
permanent: true,
},
{
source: "/learn/whats-new",
destination: "/whats-new",
permanent: true,
},
// Concepts subgroup tightened: protocol pages moved into a new
// /agentic-protocols/ section under Get Started, the
// Intelligence Platform + Threads explanation pages moved to
// Enterprise (/premium/), and three-types-of-gen-ui merged into
// /concepts/generative-ui-overview. Per-path redirects below
// catch URLs that were live in the brief window between the
// first /learn/ consolidation pass and this restructure.
{
source: "/concepts/agentic-protocols",
destination: "/agentic-protocols",
permanent: true,
},
{
source: "/concepts/ag-ui-protocol",
destination: "/agentic-protocols/ag-ui",
permanent: true,
},
{
source: "/concepts/mcp-servers",
destination: "/agentic-protocols/mcp",
permanent: true,
},
{
source: "/concepts/a2a-protocol",
destination: "/agentic-protocols/a2a",
permanent: true,
},
{
source: "/concepts/intelligence-platform",
destination: "/premium/intelligence-platform",
permanent: true,
},
{
source: "/concepts/threads",
destination: "/premium/threads-explained",
permanent: true,
},
{
source: "/concepts/three-types-of-gen-ui",
destination: "/concepts/generative-ui-overview",
permanent: true,
},
// Stale pages hidden pre-launch. 302 (not permanent) — these
// URLs may be restored once the underlying content is rewritten.
// Tutorials are broken end-to-end and pulled from nav; files
// remain on disk under content/docs/tutorials/ for post-launch
// rewrite.
{
source: "/tutorials/:path*",
destination: "/",
permanent: false,
},
// Old-name straggler from the coding-agents rename; the page
// moved to /coding-agents.
{
source: "/coding-agent-setup",
destination: "/coding-agents",
permanent: false,
},
// Old guide now belongs in the v2 hook reference.
{
source: "/copilot-suggestions",
destination: "/reference/v2/hooks/useSuggestions",
permanent: true,
},
// AI-slop placeholder pulled from nav until properly authored;
// file stays on disk for rewrite.
{
source: "/generative-ui/open-json-ui",
destination: "/generative-ui",
permanent: false,
},
// ag-ui-middleware moved into the agentic-protocols group so it
// appears in the sidebar under AG-UI rather than as an orphan
// root page. 302 (not 301) since the new home is recent and we
// want flexibility to revisit placement without burning the
// permanent-redirect cache.
{
source: "/ag-ui-middleware",
destination: "/agentic-protocols/ag-ui-middleware",
permanent: false,
},
// No bare redirects for `/generative-ui/your-components/*`: the
// Built-in Agent docs are served at the root, and BIA authors real
// pages at those paths (display-only, interactive). Framework-scoped
// variants (`/:framework/generative-ui/your-components/*`) also
// render directly.
// "Bots SDK" renamed to "Channels SDK" (OSS-438): docs moved from /bots to
// /channels and /reference/bot to /reference/channels. Preserve links + SEO.
{
source: "/bots",
destination: "/channels",
permanent: true,
},
{
source: "/bots/:path*",
destination: "/channels/:path*",
permanent: true,
},
{
source: "/reference/bot",
destination: "/reference/channels",
permanent: true,
},
{
source: "/reference/bot/:path*",
destination: "/reference/channels/:path*",
permanent: true,
},
];
},
};
export default nextConfig;
File diff suppressed because it is too large Load Diff
+51
View File
@@ -0,0 +1,51 @@
{
"name": "@copilotkit/showcase-shell-docs",
"version": "0.1.0",
"private": true,
"scripts": {
"predev": "tsx ../scripts/generate-registry.ts && tsx ../scripts/bundle-demo-content.ts && tsx ../scripts/bundle-setup-content.ts && tsx ../scripts/generate-search-index.ts",
"dev": "next dev --port 3003",
"build": "tsx ../scripts/generate-registry.ts && tsx ../scripts/bundle-demo-content.ts && tsx ../scripts/bundle-setup-content.ts && tsx ../scripts/generate-search-index.ts && next build",
"start": "next start -p 3003",
"lint": "oxlint .",
"test": "vitest run",
"test:watch": "vitest",
"pretypecheck": "tsx ../scripts/generate-registry.ts && tsx ../scripts/bundle-demo-content.ts && tsx ../scripts/bundle-setup-content.ts && tsx ../scripts/generate-search-index.ts",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@radix-ui/react-popover": "^1.1.15",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"fumadocs-core": "^16.8.12",
"fumadocs-ui": "^16.8.12",
"gray-matter": "^4.0.3",
"lucide-react": "^0.469.0",
"next": "^16.0.0",
"next-mdx-remote": "^6.0.0",
"next-themes": "^0.4.6",
"posthog-js": "^1.175.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-ga4": "^2.1.0",
"react-icons": "^5.6.0",
"remark-gfm": "^4.0.0",
"tailwind-merge": "^3.6.0",
"yaml": "^2.7.0"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.0.0",
"@types/node": "^22.0.0",
"@types/react": "^19.2.0",
"@types/react-dom": "^19.2.0",
"oxlint": "^1.51.0",
"postcss": "^8.5.0",
"tailwindcss": "^4.0.0",
"tsx": "^4.19.0",
"typescript": "^5.7.0",
"vitest": "^4.1.5"
},
"engines": {
"node": ">=18.18"
}
}
+8
View File
@@ -0,0 +1,8 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;
@@ -0,0 +1,92 @@
Copyright (c) 2016 The Inter Project Authors (https://github.com/rsms/inter)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION AND CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
@@ -0,0 +1,93 @@
Copyright 2020 The Plus Jakarta Sans Project Authors (https://github.com/tokotype/PlusJakartaSans)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
@@ -0,0 +1,13 @@
Source: Plus Jakarta Sans upstream repository
- Medium: https://github.com/tokotype/PlusJakartaSans/blob/master/fonts/ttf/PlusJakartaSans-Medium.ttf
- Bold: https://github.com/tokotype/PlusJakartaSans/blob/master/fonts/ttf/PlusJakartaSans-Bold.ttf
- License: https://github.com/tokotype/PlusJakartaSans/blob/master/OFL.txt
- Downloaded: 2026-06-29
- Medium SHA-256: c77bab757d7402ec6d9341d5f7ddaafb2474e17026792697ba4624c7dc89caf7
- Bold SHA-256: 5f5342ef76862b5b5365d1dff1a667629dfa484e388dd602552f647219c3870f
This file is used only by the docs Open Graph image route. The main docs
application continues to load Plus Jakarta Sans through `next/font/google`.
The Google Fonts variable TTF is not used because the current bundled
`next/og` renderer crashes while parsing its `fvar` table.
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:baa7d3e76f2339bb00b721d472a8353a4f9f4e1ed0959bea30c12ccc045d8c3c
size 599513
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:41ce2fbc7fd951c5e0c0b5fb6ff64bade385b58867123f4536a24e6e0647ec30
size 714781
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:23a22b0d95a23a003114846f0913bad84efbc01dec9ba94d22716b49fffba010
size 2355700
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c1fc61bc22dfb3b42ee9d903aacadc7450291d7201f80861740c22130466b714
size 634934
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d2eed66218e943b9e143d7d94ff2af63abf40541006e2a405dd16a6aebd6c1fb
size 420095
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f0c8c00bd656e04d2d612da6a3e0ce80f04370e959ab71dc1d9dd9c3229ad7c8
size 714749
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7373e0bbfffe8bb9140ea1953a1c2f1ebd6fe86978592b9380bfac41cb92fa31
size 205679
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:eb771cdce63d48398eaa8e732b02cf92795aa4f79f44498ef170fcf4b60693d1
size 192665
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:03c120bb55b3728a5bf9049f8feb8b5593fa6cf5ad32fa46d5628e33cb2daf7a
size 107443
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7ef0700c1b1b5243a7e47430f54b944440c04b3ac8ddc90ef85b7eaf3fdd3c0a
size 42859
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:90579b24ca6c61184bd93c8e182ae9bac0509ad8dcbedbc13290158d13b01b22
size 59776
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:50f72516390131474fbb3454a9297d48f70e96d1f960bd901e0b45d5949245a9
size 483545
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ac0ada94ebe9bd45d14de614e1cf9c4f56aa34e2d668ca661ff6113785c8120f
size 92043
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ad82ed9300c3a8de6941b9445850a8fe801660e01ce0aeac153fc00040c86996
size 95563
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c7d36d90a9cd717b8bbcd0a7377c3a9de0e5b434f3ff838e461ebd2e691f0e9e
size 499232
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:af5e06d44c1903ebf46457f49aead16f6e84a022cf59c19ecdbd4b6c753864cd
size 125732
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:fe22569ad66d2d4dc0c159c7cb3adf0a36bb66f2d17753a4a8b42e117e829c0a
size 496127
Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:dc08002f0fd2792bb1d24523800d26c654146e4142b8654b745a35bf1e1c0bfa
size 408807
Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5316fac37c971a5ae86afdadc3312a06ffcc787155cabc0475952a897cba4d5a
size 321879
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:273c6b5441df0ce2a26022ec290b240a8786842306f525befa6bc0909985f5cd
size 128570
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a628a8184a0a1c1073ea72755d622c96bb22344bc7baddba042e7c3ed3b8f4b6
size 230622
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0a20718f42a21321878725119d29e682ab451659be06981a5f3aca2b0d4cfa88
size 225872
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b717393909ca56e5a3a995bd256cabb02ead33e3438f8a456a6cb085693d3ae0
size 213295
@@ -0,0 +1,4 @@
<svg width="25" height="60" viewBox="0 0 25 60" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.37099 0.617676L0.0234375 0.617676L0.0236309 59.3831H14.7459C20.4617 59.3831 24.5671 53.8813 22.9399 48.402L10.565 6.73197C9.48788 3.10501 6.15451 0.617676 2.37099 0.617676Z" fill="#111618"/>
<path d="M0 0.501709L2.34282 0.501709C6.12859 0.501709 9.46338 2.99195 10.5385 6.62184L22.9507 48.5264C24.5733 54.0045 20.4683 59.5017 14.7549 59.5017H0.0242066" stroke="white" stroke-opacity="0.1"/>
</svg>

After

Width:  |  Height:  |  Size: 506 B

@@ -0,0 +1,4 @@
<svg width="29" height="72" viewBox="0 0 29 72" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.85378 0.642944H0.0289001L0.0291329 71.3572H17.7449C24.6229 71.3572 29.563 64.7368 27.605 58.1433L12.7139 8.00047C11.4178 3.63603 7.40662 0.642944 2.85378 0.642944Z" fill="#ffffff"/>
<path d="M0 0.503418H2.81919C7.37473 0.503418 11.3876 3.5 12.6814 7.86796L27.6173 58.2931C29.5698 64.885 24.6301 71.5 17.7551 71.5H0.0242066" stroke="#E5E5E5"/>
</svg>

After

Width:  |  Height:  |  Size: 458 B

@@ -0,0 +1,4 @@
<svg width="29" height="72" viewBox="0 0 29 72" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M25.6127 71.9946H28.5859L28.5857 0.00495911H10.7567C3.56415 0.00495911 -1.59949 6.93131 0.454178 13.8244L15.5499 64.4926C16.8759 68.9435 20.9685 71.9946 25.6127 71.9946Z" fill="#111618"/>
<path d="M28.6152 71.4966H25.796C21.2405 71.4966 17.2276 68.5 15.9339 64.132L0.99798 13.7069C-0.954548 7.11496 3.98513 0.5 10.8602 0.5H28.5861" stroke="white" stroke-opacity="0.1"/>
</svg>

After

Width:  |  Height:  |  Size: 482 B

@@ -0,0 +1,4 @@
<svg width="29" height="72" viewBox="0 0 29 72" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M25.694 71.3571L28.5189 71.3571L28.5186 0.642771L10.8029 0.64277C3.92483 0.642769 -1.01528 7.2632 0.942809 13.8567L15.8339 63.9995C17.13 68.364 21.1411 71.3571 25.694 71.3571Z" fill="#ffffff"/>
<path d="M28.5477 71.4966L25.7285 71.4966C21.173 71.4966 17.1601 68.5 15.8664 64.132L0.93048 13.7069C-1.02205 7.11496 3.91763 0.5 10.7927 0.500001L28.5186 0.500002" stroke="#E5E5E5"/>
</svg>

After

Width:  |  Height:  |  Size: 490 B

@@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18.2451 11.1117C18.165 11.0314 18.0644 10.9744 17.9543 10.9469C17.8442 10.9193 17.7287 10.9223 17.6201 10.9555C16.4284 11.3158 15.1612 11.346 13.9537 11.0429C12.7461 10.7398 11.6434 10.1148 10.7631 9.23442C9.88272 8.35406 9.2577 7.25136 8.95462 6.0438C8.65153 4.83625 8.68174 3.56909 9.04202 2.37735C9.07546 2.26875 9.07866 2.1531 9.05128 2.04282C9.0239 1.93255 8.96698 1.83182 8.88664 1.75148C8.80629 1.67113 8.70557 1.61421 8.59529 1.58683C8.48502 1.55946 8.36936 1.56266 8.26077 1.5961C6.61313 2.10082 5.16666 3.11234 4.12718 4.48672C3.21812 5.69364 2.66362 7.13014 2.52598 8.63482C2.38833 10.1395 2.67298 11.6528 3.34796 13.0046C4.02294 14.3564 5.0615 15.4933 6.34697 16.2874C7.63243 17.0815 9.11386 17.5014 10.6248 17.5C12.3876 17.5054 14.1035 16.9324 15.5092 15.8688C16.8836 14.8293 17.8951 13.3828 18.3998 11.7352C18.4329 11.627 18.436 11.5118 18.4087 11.402C18.3815 11.2923 18.3249 11.1919 18.2451 11.1117ZM14.7576 14.8703C13.4339 15.8674 11.7944 16.3527 10.1412 16.237C8.48796 16.1212 6.93218 15.4121 5.76025 14.2403C4.58833 13.0685 3.87914 11.5128 3.76325 9.85953C3.64735 8.2063 4.13255 6.56685 5.12952 5.24297C5.77905 4.38521 6.61879 3.68988 7.58264 3.21172C7.52774 3.59705 7.50006 3.98578 7.49983 4.375C7.50211 6.52919 8.35886 8.59449 9.8821 10.1177C11.4053 11.641 13.4706 12.4977 15.6248 12.5C16.0148 12.4999 16.4044 12.4722 16.7905 12.4172C16.3118 13.3812 15.616 14.221 14.7576 14.8703Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1,10 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_6481_42098)">
<path d="M9.375 3.125V1.25C9.375 1.08424 9.44085 0.925268 9.55806 0.808058C9.67527 0.690848 9.83424 0.625 10 0.625C10.1658 0.625 10.3247 0.690848 10.4419 0.808058C10.5592 0.925268 10.625 1.08424 10.625 1.25V3.125C10.625 3.29076 10.5592 3.44973 10.4419 3.56694C10.3247 3.68415 10.1658 3.75 10 3.75C9.83424 3.75 9.67527 3.68415 9.55806 3.56694C9.44085 3.44973 9.375 3.29076 9.375 3.125ZM15 10C15 10.9889 14.7068 11.9556 14.1573 12.7779C13.6079 13.6001 12.827 14.241 11.9134 14.6194C10.9998 14.9978 9.99445 15.0969 9.02455 14.9039C8.05464 14.711 7.16373 14.2348 6.46447 13.5355C5.7652 12.8363 5.289 11.9454 5.09607 10.9755C4.90315 10.0055 5.00216 9.00021 5.3806 8.08658C5.75904 7.17295 6.3999 6.39206 7.22215 5.84265C8.04439 5.29324 9.01109 5 10 5C11.3256 5.00145 12.5966 5.5287 13.5339 6.46606C14.4713 7.40343 14.9986 8.67436 15 10ZM13.75 10C13.75 9.25832 13.5301 8.5333 13.118 7.91661C12.706 7.29993 12.1203 6.81928 11.4351 6.53545C10.7498 6.25162 9.99584 6.17736 9.26841 6.32206C8.54098 6.46675 7.8728 6.8239 7.34835 7.34835C6.8239 7.8728 6.46675 8.54098 6.32206 9.26841C6.17736 9.99584 6.25162 10.7498 6.53545 11.4351C6.81928 12.1203 7.29993 12.706 7.91661 13.118C8.5333 13.5301 9.25832 13.75 10 13.75C10.9942 13.749 11.9475 13.3535 12.6505 12.6505C13.3535 11.9475 13.749 10.9942 13.75 10ZM4.55781 5.44219C4.67509 5.55946 4.83415 5.62535 5 5.62535C5.16585 5.62535 5.32491 5.55946 5.44219 5.44219C5.55946 5.32491 5.62535 5.16585 5.62535 5C5.62535 4.83415 5.55946 4.67509 5.44219 4.55781L4.19219 3.30781C4.07491 3.19054 3.91585 3.12465 3.75 3.12465C3.58415 3.12465 3.42509 3.19054 3.30781 3.30781C3.19054 3.42509 3.12465 3.58415 3.12465 3.75C3.12465 3.91585 3.19054 4.07491 3.30781 4.19219L4.55781 5.44219ZM4.55781 14.5578L3.30781 15.8078C3.19054 15.9251 3.12465 16.0841 3.12465 16.25C3.12465 16.4159 3.19054 16.5749 3.30781 16.6922C3.42509 16.8095 3.58415 16.8753 3.75 16.8753C3.91585 16.8753 4.07491 16.8095 4.19219 16.6922L5.44219 15.4422C5.50026 15.3841 5.54632 15.3152 5.57775 15.2393C5.60917 15.1634 5.62535 15.0821 5.62535 15C5.62535 14.9179 5.60917 14.8366 5.57775 14.7607C5.54632 14.6848 5.50026 14.6159 5.44219 14.5578C5.38412 14.4997 5.31518 14.4537 5.23931 14.4223C5.16344 14.3908 5.08212 14.3747 5 14.3747C4.91788 14.3747 4.83656 14.3908 4.76069 14.4223C4.68482 14.4537 4.61588 14.4997 4.55781 14.5578ZM15 5.625C15.0821 5.62506 15.1634 5.60895 15.2393 5.57759C15.3152 5.54622 15.3841 5.50021 15.4422 5.44219L16.6922 4.19219C16.8095 4.07491 16.8753 3.91585 16.8753 3.75C16.8753 3.58415 16.8095 3.42509 16.6922 3.30781C16.5749 3.19054 16.4159 3.12465 16.25 3.12465C16.0841 3.12465 15.9251 3.19054 15.8078 3.30781L14.5578 4.55781C14.4703 4.64522 14.4107 4.75663 14.3865 4.87793C14.3624 4.99924 14.3748 5.12498 14.4221 5.23924C14.4695 5.35351 14.5496 5.45116 14.6525 5.51982C14.7554 5.58849 14.8763 5.6251 15 5.625ZM15.4422 14.5578C15.3249 14.4405 15.1659 14.3747 15 14.3747C14.8341 14.3747 14.6751 14.4405 14.5578 14.5578C14.4405 14.6751 14.3747 14.8341 14.3747 15C14.3747 15.1659 14.4405 15.3249 14.5578 15.4422L15.8078 16.6922C15.8659 16.7503 15.9348 16.7963 16.0107 16.8277C16.0866 16.8592 16.1679 16.8753 16.25 16.8753C16.3321 16.8753 16.4134 16.8592 16.4893 16.8277C16.5652 16.7963 16.6341 16.7503 16.6922 16.6922C16.7503 16.6341 16.7963 16.5652 16.8277 16.4893C16.8592 16.4134 16.8753 16.3321 16.8753 16.25C16.8753 16.1679 16.8592 16.0866 16.8277 16.0107C16.7963 15.9348 16.7503 15.8659 16.6922 15.8078L15.4422 14.5578ZM3.75 10C3.75 9.83424 3.68415 9.67527 3.56694 9.55806C3.44973 9.44085 3.29076 9.375 3.125 9.375H1.25C1.08424 9.375 0.925268 9.44085 0.808058 9.55806C0.690848 9.67527 0.625 9.83424 0.625 10C0.625 10.1658 0.690848 10.3247 0.808058 10.4419C0.925268 10.5592 1.08424 10.625 1.25 10.625H3.125C3.29076 10.625 3.44973 10.5592 3.56694 10.4419C3.68415 10.3247 3.75 10.1658 3.75 10ZM10 16.25C9.83424 16.25 9.67527 16.3158 9.55806 16.4331C9.44085 16.5503 9.375 16.7092 9.375 16.875V18.75C9.375 18.9158 9.44085 19.0747 9.55806 19.1919C9.67527 19.3092 9.83424 19.375 10 19.375C10.1658 19.375 10.3247 19.3092 10.4419 19.1919C10.5592 19.0747 10.625 18.9158 10.625 18.75V16.875C10.625 16.7092 10.5592 16.5503 10.4419 16.4331C10.3247 16.3158 10.1658 16.25 10 16.25ZM18.75 9.375H16.875C16.7092 9.375 16.5503 9.44085 16.4331 9.55806C16.3158 9.67527 16.25 9.83424 16.25 10C16.25 10.1658 16.3158 10.3247 16.4331 10.4419C16.5503 10.5592 16.7092 10.625 16.875 10.625H18.75C18.9158 10.625 19.0747 10.5592 19.1919 10.4419C19.3092 10.3247 19.375 10.1658 19.375 10C19.375 9.83424 19.3092 9.67527 19.1919 9.55806C19.0747 9.44085 18.9158 9.375 18.75 9.375Z" fill="#010507"/>
</g>
<defs>
<clipPath id="clip0_6481_42098">
<rect width="20" height="20" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 4.7 KiB

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:30aa31b5560ab189e371e16b75e954e01efdd3e309faeade189323b978cf99e5
size 7654
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4885fc2366d451a8ede7678b93f8356b8d8feedf7c59cf0623ca214e03c9a8b8
size 30387
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:71b099687a2bbe494fef513c92574da324e964d29db5e8f790fc26cca3f01ddb
size 17111
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b3f8baa4728e107358284e22fc47f8dae534005669c5e42743db773f6447e9a1
size 383708
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9939bce6536fbd6c7194ea02b7aba83fc4c9381153d3a26281607c453645f27a
size 118541
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:51b7dea10980e4316f8d88e1106bcf159c8af278e615e61429f424d1080108bb
size 784573
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9b62e40a4552b9d24b10d0fffbc2666981b8faaa464243370a2a26ce8c6cdea0
size 903988
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 357 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 218 KiB

+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
<rect width="32" height="32" rx="6" fill="#F0F0F0"/>
<text x="16" y="20" text-anchor="middle" font-size="10" fill="#666" font-family="system-ui">AG</text>
</svg>

After

Width:  |  Height:  |  Size: 239 B

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
<rect width="32" height="32" rx="6" fill="#F0F0F0"/>
<text x="16" y="20" text-anchor="middle" font-size="10" fill="#666" font-family="system-ui">Ag</text>
</svg>

After

Width:  |  Height:  |  Size: 239 B

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7db0a86c12fa261b77141a69846bf4b65607d70768d9d22fd3fe0a67ed79282c
size 2825
@@ -0,0 +1,28 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="111 0 25 26" width="24" height="25" aria-hidden="true">
<defs>
<linearGradient id="cpk_g0" x1="129.301" y1="2.339" x2="125.623" y2="12.452" gradientUnits="userSpaceOnUse">
<stop stop-color="#6430AB"/>
<stop offset="1" stop-color="#AA89D8"/>
</linearGradient>
<linearGradient id="cpk_g1" x1="126.451" y1="8.039" x2="121.717" y2="17.187" gradientUnits="userSpaceOnUse">
<stop stop-color="#005DBB"/>
<stop offset="1" stop-color="#3D92E8"/>
</linearGradient>
<linearGradient id="cpk_g2" x1="128.565" y1="2.339" x2="127.139" y2="6.798" gradientUnits="userSpaceOnUse">
<stop stop-color="#1B70C4"/>
<stop offset="1" stop-color="#54A4F2"/>
</linearGradient>
<linearGradient id="cpk_g3" x1="117.94" y1="22.784" x2="132.981" y2="22.784" gradientUnits="userSpaceOnUse">
<stop stop-color="#4497EA"/>
<stop offset="0.2548" stop-color="#1463B2"/>
<stop offset="0.4987" stop-color="#0A437D"/>
<stop offset="0.6667" stop-color="#2476C8"/>
<stop offset="0.9725" stop-color="#0C549A"/>
</linearGradient>
</defs>
<path d="M119.539 8.67724C121.647 5.91912 123.397 3.19174 124.071 0.989235C124.089 0.929306 124.159 0.903848 124.211 0.938445C126.553 2.4891 130.818 3.50978 134.591 3.53373C134.655 3.53415 134.7 3.59815 134.677 3.65868C133.422 6.84085 131.89 12.5427 131.831 19.054C131.831 19.1507 131.695 19.1854 131.647 19.1014C129.5 15.3443 122.623 10.0649 119.574 8.81884C119.517 8.79565 119.501 8.72596 119.539 8.67724Z" fill="url(#cpk_g0)"/>
<path d="M126.653 6.99011C123.357 8.03363 120.345 8.61377 119.626 8.74558C119.581 8.75399 119.571 8.81729 119.615 8.83516C122.687 10.1126 129.53 15.3766 131.657 19.1184C131.661 19.1266 131.672 19.1296 131.68 19.1259C131.689 19.1218 131.693 19.1112 131.69 19.1021L126.653 6.99011Z" fill="url(#cpk_g1)"/>
<path d="M124.221 0.931583C127.042 2.47061 130.303 3.16182 134.629 3.52604C134.656 3.52836 134.665 3.56478 134.641 3.57743C134.087 3.86176 130.918 5.47449 128.565 6.33825C127.934 6.56966 127.3 6.78434 126.675 6.98241C126.662 6.98674 126.647 6.97992 126.641 6.96671L124.156 0.989873C124.139 0.949626 124.183 0.91071 124.221 0.931583Z" fill="url(#cpk_g2)"/>
<path d="M125.209 3.30419L122.405 12.6362M122.405 12.6362H129.07M122.405 12.6362L111.874 25.0387" stroke="#ABABAB" stroke-width="0.321797" stroke-linecap="round"/>
<path d="M119.181 22.4856L117.94 22.6601C118.584 24.3624 119.904 25.1059 121.479 25.1059C125.341 25.1059 124.163 20.7388 126.4 20.7388C128.023 20.7388 127.364 24.2784 130.857 24.2784C132.989 24.2784 133.201 22.1307 132.837 21.2067C132.835 21.201 132.833 21.1959 132.83 21.1908L132.259 20.316C132.222 20.2578 132.131 20.2797 132.125 20.3489L132.018 21.4092C132.011 21.483 132.013 21.5565 132.021 21.6301C132.109 22.3627 132.165 24.1405 130.857 24.1405C129.477 24.1405 129.145 20.6468 126.4 20.6468C123.181 20.6468 123.594 24.968 121.618 24.968C120.313 24.968 119.319 23.497 119.181 22.4856Z" fill="url(#cpk_g3)"/>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
<rect width="32" height="32" rx="6" fill="#F0F0F0"/>
<text x="16" y="20" text-anchor="middle" font-size="10" fill="#666" font-family="system-ui">Cl</text>
</svg>

After

Width:  |  Height:  |  Size: 239 B

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
<rect width="32" height="32" rx="6" fill="#F0F0F0"/>
<text x="16" y="20" text-anchor="middle" font-size="10" fill="#666" font-family="system-ui">Cl</text>
</svg>

After

Width:  |  Height:  |  Size: 239 B

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
<rect width="32" height="32" rx="6" fill="#F0F0F0"/>
<text x="16" y="20" text-anchor="middle" font-size="10" fill="#666" font-family="system-ui">Cr</text>
</svg>

After

Width:  |  Height:  |  Size: 239 B

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:08ace57d5bb286e700922483ddf30e0f6b8a742f2a7f260aa4e37d4ccd3a1648
size 2240
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7b3a16ca4417a134f1463ae3722ac0a437467fdbf3f7d7c9f29a08fc0cff0fbf
size 11976
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
<rect width="32" height="32" rx="6" fill="#F0F0F0"/>
<text x="16" y="20" text-anchor="middle" font-size="10" fill="#666" font-family="system-ui">Go</text>
</svg>

After

Width:  |  Height:  |  Size: 239 B

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
<rect width="32" height="32" rx="6" fill="#F0F0F0"/>
<text x="16" y="20" text-anchor="middle" font-size="10" fill="#666" font-family="system-ui">LG</text>
</svg>

After

Width:  |  Height:  |  Size: 239 B

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
<rect width="32" height="32" rx="6" fill="#F0F0F0"/>
<text x="16" y="20" text-anchor="middle" font-size="10" fill="#666" font-family="system-ui">LG</text>
</svg>

After

Width:  |  Height:  |  Size: 239 B

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
<rect width="32" height="32" rx="6" fill="#F0F0F0"/>
<text x="16" y="20" text-anchor="middle" font-size="10" fill="#666" font-family="system-ui">LG</text>
</svg>

After

Width:  |  Height:  |  Size: 239 B

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
<rect width="32" height="32" rx="6" fill="#F0F0F0"/>
<text x="16" y="20" text-anchor="middle" font-size="10" fill="#666" font-family="system-ui">La</text>
</svg>

After

Width:  |  Height:  |  Size: 239 B

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
<rect width="32" height="32" rx="6" fill="#F0F0F0"/>
<text x="16" y="20" text-anchor="middle" font-size="10" fill="#666" font-family="system-ui">Ll</text>
</svg>

After

Width:  |  Height:  |  Size: 239 B

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
<rect width="32" height="32" rx="6" fill="#F0F0F0"/>
<text x="16" y="20" text-anchor="middle" font-size="10" fill="#666" font-family="system-ui">Ma</text>
</svg>

After

Width:  |  Height:  |  Size: 239 B

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
<rect width="32" height="32" rx="6" fill="#F0F0F0"/>
<text x="16" y="20" text-anchor="middle" font-size="10" fill="#666" font-family="system-ui">MS</text>
</svg>

After

Width:  |  Height:  |  Size: 239 B

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
<rect width="32" height="32" rx="6" fill="#F0F0F0"/>
<text x="16" y="20" text-anchor="middle" font-size="10" fill="#666" font-family="system-ui">MS</text>
</svg>

After

Width:  |  Height:  |  Size: 239 B

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6e43206aba8ce88c2fbe5354fb82bab73073b0c08753fedd34c4f414e9decb9a
size 25869
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5be3c1684989cce18a7a20398e7ab44f8a86abcea989eb624a209082e9f81c9c
size 1566
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
<rect width="32" height="32" rx="6" fill="#F0F0F0"/>
<text x="16" y="20" text-anchor="middle" font-size="10" fill="#666" font-family="system-ui">Py</text>
</svg>

After

Width:  |  Height:  |  Size: 239 B

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
<rect width="32" height="32" rx="6" fill="#F0F0F0"/>
<text x="16" y="20" text-anchor="middle" font-size="10" fill="#666" font-family="system-ui">Sp</text>
</svg>

After

Width:  |  Height:  |  Size: 239 B

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
<rect width="32" height="32" rx="6" fill="#F0F0F0"/>
<text x="16" y="20" text-anchor="middle" font-size="10" fill="#666" font-family="system-ui">St</text>
</svg>

After

Width:  |  Height:  |  Size: 239 B

@@ -0,0 +1,48 @@
"use client";
import { useEffect } from "react";
import { usePathname } from "next/navigation";
export default function DocsError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
const pathname = usePathname();
useEffect(() => {
// Log the full Error instance (not just .message/.digest) so the
// stack trace reaches the server log / browser devtools. Include
// pathname where available so reports can be tied back to a page.
console.error(
`[docs] Page render error${pathname ? ` on ${pathname}` : ""}:`,
error,
);
}, [error, pathname]);
return (
<div className="flex items-center justify-center min-h-[50vh]">
<div className="text-center max-w-md">
<h2 className="text-lg font-semibold text-[var(--text)] mb-2">
Something went wrong
</h2>
<p className="text-sm text-[var(--text-muted)] mb-4">
We hit an error rendering this page. Please refresh, and if it keeps
happening, report it with the ID below so we can track it down.
</p>
{error.digest && (
<p className="text-xs text-[var(--text-faint)] mb-4 font-mono">
Error ID: {error.digest}
</p>
)}
<button
onClick={reset}
className="shell-docs-radius-control h-10 border border-[var(--border)] px-4 text-sm text-[var(--text-secondary)] transition-colors hover:bg-[var(--bg-elevated)]"
>
Try again
</button>
</div>
</div>
);
}
@@ -0,0 +1,180 @@
// /<...slug> — the root docs entry point.
//
// The Built-in Agent (the default framework) is served at the root
// surface: the bare `/` renders the docs overview with the BIA sidebar,
// and `/<slug>` URLs resolve BIA-authored pages first (see
// UnscopedDocsPage). Other frameworks remain at `/<framework>/<slug>`.
import React from "react";
import type { Metadata } from "next";
import { DocsLandingNext } from "@/components/docs-landing-next";
import { HeroQuickstartDropdown } from "@/components/hero-quickstart-dropdown";
import {
HeroStartActions,
LearnMoreAgentsLink,
} from "@/components/hero-start-commands";
import { LandingSampleTabs } from "@/components/landing-sample-tabs";
import { ShellDocsLayout } from "@/components/shell-docs-layout";
import { SidebarFrameworkSelector } from "@/components/sidebar-framework-selector";
import { UnscopedDocsPage } from "@/components/unscoped-docs-page";
import {
buildFrameworkNav,
buildRootSurfaceNav,
loadDoc,
} from "@/lib/docs-render";
import { compareByDisplayOrder } from "@/lib/framework-order";
import { navTreeToPageTree } from "@/lib/page-tree-bridge";
import {
getDocsFolder,
getDocsMode,
getIntegration,
getIntegrations,
ROOT_FRAMEWORK,
} from "@/lib/registry";
import { buildDocMetadata } from "@/lib/seo-metadata";
// Force dynamic rendering so unknown slugs reliably return HTTP 404
// from `notFound()` instead of being cached as a 200 with the not-found
// UI baked in (the search-engine-killing soft-404). The bare home page
// and known unscoped docs are still cheap to render — they're
// filesystem reads of MDX content — and Railway / upstream CDN caches
// successful responses at the edge anyway.
export const dynamic = "force-dynamic";
// Soft-default framework rendered on the bare `/` URL — the same
// framework whose docs are served at the root surface, so the sidebar
// tree on `/` is identical to what the user sees after clicking any
// Built-in Agent sidebar link.
const HOME_DEFAULT_FRAMEWORK = ROOT_FRAMEWORK;
// Per-framework self-canonical: each variant of a doc page declares
// itself canonical so search engines index every framework's quickstart
// (etc.) at its own URL rather than collapsing them all onto the bare
// /quickstart. Done at the page level so the metadata depends on params.
//
// For the bare home page we hand-wire title/description so visitors and
// social platforms see CopilotKit's positioning rather than the page's
// own first MDX line.
export async function generateMetadata({
params,
}: {
params: Promise<{ slug?: string[] }>;
}): Promise<Metadata> {
const { slug } = await params;
const slugPath = slug?.join("/") ?? "";
const canonicalPath = slugPath ? `/${slugPath}` : "/";
// Home page: brand-level title + tagline. Other unscoped slugs (e.g.
// /quickstart, /concepts/architecture) read frontmatter via loadDoc.
if (!slugPath) {
return buildDocMetadata({
title: "CopilotKit: the frontend stack for agents",
description:
"Connect any agent framework or model to your React app for chat, generative UI, canvas, and human-in-the-loop workflows.",
canonicalPath: "/",
});
}
// Root URLs serve the BIA-authored page when one exists (see
// UnscopedDocsPage) — mirror that resolution for metadata.
const doc =
loadDoc(
`integrations/${getDocsFolder(HOME_DEFAULT_FRAMEWORK)}/${slugPath}`,
) ?? loadDoc(slugPath);
return buildDocMetadata({
title: doc?.fm.title ?? slugPath,
description: doc?.fm.description,
canonicalPath,
ogPath: `/og${canonicalPath}/og.png`,
});
}
function DocsOverview() {
// Sidebar matches the soft-default framework so home `/` and the
// root-served BIA pages share the same authored IA. The empty href
// prefix serves every sidebar link at the root (`/quickstart`, …);
// the tree's `index` entry resolves to `/` and gets the active
// highlight on landing.
const docsFolder = getDocsFolder(HOME_DEFAULT_FRAMEWORK);
const integrationName =
getIntegration(HOME_DEFAULT_FRAMEWORK)?.name ?? "Built-in Agent";
// Same unified root-surface sidebar every other root page uses, so the
// sidebar is stable from the home page into any doc.
const navTree =
getDocsMode(HOME_DEFAULT_FRAMEWORK) === "authored"
? buildRootSurfaceNav(docsFolder)
: buildFrameworkNav(docsFolder, integrationName, HOME_DEFAULT_FRAMEWORK);
const pageTree = navTreeToPageTree(navTree, "");
// The home hero has no framework context, so its quickstart CTA is the
// framework picker dropdown (same accent treatment as the framework pages'
// direct quickstart link). The default framework sorts first; its
// quickstart lives at the root.
const quickstartOptions = getIntegrations()
.filter((i) => getDocsMode(i.slug) !== "hidden")
.slice()
.sort((a, b) => {
if (a.slug === HOME_DEFAULT_FRAMEWORK) return -1;
if (b.slug === HOME_DEFAULT_FRAMEWORK) return 1;
return compareByDisplayOrder(a.slug, b.slug);
})
.map((i) => ({
slug: i.slug,
name: i.slug === HOME_DEFAULT_FRAMEWORK ? "CopilotKit (Default)" : i.name,
logo: i.logo ?? null,
href:
i.slug === HOME_DEFAULT_FRAMEWORK
? "/quickstart"
: `/${i.slug}/quickstart`,
}));
return (
<ShellDocsLayout tree={pageTree} banner={<SidebarFrameworkSelector />}>
<div className="docs-inner-content max-w-[1040px] mx-auto px-4 md:px-6 pt-0 pb-6">
<section className="relative border-b border-[var(--border)] pb-6 sm:pb-7">
<div className="flex max-w-[765px] flex-col">
<div>
<h1 className="max-w-[24ch] text-[2rem] font-semibold leading-[1.08] tracking-[-0.02em] text-[var(--text)] sm:text-[2.5rem] md:mt-3">
CopilotKit
</h1>
<p className="mt-3 max-w-[58ch] text-lg font-medium leading-snug text-[var(--text-muted)] sm:text-[1.375rem]">
The frontend stack for agentic user experience.
</p>
<p className="mt-4 max-w-[58ch] text-base leading-[1.55] text-[var(--text-secondary)] sm:text-lg">
Build production chat, generative UI, shared state, and
human-in-the-loop workflows on any AG-UI compatible backend.
</p>
</div>
<div className="mt-7">
<HeroStartActions
quickstart={
<HeroQuickstartDropdown options={quickstartOptions} />
}
trailing={<LearnMoreAgentsLink />}
/>
</div>
</div>
</section>
<div className="space-y-10 pt-8">
<LandingSampleTabs />
<DocsLandingNext />
</div>
</div>
</ShellDocsLayout>
);
}
export default async function DocsPage({
params,
}: {
params: Promise<{ slug?: string[] }>;
}) {
const { slug } = await params;
// Overview page when no slug — the only path this route exclusively owns.
// All other paths (e.g. /quickstart) are intercepted by [framework] first
// due to Next.js routing precedence and fall through to UnscopedDocsPage there.
if (!slug || slug.length === 0) {
return <DocsOverview />;
}
return <UnscopedDocsPage slugPath={slug.join("/")} />;
}
@@ -0,0 +1,87 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const pageSource = readFileSync(
new URL("../page.tsx", import.meta.url),
"utf8",
);
describe("FrameworkRootShell layout", () => {
it("does not add top padding above framework landing content", () => {
const shellSource = pageSource.match(
/function FrameworkRootShell[\s\S]*?<\/ShellDocsLayout>/,
)?.[0];
expect(shellSource).toContain(
'className="docs-inner-content max-w-[900px] mx-auto px-4 md:px-6 pt-0 pb-6"',
);
expect(shellSource).not.toContain("pt-2 pb-6 md:pt-3 xl:pt-4");
});
it("parses frontend routes before resolving frontend content slugs", () => {
expect(pageSource).toContain("parseFrontendRoutePath");
expect(pageSource).toContain("activeBackendFramework");
expect(pageSource).toContain("frameworkOverride={activeBackendFramework}");
});
it("redirects retired frontend URL shapes instead of rendering them", () => {
expect(pageSource).toContain('if (framework === "frontends")');
expect(pageSource).toContain("legacyFrontendPathRedirect(");
expect(pageSource).toContain(
"const frontendRedirect = legacyFrontendPathRedirect(",
);
});
it("canonicalizes React guidance routes to the React root", () => {
expect(pageSource).toContain(
'return frontendPathForBackend("react", slugPath);',
);
});
it("renders backend docs when a frontend route includes a backend slug", () => {
expect(pageSource).toContain("scopedFramework = activeBackendFramework");
expect(pageSource).toContain("scopedSlugHrefPrefix = frontendRoutePath(");
expect(pageSource).toContain("frameworkOverride={scopedFramework}");
expect(pageSource).toContain(
"slugHrefPrefix={scopedSlugHrefPrefix ?? `/${scopedFramework}`}",
);
expect(pageSource).toContain(
"preferIndexMdx={Boolean(scopedSlugHrefPrefix)}",
);
});
it("keeps frontend root pages available under frontend/backend routes", () => {
const frontendRootIndex = pageSource.indexOf(
"if (!activeFrontendSlugPath) {\n return (\n <FrontendQuickstartDocsPage",
);
const backendScopingIndex = pageSource.indexOf(
"if (activeBackendFramework) {\n scopedFramework = activeBackendFramework",
);
expect(frontendRootIndex).toBeGreaterThan(-1);
expect(backendScopingIndex).toBeGreaterThan(-1);
expect(frontendRootIndex).toBeLessThan(backendScopingIndex);
});
it("keeps frontend guidance pages available under frontend/backend routes", () => {
const guidanceIndex = pageSource.indexOf(
"if (isFrontendGuidanceSlug(activeFrontendSlugPath))",
);
const backendScopingIndex = pageSource.indexOf(
"if (activeBackendFramework) {\n scopedFramework = activeBackendFramework",
);
expect(guidanceIndex).toBeGreaterThan(-1);
expect(backendScopingIndex).toBeGreaterThan(-1);
expect(guidanceIndex).toBeLessThan(backendScopingIndex);
expect(pageSource).toContain("<FrontendGuidanceDocsPage");
});
it("uses backend metadata for frontend routes that include a backend slug", () => {
expect(pageSource).toContain("frameworkMetadata(");
expect(pageSource).toMatch(
/frameworkMetadata\(\s*activeBackendFramework,\s*activeFrontendSlugPath/s,
);
expect(pageSource).toContain("scopedRoutePath(");
});
});
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,82 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const globalsCss = readFileSync(
new URL("../globals.css", import.meta.url),
"utf8",
);
function normalizeWhitespace(value: string): string {
return value.replace(/\s+/g, " ").trim();
}
describe("globals.css mobile docs layout", () => {
it("collapses the Fumadocs grid to one content column on mobile", () => {
expect(globalsCss).toContain(
"grid-template-columns: minmax(0, 1fr) !important;",
);
});
it("does not double-count the announcement banner in sub-xl docs layout offsets", () => {
const subXlDocsLayoutRules = globalsCss.matchAll(
/@media \((?:max-width: 767px|min-width: 768px\) and \(max-width: 1279px)\) \{\n #nd-docs-layout \{(?<body>[\s\S]*?)\n \}/g,
);
const bodies = Array.from(
subXlDocsLayoutRules,
(match) => match.groups?.body ?? "",
);
expect(bodies).toHaveLength(2);
const [mobileBody, tabletBody] = bodies;
for (const body of bodies) {
expect(body).toContain("--fd-docs-row-1: 0px !important;");
expect(body).not.toContain("--fd-banner-height");
}
expect(mobileBody).toContain(
"padding-top: calc(var(--fd-nav-height) + 1rem) !important;",
);
expect(tabletBody).toContain(
"padding-top: var(--fd-nav-height) !important;",
);
});
it("adds extra left breathing room when the tablet sidebar is visible", () => {
expect(globalsCss).toContain(
"@media (min-width: 768px) and (max-width: 1279px) {\n .docs-inner-content {\n padding-left: 24px !important;",
);
});
});
describe("globals.css docs headings", () => {
it("keeps heading anchors in block-level heading rows", () => {
expect(globalsCss).toContain(
".reference-content .docs-heading {\n display: flex;",
);
expect(globalsCss).not.toContain(
".reference-content .docs-heading {\n display: inline-flex;",
);
});
});
describe("globals.css cookbook sidebar", () => {
it("removes the empty cookbook sidebar banner and aligns the recipe list", () => {
const normalizedGlobalsCss = normalizeWhitespace(globalsCss);
expect(normalizedGlobalsCss).toContain(
normalizeWhitespace(`
.shell-docs-sidebar-cookbook > div:first-child {
display: none !important;
}
`),
);
expect(normalizedGlobalsCss).toContain(
normalizeWhitespace(`
.shell-docs-sidebar-cookbook [data-radix-scroll-area-viewport] > div:first-child {
padding-top: 0 !important;
padding-bottom: 1.5rem !important;
}
`),
);
});
});
@@ -0,0 +1,44 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const pngSignature = Buffer.from([0x89, 0x50, 0x4e, 0x47]);
function readPublicAsset(relativePath: string) {
return readFileSync(
new URL(`../../../public/${relativePath}`, import.meta.url),
);
}
describe("public image assets", () => {
it("serves agentic protocol diagrams as real PNG files", () => {
for (const assetPath of [
"images/agui-ecosystem-light.png",
"images/agui-ecosystem-dark.png",
"images/any-agentic-backend-light.png",
"images/any-agentic-backend-dark.png",
"images/mcp-and-a2a-through-agui-light.png",
"images/mcp-and-a2a-through-agui-dark.png",
]) {
const bytes = readPublicAsset(assetPath);
expect(bytes.subarray(0, pngSignature.length)).toEqual(pngSignature);
expect(bytes.toString("utf8", 0, 32)).not.toContain(
"version https://git-lfs",
);
}
});
it("serves the slack early-access gate previews as real PNG files", () => {
for (const assetPath of [
"images/slack-bot-generative-ui-light.png",
"images/slack-bot-generative-ui-dark.png",
]) {
const bytes = readPublicAsset(assetPath);
expect(bytes.subarray(0, pngSignature.length)).toEqual(pngSignature);
expect(bytes.toString("utf8", 0, 32)).not.toContain(
"version https://git-lfs",
);
}
});
});
@@ -0,0 +1,48 @@
"use client";
import { useEffect } from "react";
import { usePathname } from "next/navigation";
export default function AgUiError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
const pathname = usePathname();
useEffect(() => {
// Log the full Error instance (not just .message/.digest) so the
// stack trace reaches the server log / browser devtools. Include
// pathname where available so reports can be tied back to a page.
console.error(
`[ag-ui] Page render error${pathname ? ` on ${pathname}` : ""}:`,
error,
);
}, [error, pathname]);
return (
<div className="flex items-center justify-center min-h-[50vh]">
<div className="text-center max-w-md">
<h2 className="text-lg font-semibold text-[var(--text)] mb-2">
Something went wrong
</h2>
<p className="text-sm text-[var(--text-muted)] mb-4">
We hit an error rendering this page. Please refresh, and if it keeps
happening, report it with the ID below so we can track it down.
</p>
{error.digest && (
<p className="text-xs text-[var(--text-faint)] mb-4 font-mono">
Error ID: {error.digest}
</p>
)}
<button
onClick={reset}
className="shell-docs-radius-control h-10 border border-[var(--border)] px-4 text-sm text-[var(--text-secondary)] transition-colors hover:bg-[var(--bg-elevated)]"
>
Try again
</button>
</div>
</div>
);
}
@@ -0,0 +1,590 @@
import React from "react";
import fs from "fs";
import path from "path";
import matter from "gray-matter";
import type { Metadata } from "next";
import { notFound } from "next/navigation";
import { MDXRemote } from "next-mdx-remote/rsc";
import remarkGfm from "remark-gfm";
import {
rehypeCode,
rehypeCodeDefaultOptions,
} from "fumadocs-core/mdx-plugins";
import Link from "next/link";
import { ShellDocsLayout } from "@/components/shell-docs-layout";
import { DocsPage, DocsBody, DocsTitle } from "fumadocs-ui/page";
import type * as PageTree from "fumadocs-core/page-tree";
import { MdxCodeBlock } from "@/components/mdx-code-block";
import { docsComponents } from "@/lib/mdx-registry";
import { stripLeadingImports } from "@/lib/docs-render";
import { transformerMeta } from "@/lib/rehype-code-meta";
import { resolveWithinDir, safeReadFileSync } from "@/lib/safe-fs";
import { buildDocMetadata } from "@/lib/seo-metadata";
// Self-canonical for /ag-ui[/<slug>]. AG-UI pages aren't per-framework
// but get a canonical for parity with the rest of the docs surface.
// Title/description come from the page's MDX frontmatter so every AG-UI
// doc emits its own social card and `<meta description>` rather than
// inheriting the layout's generic site-wide values.
export async function generateMetadata({
params,
}: {
params: Promise<{ slug?: string[] }>;
}): Promise<Metadata> {
const { slug } = await params;
const slugPath = slug && slug.length > 0 ? slug.join("/") : "";
const canonicalPath = slugPath ? `/ag-ui/${slugPath}` : "/ag-ui";
// Overview page (no slug): hard-code the protocol-level title rather
// than reading from MDX, since /ag-ui has no backing file — its body
// is rendered by `OverviewContent` in this same module.
if (!slugPath) {
return buildDocMetadata({
title: "AG-UI: the Agent-User Interaction Protocol",
description:
"AG-UI is an open protocol for connecting AI agents to frontend applications via a standard event-based interface.",
canonicalPath,
});
}
// Read frontmatter from the AG-UI MDX file. Mirror the page render's
// own resolution (file.mdx → folder/index.mdx) so the metadata always
// matches what the user sees. Use safeReadFileSync to keep the read
// path-traversal-guarded.
const mdxResolved = resolveWithinDir(CONTENT_DIR, `${slugPath}.mdx`);
const indexResolved = resolveWithinDir(
CONTENT_DIR,
path.join(slugPath, "index.mdx"),
);
let title: string | undefined;
let description: string | undefined;
const relPath = mdxResolved
? path.relative(CONTENT_DIR, mdxResolved)
: indexResolved
? path.relative(CONTENT_DIR, indexResolved)
: null;
if (relPath) {
const raw = safeReadFileSync(CONTENT_DIR, relPath);
if (raw !== null) {
try {
const { data } = matter(raw);
if (typeof data.title === "string" && data.title.length > 0) {
title = data.title;
}
if (
typeof data.description === "string" &&
data.description.length > 0
) {
description = data.description;
}
} catch {
// Malformed frontmatter — fall back to the slug-derived title.
}
}
}
return buildDocMetadata({
title: title ?? titleFromSlug(slugPath),
description,
canonicalPath,
});
}
// Force dynamic rendering so unknown AG-UI slugs reliably return HTTP
// 404 from `notFound()` instead of being cached as a 200 (soft-404).
// See the matching note in the framework route.
export const dynamic = "force-dynamic";
const CONTENT_DIR = path.join(process.cwd(), "src/content/ag-ui");
// A nav entry is either a page (slug) or a named sub-group with children
type NavEntry = string | { group: string; children: NavEntry[] };
type NavSection = { section: string; entries: NavEntry[] };
type NavTab = { tab: string; sections: NavSection[] };
// Hardcoded navigation matching the original AG-UI docs.json structure.
// Only these pages appear in the sidebar — no filesystem scanning.
const NAV_DEFINITION: NavTab[] = [
{
tab: "Docs",
sections: [
{
section: "Get Started",
entries: [
"introduction",
"agentic-protocols",
{
group: "Quickstart",
children: [
"quickstart/applications",
{
group: "Build integrations",
children: [
"quickstart/introduction",
"quickstart/server",
"quickstart/middleware",
],
},
"quickstart/clients",
],
},
],
},
{
section: "Concepts",
entries: [
"concepts/architecture",
"concepts/events",
"concepts/agents",
"concepts/middleware",
"concepts/messages",
"concepts/reasoning",
"concepts/state",
"concepts/serialization",
"concepts/tools",
"concepts/capabilities",
"concepts/generative-ui-specs",
],
},
{
section: "Draft Proposals",
entries: [
"drafts/overview",
"drafts/multimodal-messages",
"drafts/interrupts",
"drafts/generative-ui",
"drafts/meta-events",
],
},
{
section: "Tutorials",
entries: ["tutorials/cursor", "tutorials/debugging"],
},
{
section: "Development",
entries: [
"development/updates",
"development/roadmap",
"development/contributing",
],
},
],
},
{
tab: "SDKs",
sections: [
{
section: "TypeScript",
entries: [
{
group: "@ag-ui/core",
children: [
"sdk/js/core/overview",
"sdk/js/core/types",
"sdk/js/core/multimodal-inputs",
"sdk/js/core/events",
],
},
{
group: "@ag-ui/client",
children: [
"sdk/js/client/overview",
"sdk/js/client/abstract-agent",
"sdk/js/client/http-agent",
"sdk/js/client/middleware",
"sdk/js/client/subscriber",
"sdk/js/client/compaction",
],
},
// sdk/js/encoder and sdk/js/proto removed (empty placeholder pages)
],
},
{
section: "Python",
entries: [
{
group: "ag_ui.core",
children: [
"sdk/python/core/overview",
"sdk/python/core/types",
"sdk/python/core/multimodal-inputs",
"sdk/python/core/events",
],
},
{
group: "ag_ui.encoder",
children: ["sdk/python/encoder/overview"],
},
],
},
],
},
];
// Fallback title derived from the slug itself when we can't read a better
// one from the file (missing file, IO error, malformed frontmatter, etc.).
function titleFromSlug(slug: string): string {
return slug.split("/").pop()?.replace(/-/g, " ") || slug;
}
// Read the title for a given slug from its MDX file. Uses gray-matter so
// frontmatter parsing is scoped to the frontmatter block (previously a
// global `title:` regex could match any `title:` line buried in an MDX
// body). Guards fs reads so a single malformed file doesn't crash the
// whole nav build.
function getTitleForSlug(slug: string): string {
const filePath = path.join(CONTENT_DIR, `${slug}.mdx`);
if (!fs.existsSync(filePath)) {
return titleFromSlug(slug);
}
let raw: string;
try {
raw = fs.readFileSync(filePath, "utf-8");
} catch (err) {
console.error(`[ag-ui] Failed to read ${filePath}:`, err);
return titleFromSlug(slug);
}
try {
const { data, content } = matter(raw);
if (typeof data.title === "string" && data.title.length > 0) {
return data.title;
}
const headingMatch = content.match(/^#\s+(.+)$/m);
if (headingMatch) return headingMatch[1];
} catch (err) {
console.error(`[ag-ui] Failed to parse frontmatter in ${filePath}:`, err);
}
return titleFromSlug(slug);
}
// Resolved nav types used for rendering
type ResolvedPage = { kind: "page"; slug: string; title: string };
type ResolvedGroup = {
kind: "group";
name: string;
children: ResolvedNavItem[];
};
type ResolvedNavItem = ResolvedPage | ResolvedGroup;
type ResolvedSection = { section: string; items: ResolvedNavItem[] };
type ResolvedTab = { tab: string; sections: ResolvedSection[] };
function resolveEntry(entry: NavEntry): ResolvedNavItem {
if (typeof entry === "string") {
return { kind: "page", slug: entry, title: getTitleForSlug(entry) };
}
return {
kind: "group",
name: entry.group,
children: entry.children.map(resolveEntry),
};
}
function getNavTabs(): ResolvedTab[] {
return NAV_DEFINITION.map((tab) => ({
tab: tab.tab,
sections: tab.sections.map((sec) => ({
section: sec.section,
items: sec.entries.map(resolveEntry),
})),
}));
}
// AG-UI-specific MDX component map: spread the full shared `docsComponents`
// registry (which already provides Callout/Cards/Tabs/FrameworkTabs/Snippet/
// PropertyReference/Steps/Step/InlineDemo/etc.) so AG-UI MDX no longer
// silently renders raw JSX when it uses anything outside a tiny local
// subset. The shared `InlineDemo` in mdx-registry is also the canonical
// implementation whose "Open full demo →" link uses an absolute
// `${NEXT_PUBLIC_SHELL_URL}/integrations/...` URL — the previous local
// copy used a relative `/integrations/...` URL that 404'd on the docs
// host (which has no /integrations route). Steps/Step now render through
// the real @/components/docs-steps component rather than a local no-op
// shim that discarded numbering.
const components = {
...docsComponents,
// Same `pre` override the docs renderer uses — surfaces a copy button
// and the optional file-path caption (driven by `rehypeCodeMeta` below).
pre: MdxCodeBlock,
};
function OverviewContent() {
return (
<div className="mx-auto max-w-3xl px-6 py-16 text-center">
<h1 className="text-3xl font-semibold text-[var(--violet)] tracking-tight mb-3">
The Agent-User Interaction Protocol
</h1>
<p className="text-base text-[var(--text-secondary)] leading-relaxed mb-10">
AG-UI is an open protocol for connecting AI agents to frontend
applications. It defines a standard event-based interface for streaming
agent state, tool calls, and generative UI to any client.
</p>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 text-left mb-10">
<Link
href="/ag-ui/concepts/architecture"
className="shell-docs-radius-surface group border border-[var(--border)] bg-[var(--bg-surface)] p-5 shadow-[var(--shadow-control)] transition-colors hover:border-[var(--accent)] hover:bg-[var(--bg-elevated)]"
>
<h3 className="text-sm font-semibold text-[var(--text)] group-hover:text-[var(--accent)] mb-1">
Concepts
</h3>
<p className="text-xs text-[var(--text-muted)]">
Architecture, events, agents, state, tools, middleware
</p>
</Link>
<Link
href="/ag-ui/quickstart/introduction"
className="shell-docs-radius-surface group border border-[var(--border)] bg-[var(--bg-surface)] p-5 shadow-[var(--shadow-control)] transition-colors hover:border-[var(--accent)] hover:bg-[var(--bg-elevated)]"
>
<h3 className="text-sm font-semibold text-[var(--text)] group-hover:text-[var(--accent)] mb-1">
Quick Start
</h3>
<p className="text-xs text-[var(--text-muted)]">
Build your first AG-UI integration step by step
</p>
</Link>
<Link
href="/ag-ui/sdk/js/core/overview"
className="shell-docs-radius-surface group border border-[var(--border)] bg-[var(--bg-surface)] p-5 shadow-[var(--shadow-control)] transition-colors hover:border-[var(--accent)] hover:bg-[var(--bg-elevated)]"
>
<h3 className="text-sm font-semibold text-[var(--text)] group-hover:text-[var(--accent)] mb-1">
JavaScript SDK
</h3>
<p className="text-xs text-[var(--text-muted)]">
@ag-ui/core, @ag-ui/client, @ag-ui/encoder
</p>
</Link>
<Link
href="/ag-ui/sdk/python/core/overview"
className="shell-docs-radius-surface group border border-[var(--border)] bg-[var(--bg-surface)] p-5 shadow-[var(--shadow-control)] transition-colors hover:border-[var(--accent)] hover:bg-[var(--bg-elevated)]"
>
<h3 className="text-sm font-semibold text-[var(--text)] group-hover:text-[var(--accent)] mb-1">
Python SDK
</h3>
<p className="text-xs text-[var(--text-muted)]">
ag_ui.core, ag_ui.encoder
</p>
</Link>
<Link
href="/ag-ui/tutorials/cursor"
className="shell-docs-radius-surface group border border-[var(--border)] bg-[var(--bg-surface)] p-5 shadow-[var(--shadow-control)] transition-colors hover:border-[var(--accent)] hover:bg-[var(--bg-elevated)]"
>
<h3 className="text-sm font-semibold text-[var(--text)] group-hover:text-[var(--accent)] mb-1">
Tutorials
</h3>
<p className="text-xs text-[var(--text-muted)]">
Hands-on guides and debugging walkthroughs
</p>
</Link>
<a
href="https://github.com/ag-ui-protocol/ag-ui"
target="_blank"
rel="noopener noreferrer"
className="shell-docs-radius-surface group border border-[var(--border)] bg-[var(--bg-surface)] p-5 shadow-[var(--shadow-control)] transition-colors hover:border-[var(--accent)] hover:bg-[var(--bg-elevated)]"
>
<h3 className="text-sm font-semibold text-[var(--text)] group-hover:text-[var(--accent)] mb-1">
GitHub
</h3>
<p className="text-xs text-[var(--text-muted)]">
Open source · Apache 2.0 · ag-ui-protocol/ag-ui
</p>
</a>
</div>
<p className="text-xs text-[var(--text-faint)]">
2 SDKs · 15+ framework adapters · Open protocol
</p>
</div>
);
}
export default async function AgUiDocPage({
params,
}: {
params: Promise<{ slug?: string[] }>;
}) {
const { slug } = await params;
const isOverview = !slug || slug.length === 0;
const slugPath = isOverview ? "" : slug.join("/");
const navTabs = getNavTabs();
function renderNavItem(
item: ResolvedNavItem,
depth: number = 0,
): React.ReactNode {
const indent = depth * 16;
if (item.kind === "page") {
const isActive = item.slug === slugPath;
return (
<Link
key={item.slug}
href={`/ag-ui/${item.slug}`}
data-active={isActive ? "true" : undefined}
className={`block py-[5px] text-[14px] transition-colors ${
isActive
? "text-[var(--text)] font-medium"
: "text-[var(--text-secondary)] hover:text-[var(--text)]"
}`}
style={{ paddingLeft: `${indent}px` }}
>
{item.title}
</Link>
);
}
return (
<div key={item.name} className="mt-2">
<div
className="py-[5px] text-[14px] text-[var(--text-muted)]"
style={{ paddingLeft: `${indent}px` }}
>
{item.name}
</div>
{item.children.map((child) => renderNavItem(child, depth + 1))}
</div>
);
}
// Overview page: no sidebar, card-based landing. Wrap in a scroll
// container because <main> in the root layout is fixed-height with
// hidden overflow (canonical layout architecture).
if (isOverview) {
return (
<div className="flex-1 min-w-0 overflow-y-auto">
<OverviewContent />
</div>
);
}
// Doc page: sidebar + MDX content. slugPath is user-supplied (URL
// segments) so route every filesystem access through resolveWithinDir
// so a crafted path like `..%2F..%2Fsecrets` can't escape CONTENT_DIR.
const mdxResolved = resolveWithinDir(CONTENT_DIR, `${slugPath}.mdx`);
const indexResolved = resolveWithinDir(
CONTENT_DIR,
path.join(slugPath, "index.mdx"),
);
let filePath: string;
if (mdxResolved && fs.existsSync(mdxResolved)) {
filePath = mdxResolved;
} else if (indexResolved && fs.existsSync(indexResolved)) {
filePath = indexResolved;
} else {
notFound();
}
const source = safeReadFileSync(
CONTENT_DIR,
path.relative(CONTENT_DIR, filePath),
);
if (source === null) {
console.error(`[ag-ui] Failed to read ${filePath}`);
notFound();
}
let content = "";
let title = titleFromSlug(slugPath) || "AG-UI";
try {
const parsed = matter(source);
content = stripLeadingImports(parsed.content);
if (typeof parsed.data.title === "string" && parsed.data.title.length > 0) {
title = parsed.data.title;
} else {
const headingMatch = parsed.content.match(/^#\s+(.+)$/m);
if (headingMatch) title = headingMatch[1];
}
// The page wrapper below renders `title` inside its own <h1>. If the
// MDX body also leads with a `# Title` heading — which is the common
// case, since that's how we extract the title when frontmatter is
// absent — MDXRemote renders a second h1 and the page shows two
// stacked titles. Strip one leading `# …` line (skipping any blank
// lines above it) so the body picks up from the body text. We only
// strip the FIRST heading and only when it's the first non-blank
// content line, so code fences and deeper headings are untouched.
content = content.replace(/^(\s*\n)*#\s+.+\n?/, "");
} catch (err) {
console.error(`[ag-ui] Failed to parse MDX in ${filePath}:`, err);
notFound();
}
// Convert AG-UI's tab > section > item structure into a Fumadocs
// PageTree. Tabs become top-level separators; sections become folders;
// items map to pages (recursively when they're groups).
function itemToNode(item: ResolvedNavItem): PageTree.Node {
if (item.kind === "page") {
return {
type: "page",
name: item.title,
url: `/ag-ui/${item.slug}`,
};
}
return {
type: "folder",
name: item.name,
defaultOpen: true,
children: item.children.map(itemToNode),
};
}
const pageTree: PageTree.Root = {
name: "AG-UI",
children: navTabs.flatMap((tab) => [
{ type: "separator" as const, name: tab.tab },
...tab.sections.flatMap((s) => [
{
type: "folder" as const,
name: s.section,
defaultOpen: true,
children: s.items.map(itemToNode),
},
]),
]),
};
return (
<ShellDocsLayout
tree={pageTree}
banner={
<Link
href="/ag-ui"
className="block text-xs font-mono uppercase tracking-widest text-[var(--violet)]"
>
AG-UI Protocol
</Link>
}
>
<DocsPage
toc={[]}
tableOfContent={{ enabled: false }}
tableOfContentPopover={{ enabled: false }}
breadcrumb={{ enabled: false }}
footer={{ enabled: false }}
>
<div className="max-w-3xl px-8 py-8">
<DocsTitle className="text-2xl font-semibold tracking-tight mb-6">
{title}
</DocsTitle>
<DocsBody className="reference-content">
<MDXRemote
source={content}
components={components}
options={{
mdxOptions: {
remarkPlugins: [remarkGfm],
rehypePlugins: [
[
rehypeCode,
{
fallbackLanguage: "plaintext",
transformers: [
...(rehypeCodeDefaultOptions.transformers ?? []),
transformerMeta(),
],
},
],
],
},
}}
/>
</DocsBody>
</div>
</DocsPage>
</ShellDocsLayout>
);
}
@@ -0,0 +1,51 @@
// /cookbook/<...slug> — individual cookbook recipe pages.
//
// Same scoped-sidebar pattern as the landing route (mirrors how
// /reference handles per-item pages). Forces the sidebar tree to the
// cookbook subtree so each recipe shows only its sibling recipes, not
// the full Documentation tree.
import type { Metadata } from "next";
import { notFound } from "next/navigation";
import { DocsPageView } from "@/components/docs-page-view";
import { buildCookbookNavTree } from "@/lib/cookbook-nav";
import { loadDoc } from "@/lib/docs-render";
import { buildDocMetadata } from "@/lib/seo-metadata";
export const dynamic = "force-dynamic";
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string[] }>;
}): Promise<Metadata> {
const { slug } = await params;
const slugTail = slug.join("/");
const slugPath = `cookbook/${slugTail}`;
const doc = loadDoc(slugPath);
return buildDocMetadata({
title: doc?.fm.title ?? slugPath,
description: doc?.fm.description,
canonicalPath: `/cookbook/${slugTail}`,
ogPath: `/og/cookbook/${slugTail}/og.png`,
});
}
export default async function CookbookSlugPage({
params,
}: {
params: Promise<{ slug: string[] }>;
}) {
const { slug } = await params;
if (!slug || slug.length === 0) notFound();
const slugPath = `cookbook/${slug.join("/")}`;
return (
<DocsPageView
slugPath={slugPath}
slugHrefPrefix=""
navTree={buildCookbookNavTree()}
sidebarBannerSlot={null}
sidebarClassName="shell-docs-sidebar-cookbook"
/>
);
}
@@ -0,0 +1,29 @@
import type { Metadata } from "next";
import { DocsPageView } from "@/components/docs-page-view";
import { buildCookbookNavTree } from "@/lib/cookbook-nav";
import { loadDoc } from "@/lib/docs-render";
import { buildDocMetadata } from "@/lib/seo-metadata";
export const dynamic = "force-dynamic";
export async function generateMetadata(): Promise<Metadata> {
const doc = loadDoc("cookbook");
return buildDocMetadata({
title: doc?.fm.title ?? "Cookbook",
description: doc?.fm.description,
canonicalPath: "/cookbook",
ogPath: "/og/cookbook/og.png",
});
}
export default function CookbookLandingPage() {
return (
<DocsPageView
slugPath="cookbook"
slugHrefPrefix=""
navTree={buildCookbookNavTree()}
sidebarBannerSlot={null}
sidebarClassName="shell-docs-sidebar-cookbook"
/>
);
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

@@ -0,0 +1,36 @@
import { describe, expect, it, vi } from "vitest";
import { redirect } from "next/navigation";
const navigation = vi.hoisted(() => ({
redirect: vi.fn((path: string) => {
throw new Error(`NEXT_REDIRECT:${path}`);
}),
notFound: vi.fn(() => {
throw new Error("NEXT_NOT_FOUND");
}),
}));
vi.mock("next/navigation", () => ({
redirect: navigation.redirect,
notFound: navigation.notFound,
}));
import FrontendDocPage from "./page";
const redirectMock = vi.mocked(redirect);
function callFrontendDocPage(frontend: string, slug: string[]) {
return FrontendDocPage({
params: Promise.resolve({ frontend, slug }),
});
}
describe("legacy frontend docs route", () => {
it("canonicalizes React guidance docs to the React root", async () => {
await expect(
callFrontendDocPage("react", ["using-these-docs"]),
).rejects.toThrow("NEXT_REDIRECT:/");
expect(redirectMock).toHaveBeenCalledWith("/");
});
});
@@ -0,0 +1,66 @@
import type { Metadata } from "next";
import { notFound, redirect } from "next/navigation";
import { frontendPathForBackend, isFrontendId } from "@/lib/frontend-options";
import { loadDoc } from "@/lib/docs-render";
import { resolveFrontendDocPage } from "@/lib/frontend-doc-policy";
import { buildDocMetadata } from "@/lib/seo-metadata";
type FrontendDocPageParams = {
frontend: string;
slug?: string[];
};
function slugPathFromParams(params: FrontendDocPageParams): string {
return params.slug?.join("/") ?? "";
}
export async function generateMetadata({
params,
}: {
params: Promise<FrontendDocPageParams>;
}): Promise<Metadata> {
const resolvedParams = await params;
const { frontend } = resolvedParams;
const slugPath = slugPathFromParams(resolvedParams);
if (!isFrontendId(frontend) || frontend === "react") {
return buildDocMetadata({
title: "Frontend docs",
canonicalPath: "/",
});
}
const resolution = resolveFrontendDocPage(frontend, slugPath);
const doc =
resolution.status === "found" ? loadDoc(resolution.contentSlugPath) : null;
return buildDocMetadata({
title: doc?.fm.title ?? slugPath,
description: doc?.fm.description,
canonicalPath:
resolution.status === "found"
? resolution.canonicalPath
: `/${frontend}/${slugPath}`,
});
}
export default async function FrontendDocPage({
params,
}: {
params: Promise<FrontendDocPageParams>;
}) {
const resolvedParams = await params;
const { frontend } = resolvedParams;
if (!isFrontendId(frontend)) notFound();
const slugPath = slugPathFromParams(resolvedParams);
if (frontend === "react") {
redirect(frontendPathForBackend("react", slugPath));
}
if (slugPath === "quickstart") redirect(`/${frontend}`);
if (slugPath === "using-these-docs") {
redirect(`/${frontend}/using-these-docs`);
}
redirect(`/${frontend}/${slugPath}`);
}
@@ -0,0 +1,49 @@
import type { Metadata } from "next";
import { notFound, redirect } from "next/navigation";
import {
FRONTEND_PAGE_IDS,
getFrontendContentSlug,
} from "@/lib/frontend-page-content";
import { getFrontendOption, isFrontendId } from "@/lib/frontend-options";
import { loadDoc } from "@/lib/docs-render";
import { buildDocMetadata } from "@/lib/seo-metadata";
export function generateStaticParams() {
return FRONTEND_PAGE_IDS.map((frontend) => ({ frontend }));
}
export async function generateMetadata({
params,
}: {
params: Promise<{ frontend: string }>;
}): Promise<Metadata> {
const { frontend } = await params;
if (!isFrontendId(frontend) || frontend === "react") {
return buildDocMetadata({
title: "Frontend quickstart",
canonicalPath: "/",
});
}
const contentSlug = getFrontendContentSlug(frontend);
const doc = loadDoc(contentSlug);
const option = getFrontendOption(frontend);
return buildDocMetadata({
title: `${doc?.fm.title ?? option.name} quickstart`,
description: doc?.fm.description,
canonicalPath: `/${frontend}`,
});
}
export default async function FrontendQuickstartPage({
params,
}: {
params: Promise<{ frontend: string }>;
}) {
const { frontend } = await params;
if (!isFrontendId(frontend)) notFound();
if (frontend === "react") redirect("/");
redirect(`/${frontend}`);
}
@@ -0,0 +1,48 @@
import type { Metadata } from "next";
import { notFound, redirect } from "next/navigation";
import {
FRONTEND_PAGE_IDS,
getFrontendGuidanceContentSlug,
} from "@/lib/frontend-page-content";
import { getFrontendOption, isFrontendId } from "@/lib/frontend-options";
import { loadDoc } from "@/lib/docs-render";
import { buildDocMetadata } from "@/lib/seo-metadata";
export function generateStaticParams() {
return FRONTEND_PAGE_IDS.map((frontend) => ({ frontend }));
}
export async function generateMetadata({
params,
}: {
params: Promise<{ frontend: string }>;
}): Promise<Metadata> {
const { frontend } = await params;
if (!isFrontendId(frontend) || frontend === "react") {
return buildDocMetadata({
title: "Using these frontend docs",
canonicalPath: "/",
});
}
const doc = loadDoc(getFrontendGuidanceContentSlug(frontend));
const option = getFrontendOption(frontend);
return buildDocMetadata({
title: `${option.name}: ${doc?.fm.title ?? "using these docs"}`,
description: doc?.fm.description,
canonicalPath: `/${frontend}/using-these-docs`,
});
}
export default async function FrontendGuidancePage({
params,
}: {
params: Promise<{ frontend: string }>;
}) {
const { frontend } = await params;
if (!isFrontendId(frontend)) notFound();
if (frontend === "react") redirect("/");
redirect(`/${frontend}/using-these-docs`);
}
@@ -0,0 +1,5 @@
import { redirect } from "next/navigation";
export default function FrontendsIndexPage() {
redirect("/");
}
File diff suppressed because it is too large Load Diff
+213
View File
@@ -0,0 +1,213 @@
import type { Metadata } from "next";
import { Plus_Jakarta_Sans } from "next/font/google";
import Script from "next/script";
import { RootProvider } from "fumadocs-ui/provider/next";
import { AnalyticsClient } from "@/components/analytics-client";
import { Banners } from "@/components/banners";
import { BrandNav } from "@/components/brand-nav";
import { FrameworkProvider } from "@/components/framework-provider";
import { ShellSearchProvider } from "@/components/search-trigger";
import { PostHogProvider } from "@/lib/providers/posthog-provider";
import { ScarfPixel } from "@/lib/providers/scarf-pixel";
import { getIntegrations } from "@/lib/registry";
import { RESERVED_ROUTE_SLUGS } from "@/lib/reserved-route-slugs";
import { getRuntimeConfig } from "@/lib/runtime-config";
import { serializeRuntimeConfig } from "@/lib/runtime-config-serialize";
import "./globals.css";
// serializeRuntimeConfig is extracted to `lib/runtime-config-serialize.ts`
// so it can be unit-tested for the OWASP escape behavior (XSS via
// `</script>`, U+2028/U+2029 line-terminator injection) without
// importing the layout into the test runner.
const plusJakartaSans = Plus_Jakarta_Sans({
subsets: ["latin"],
variable: "--font-prose",
display: "swap",
});
export const metadata: Metadata = {
title: "CopilotKit Docs",
description: "Docs, live demos, and integrations for CopilotKit",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
// FrameworkProvider needs the set of known framework slugs so it can
// detect URL-scoped framework views. The framework *selector* now
// lives inside the docs sidebar, not in the top bar, so its own
// options are wired up in the docs page-level server components.
//
// Guard against registry slugs that would collide with top-level
// route segments under src/app/ (see RESERVED_ROUTE_SLUGS). Without
// this filter, a registry entry named e.g. "reference" would cause
// FrameworkProvider.urlFramework to treat /reference as a framework
// scope rather than the reference docs route.
const reserved = new Set<string>(RESERVED_ROUTE_SLUGS);
const knownFrameworks = getIntegrations()
.map((i) => i.slug)
.filter((slug) => {
if (reserved.has(slug)) {
// Always log — a registry integration slug colliding with a
// reserved top-level route is a hard wiring bug that production
// operators need to see in logs, not a dev-only warning.
// eslint-disable-next-line no-console
console.error(
`[layout] integration slug "${slug}" collides with a reserved top-level route and was dropped from knownFrameworks. Rename the integration slug or update RESERVED_ROUTE_SLUGS.`,
);
return false;
}
return true;
});
// Distinguish "unset" from "empty" for the commit-SHA overlay.
// Docker ARG scope bugs can surface as an empty string rather than
// undefined; showing "dev" in that case is misleading. See the
// Dockerfile fix for the root cause.
const rawSha = process.env.NEXT_PUBLIC_COMMIT_SHA;
const commitLabel =
rawSha === undefined
? "dev"
: rawSha === ""
? "unknown"
: rawSha.slice(0, 7);
// Server-side: read live env at request time. `unstable_noStore()`
// inside getRuntimeConfig opts this segment out of the static
// cache so the inline <script> below always reflects the current
// Railway env vars.
const runtimeConfig = getRuntimeConfig();
const injection = `window.__SHOWCASE_CONFIG__=${serializeRuntimeConfig(runtimeConfig)};`;
const REO_KEY = runtimeConfig.reoKey;
const REB2B_KEY = runtimeConfig.reb2bKey;
return (
// suppressHydrationWarning is required because the inline theme-init
// script below adds/removes `class="dark"` on <html> before React
// hydrates. Without this, Next.js detects the className mismatch and
// reverts the client tree to match the server (which doesn't know the
// user's persisted theme), stripping the `.dark` class and breaking
// every `dark:` variant. This is the canonical Next.js recipe for a
// theme script in the document head.
<html
lang="en"
className={plusJakartaSans.variable}
suppressHydrationWarning
>
<head>
{/* MUST be the first child of <head>. Every client component
* reads window.__SHOWCASE_CONFIG__ during hydration; populating
* it from a raw inline <script> guarantees the value is set
* before the parser reaches any next-script beforeInteractive
* block (those run after the parser passes our inline script).
* Using a plain <script> rather than next/script also avoids
* the deferred-execution semantics of `strategy="beforeInteractive"`
* — `beforeInteractive` runs before hydration but AFTER raw
* parse-time scripts. */}
<script
id="__showcase_config__"
dangerouslySetInnerHTML={{ __html: injection }}
/>
{/* Apply the persisted theme before first paint to avoid a
* light-flash on dark-preferring loads. Reads `localStorage.theme`
* and falls back to `prefers-color-scheme` when the persisted
* value is missing OR explicitly `"system"` (next-themes persists
* the literal string `"system"` when the user picks that mode via
* its API — without the `=== "system"` check here, a system-
* preferring user who explicitly chose system mode would get the
* very light-flash this script exists to prevent). */}
<Script
id="theme-init"
strategy="beforeInteractive"
dangerouslySetInnerHTML={{
__html: `(function(){try{var t=localStorage.theme;if(!t||t==='system'){t=window.matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light';}if(t==='dark'){document.documentElement.classList.add('dark');}}catch(e){}})();`,
}}
/>
{REO_KEY ? (
<Script
id="reo-init-script"
strategy="afterInteractive"
dangerouslySetInnerHTML={{
__html: `
!function(){
var e, t, n;
e = ${JSON.stringify(REO_KEY)};
t = function() {
if (window.Reo) {
window.Reo.init({ clientID: e });
}
};
n = document.createElement("script");
n.src = "https://static.reo.dev/" + e + "/reo.js";
n.defer = true;
n.onload = t;
document.head.appendChild(n);
}();
`,
}}
/>
) : null}
<Script
id="hubspot-script"
type="text/javascript"
src="https://js.hs-scripts.com/45532593.js"
async
defer
/>
{REB2B_KEY ? (
<Script
id="reb2b-script"
strategy="afterInteractive"
src={`https://b2bjsstore.s3.us-west-2.amazonaws.com/b/${REB2B_KEY}/${REB2B_KEY}.js.gz`}
/>
) : null}
</head>
<body>
<AnalyticsClient />
{/* No <Suspense> wrapper around the page tree. Previously this
* was wrapped in a Suspense with a null fallback, which caused
* Next.js to start streaming the response BEFORE the page
* component called `notFound()`. Once bytes are in the wire,
* Next can't change the response status, so every unknown URL
* returned HTTP 200 + the not-found UI (a soft-404 that demoted
* the entire site in search rankings). The PostHogProvider and
* FrameworkProvider are client components and don't suspend
* during server render, so removing the boundary is safe.
*/}
<PostHogProvider>
<FrameworkProvider knownFrameworks={knownFrameworks}>
{/* RootProvider supplies Fumadocs's theme provider (next-themes).
* Search is handled exclusively by shell-docs's SearchTrigger. */}
<RootProvider
theme={{ enabled: true, defaultTheme: "system" }}
search={{ enabled: false }}
>
<ShellSearchProvider>
{/* Body is a fixed-height (100vh) flex column with hidden
* overflow (see globals.css). Banner + nav sit naturally
* at the top; <main> takes the remaining height and is
* the horizontal flex row that hosts sidebar + the
* scrolling `.docs-content-wrapper`. No sticky positioning
* is needed — chrome stays put because it's outside the
* scroll container. Mirrors canonical `#nd-home-layout`
* (margin: 0 4px; xl: 0 8px 8px 8px). */}
<Banners />
<BrandNav />
<main className="flex flex-1 min-h-0 overflow-hidden mx-1 md:mx-[22px] mt-2 md:mt-3 mb-2 md:mb-3">
{children}
</main>
</ShellSearchProvider>
</RootProvider>
</FrameworkProvider>
</PostHogProvider>
<div aria-hidden="true" className="shell-docs-commit-label">
{commitLabel}
</div>
<ScarfPixel />
</body>
</html>
);
}
@@ -0,0 +1,56 @@
// `/llms-full.txt` — every documentation page concatenated into a
// single Markdown blob, with each page rendered through
// `renderPageToLlmText()` so `<Snippet />` tags are inlined as fenced
// code blocks (otherwise the body would be useless to an LLM that
// can't execute MDX).
//
// Pages are separated by an H2-rule header so a crawler can split the
// file back into per-page chunks; this matches the convention used by
// Fumadocs's reference site and `llmstxt.org` examples.
//
// This endpoint is intentionally slow (renders every MDX page through
// `renderPageToLlmText` on each cold request). `revalidate = false`
// caches the rendered response on the Next.js server side until the
// next deploy — without that the dev server would be fine but every
// origin hit in production (health check, monitoring probe, cold CDN
// miss) would re-walk the entire docs tree. The `Cache-Control` header
// below is the SEPARATE per-response CDN/browser hint with a shorter
// max-age so external caches refresh more frequently than server-cached
// responses.
import { NextResponse } from "next/server";
import { getAllLlmPages, renderPageToLlmText } from "@/lib/llm-text";
import { getBaseUrl } from "@/lib/sitemap-helpers";
export const revalidate = false;
export function GET(): NextResponse {
const baseUrl = getBaseUrl();
const pages = getAllLlmPages();
const chunks: string[] = [];
// Top matter — short site description so the LLM has context before
// it dives into individual pages.
chunks.push("# CopilotKit Docs (Full)\n");
chunks.push(
"> Concatenated documentation for CopilotKit — the frontend framework for AI agents.",
);
chunks.push(
"> Each section below is one page; the source URL is in the H2 header.\n",
);
for (const page of pages) {
const body = renderPageToLlmText(page);
if (!body) continue;
const url = `${baseUrl}/${page.url}`;
chunks.push(`---\n\n## Source: ${url}\n`);
chunks.push(body);
}
return new NextResponse(chunks.join("\n"), {
headers: {
"Content-Type": "text/plain; charset=utf-8",
"Cache-Control": "public, max-age=300, stale-while-revalidate=3600",
},
});
}
@@ -0,0 +1,326 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { loadDoc } from "@/lib/docs-render";
import { resolveFrontendDocPage } from "@/lib/frontend-doc-policy";
import { getFrontendContentSlug } from "@/lib/frontend-page-content";
import { getDocsFolder, getDocsMode, getIntegrations } from "@/lib/registry";
import { renderPageToLlmText } from "@/lib/llm-text";
import { GET } from "./route";
vi.mock("@/lib/docs-render", () => ({
loadDoc: vi.fn(),
}));
vi.mock("@/lib/frontend-doc-policy", () => ({
resolveFrontendDocPage: vi.fn(),
}));
vi.mock("@/lib/frontend-page-content", () => ({
FRONTEND_GUIDANCE_CONTENT_SLUG: "frontends/using-these-docs",
getFrontendContentSlug: vi.fn((id: string) => `frontends/${id}`),
getFrontendGuidanceContentSlug: vi.fn((id: string) =>
id === "slack" || id === "teams"
? "frontends/using-these-docs"
: "frontends/docs-status",
),
}));
vi.mock("@/lib/frontend-options", () => ({
isFrontendId: vi.fn((value: string | undefined) =>
["react", "vue", "react-native", "slack", "teams"].includes(value ?? ""),
),
parseFrontendRoutePath: vi.fn(
(pathname: string, backendFrameworkSlugs: readonly string[] = []) => {
const [first, ...rest] = pathname.split("/").filter(Boolean);
if (!["vue", "react-native", "slack", "teams"].includes(first ?? "")) {
return null;
}
const [maybeBackend, ...tail] = rest;
const backend =
maybeBackend && backendFrameworkSlugs.includes(maybeBackend)
? maybeBackend
: null;
return {
frontend: first,
backend,
slugPath: backend ? tail.join("/") : rest.join("/"),
};
},
),
}));
vi.mock("@/lib/registry", () => ({
getDocsFolder: vi.fn((slug: string) =>
slug === "langgraph-python" || slug === "langgraph-typescript"
? "langgraph"
: slug,
),
getDocsMode: vi.fn(() => "generated"),
getIntegrations: vi.fn(() => [
{ slug: "langgraph-python" },
{ slug: "langgraph-typescript" },
]),
ROOT_FRAMEWORK: "built-in-agent",
}));
vi.mock("@/lib/llm-text", () => ({
renderPageToLlmText: vi.fn(() => "rendered markdown"),
}));
vi.mock("@/lib/reference-items", () => ({
resolveReferencePage: vi.fn(),
}));
vi.mock("@/lib/sitemap-helpers", () => ({
AG_UI_CONTENT_DIR: "/tmp/ag-ui",
}));
const loadDocMock = vi.mocked(loadDoc);
const resolveFrontendDocPageMock = vi.mocked(resolveFrontendDocPage);
const getFrontendContentSlugMock = vi.mocked(getFrontendContentSlug);
const getDocsFolderMock = vi.mocked(getDocsFolder);
const getDocsModeMock = vi.mocked(getDocsMode);
const getIntegrationsMock = vi.mocked(getIntegrations);
const renderPageToLlmTextMock = vi.mocked(renderPageToLlmText);
function callLlmsMdxRoute(slug: string[]) {
return GET(new Request("http://localhost:3003/test.mdx"), {
params: Promise.resolve({ slug }),
});
}
describe("llms-mdx route", () => {
beforeEach(() => {
vi.clearAllMocks();
getFrontendContentSlugMock.mockImplementation(
(id: string) => `frontends/${id}`,
);
getDocsFolderMock.mockImplementation((slug: string) =>
slug === "langgraph-python" || slug === "langgraph-typescript"
? "langgraph"
: slug,
);
getDocsModeMock.mockReturnValue("generated");
getIntegrationsMock.mockReturnValue([
{ slug: "langgraph-python" } as never,
{ slug: "langgraph-typescript" } as never,
]);
renderPageToLlmTextMock.mockReturnValue("rendered markdown");
});
it("prefers framework quickstart overrides for generated docs", async () => {
loadDocMock.mockImplementation((slug: string) =>
slug === "integrations/langgraph/quickstart"
? {
source: "",
filePath: "integrations/langgraph/quickstart.mdx",
fm: {
title: "LangGraph Quickstart",
description: "Framework-specific quickstart.",
},
}
: slug === "quickstart"
? {
source: "",
filePath: "quickstart.mdx",
fm: {
title: "Root Quickstart",
description: "Routing shim.",
},
}
: null,
);
const response = await callLlmsMdxRoute(["langgraph-python", "quickstart"]);
expect(response.status).toBe(200);
await expect(response.text()).resolves.toBe("rendered markdown");
expect(loadDocMock).toHaveBeenNthCalledWith(
1,
"integrations/langgraph/quickstart",
);
expect(loadDocMock).not.toHaveBeenCalledWith("quickstart");
expect(renderPageToLlmTextMock).toHaveBeenCalledWith(
expect.objectContaining({
filePath: "integrations/langgraph/quickstart.mdx",
framework: "langgraph-python",
loadSlug: "integrations/langgraph/quickstart",
}),
{ framework: "langgraph-python" },
);
});
it("serves frontend quickstart markdown from the frontend guide content", async () => {
loadDocMock.mockImplementation((slug: string) =>
slug === "frontends/slack"
? {
source: "",
filePath: "frontends/slack.mdx",
fm: {
title: "Slack Quickstart",
description: "Slack frontend docs.",
},
}
: null,
);
const response = await callLlmsMdxRoute(["slack"]);
expect(response.status).toBe(200);
await expect(response.text()).resolves.toBe("rendered markdown");
expect(loadDocMock).toHaveBeenCalledWith("frontends/slack");
expect(renderPageToLlmTextMock).toHaveBeenCalledWith(
expect.objectContaining({
url: "slack",
filePath: "frontends/slack.mdx",
loadSlug: "frontends/slack",
}),
{ framework: undefined },
);
});
it("serves frontend quickstart markdown under two-axis frontend/backend root URLs", async () => {
resolveFrontendDocPageMock.mockReturnValue({ status: "not-found" });
loadDocMock.mockImplementation((slug: string) =>
slug === "frontends/vue"
? {
source: "",
filePath: "frontends/vue.mdx",
fm: {
title: "Vue Quickstart",
description: "Vue frontend docs.",
},
}
: null,
);
const response = await callLlmsMdxRoute(["vue", "langgraph-python"]);
expect(response.status).toBe(200);
await expect(response.text()).resolves.toBe("rendered markdown");
expect(loadDocMock).toHaveBeenCalledWith("frontends/vue");
expect(loadDocMock).not.toHaveBeenCalledWith("index");
expect(loadDocMock).not.toHaveBeenCalledWith(
"integrations/langgraph/index",
);
expect(renderPageToLlmTextMock).toHaveBeenCalledWith(
expect.objectContaining({
url: "vue/langgraph-python",
filePath: "frontends/vue.mdx",
loadSlug: "frontends/vue",
framework: "langgraph-python",
}),
{ framework: "langgraph-python" },
);
});
it("serves frontend guidance markdown from the shared guidance page", async () => {
loadDocMock.mockImplementation((slug: string) =>
slug === "frontends/using-these-docs"
? {
source: "",
filePath: "frontends/using-these-docs.mdx",
fm: {
title: "About early access",
description: "How to read frontend docs.",
},
}
: null,
);
const response = await callLlmsMdxRoute(["slack", "using-these-docs"]);
expect(response.status).toBe(200);
await expect(response.text()).resolves.toBe("rendered markdown");
expect(loadDocMock).toHaveBeenCalledWith("frontends/using-these-docs");
expect(renderPageToLlmTextMock).toHaveBeenCalledWith(
expect.objectContaining({
url: "slack/using-these-docs",
filePath: "frontends/using-these-docs.mdx",
loadSlug: "frontends/using-these-docs",
}),
{ framework: undefined },
);
});
it("serves frontend guidance markdown under two-axis frontend/backend URLs", async () => {
loadDocMock.mockImplementation((slug: string) =>
slug === "frontends/docs-status"
? {
source: "",
filePath: "frontends/docs-status.mdx",
fm: {
title: "Docs status",
description: "What to expect while frontend docs catch up.",
},
}
: null,
);
const response = await callLlmsMdxRoute([
"react-native",
"langgraph-typescript",
"using-these-docs",
]);
expect(response.status).toBe(200);
await expect(response.text()).resolves.toBe("rendered markdown");
expect(loadDocMock).toHaveBeenCalledWith("frontends/docs-status");
expect(loadDocMock).not.toHaveBeenCalledWith("using-these-docs");
expect(loadDocMock).not.toHaveBeenCalledWith(
"integrations/langgraph/using-these-docs",
);
expect(renderPageToLlmTextMock).toHaveBeenCalledWith(
expect.objectContaining({
url: "react-native/langgraph-typescript/using-these-docs",
filePath: "frontends/docs-status.mdx",
loadSlug: "frontends/docs-status",
framework: "langgraph-typescript",
}),
{ framework: "langgraph-typescript" },
);
});
it("serves frontend nested markdown through the frontend doc policy", async () => {
resolveFrontendDocPageMock.mockReturnValue({
status: "found",
slugPath: "concepts/architecture",
contentSlugPath: "concepts/architecture",
canonicalPath: "/concepts/architecture",
policy: { kind: "universal" },
});
loadDocMock.mockImplementation((slug: string) =>
slug === "concepts/architecture"
? {
source: "",
filePath: "concepts/architecture.mdx",
fm: {
title: "Architecture",
description: "Shared architecture docs.",
},
}
: null,
);
const response = await callLlmsMdxRoute([
"slack",
"concepts",
"architecture",
]);
expect(response.status).toBe(200);
await expect(response.text()).resolves.toBe("rendered markdown");
expect(resolveFrontendDocPageMock).toHaveBeenCalledWith(
"slack",
"concepts/architecture",
);
expect(loadDocMock).toHaveBeenCalledWith("concepts/architecture");
expect(renderPageToLlmTextMock).toHaveBeenCalledWith(
expect.objectContaining({
url: "slack/concepts/architecture",
filePath: "concepts/architecture.mdx",
loadSlug: "concepts/architecture",
}),
{ framework: undefined },
);
});
});
@@ -0,0 +1,312 @@
import { NextResponse } from "next/server";
import path from "path";
import { AG_UI_CONTENT_DIR } from "@/lib/sitemap-helpers";
import { loadDoc } from "@/lib/docs-render";
import { resolveFrontendDocPage } from "@/lib/frontend-doc-policy";
import {
getFrontendContentSlug,
getFrontendGuidanceContentSlug,
} from "@/lib/frontend-page-content";
import { isFrontendId, parseFrontendRoutePath } from "@/lib/frontend-options";
import type { FrontendId } from "@/lib/frontend-options";
import {
getDocsFolder,
getDocsMode,
getIntegrations,
ROOT_FRAMEWORK,
} from "@/lib/registry";
import type { LlmPage } from "@/lib/llm-text";
import { renderPageToLlmText } from "@/lib/llm-text";
import { resolveReferencePage } from "@/lib/reference-items";
import fs from "fs";
import matter from "gray-matter";
// Per-page raw-Markdown endpoint. The `next.config.ts` rewrites map
// `<path>.md` and `<path>.mdx` requests onto this route so external
// crawlers and the in-page LLMCopyButton can fetch a clean, LLM-friendly
// version of each docs page.
//
// What we serve (different from the previous version, which returned
// the unrendered MDX source verbatim):
//
// - `<Snippet ... />` tags are resolved to fenced markdown code blocks
// using `demo-content.json`, so the LLM sees real code, not a JSX
// tag it can't interpret.
// - `<InlineDemo />` tags become short HTML comments (no body content
// — they're live iframes on the site).
// - Shared `<Component />` snippets (`<AGUI />`, `<FrontendTools />`,
// etc.) are inlined from the shared snippets dir, same as the live
// page renderer.
// - Frontmatter is stripped and replaced with an H1 + description
// blockquote so the title survives.
//
// URL resolution mirrors what `app/[framework]/[[...slug]]/page.tsx` does:
// - Frontend-scoped URLs reuse the same `/<frontend>` content
// resolution as the live frontend pages.
// - When the first segment is a known integration slug, we try
// `integrations/<docsFolder>/<rest>.mdx` first (or root depending on
// docs_mode), so framework-scoped URLs resolve the correct MDX.
// - Otherwise we walk the bare slug, then fall back to `/reference/...`
// and `/ag-ui/...` content roots.
export async function GET(
_req: Request,
{ params }: { params: Promise<{ slug?: string[] }> },
): Promise<NextResponse> {
const { slug = [] } = await params;
if (slug.length === 0) {
return new NextResponse("Not found", { status: 404 });
}
const resolved = resolvePage(slug);
if (!resolved) {
return new NextResponse("Not found", { status: 404 });
}
const body = renderPageToLlmText(resolved.page, {
framework: resolved.framework,
});
if (!body) {
return new NextResponse("Not found", { status: 404 });
}
return new NextResponse(body, {
headers: {
"Content-Type": "text/markdown; charset=utf-8",
"Cache-Control": "public, max-age=60, stale-while-revalidate=300",
},
});
}
interface ResolvedPage {
page: LlmPage;
framework?: string;
}
type FrontendPageId = Exclude<FrontendId, "react">;
function isFrontendGuidanceSlug(slugPath: string): boolean {
return slugPath === "using-these-docs";
}
function isFrontendRootSlug(slugPath: string): boolean {
return !slugPath || slugPath === "quickstart";
}
function resolvePage(slug: string[]): ResolvedPage | null {
const first = slug[0]!;
const rest = slug.slice(1).join("/");
const url = slug.join("/");
// /<frontend>[/<slug>].md → the same MDX rendered by the
// frontend-scoped docs pages.
if (isFrontendId(first)) {
if (first === "react") return null;
const frontend = first as FrontendPageId;
const frontendRoute = parseFrontendRoutePath(
`/${slug.join("/")}`,
getIntegrations().map((integration) => integration.slug),
);
const frontendRest = frontendRoute?.slugPath ?? rest;
const activeBackendFramework =
frontendRoute?.backend === ROOT_FRAMEWORK
? undefined
: (frontendRoute?.backend ?? undefined);
if (isFrontendGuidanceSlug(frontendRest)) {
const contentSlug = getFrontendGuidanceContentSlug(frontend);
const doc = loadDoc(contentSlug);
if (!doc) return null;
return {
page: {
url,
title: doc.fm.title,
description: doc.fm.description,
filePath: doc.filePath,
loadSlug: contentSlug,
framework: activeBackendFramework,
},
framework: activeBackendFramework,
};
}
if (isFrontendRootSlug(frontendRest)) {
const contentSlug = getFrontendContentSlug(frontend);
const doc = loadDoc(contentSlug);
if (!doc) return null;
return {
page: {
url,
title: doc.fm.title,
description: doc.fm.description,
filePath: doc.filePath,
loadSlug: contentSlug,
framework: activeBackendFramework,
},
framework: activeBackendFramework,
};
}
if (activeBackendFramework) {
return resolveFrameworkScopedPage(
activeBackendFramework,
frontendRest || "index",
url,
);
}
const contentSlug = (() => {
const resolution = resolveFrontendDocPage(frontend, frontendRest);
return resolution.status === "found" ? resolution.contentSlugPath : null;
})();
if (!contentSlug) return null;
const doc = loadDoc(contentSlug);
if (!doc) return null;
return {
page: {
url,
title: doc.fm.title,
description: doc.fm.description,
filePath: doc.filePath,
loadSlug: contentSlug,
framework: activeBackendFramework,
},
framework: activeBackendFramework,
};
}
// /reference/<slug>.md → src/content/reference/<slug>.mdx
if (first === "reference") {
const referenceSlug = rest ? rest.split("/") : [];
const resolved = resolveReferencePage(referenceSlug);
if (!resolved) return null;
const { data } = matter(resolved.raw);
return {
page: {
url,
title:
typeof data.title === "string"
? data.title
: resolved.pageSlug || "Reference",
description:
typeof data.description === "string" ? data.description : undefined,
filePath: resolved.filePath,
loadSlug: `__reference__/${resolved.contentSlug}`,
},
};
}
// /ag-ui/<slug>.md → src/content/ag-ui/<slug>.mdx
if (first === "ag-ui") {
const agSlug = rest || "index";
const filePath = findExistingMdx(AG_UI_CONTENT_DIR, agSlug);
if (!filePath) return null;
return {
page: {
url,
title: agSlug,
filePath,
loadSlug: `__ag-ui__/${agSlug}`,
},
};
}
// Framework-scoped URL: first segment is an integration slug.
const frameworkSlugs = new Set(getIntegrations().map((i) => i.slug));
if (frameworkSlugs.has(first)) {
return resolveFrameworkScopedPage(first, rest || "index", url);
}
// Bare unscoped doc. The root surface serves ROOT_FRAMEWORK's
// authored page when one exists (mirrors UnscopedDocsPage), so the
// `.md` variant must resolve the same MDX the page renders.
const rootOverride = `integrations/${getDocsFolder(ROOT_FRAMEWORK)}/${url}`;
const candidates =
getDocsMode(ROOT_FRAMEWORK) === "authored" ? [rootOverride, url] : [url];
for (const candidate of candidates) {
const doc = loadDoc(candidate);
if (!doc) continue;
const isOverride = candidate !== url;
return {
page: {
url,
title: doc.fm.title,
description: doc.fm.description,
filePath: doc.filePath,
loadSlug: candidate,
framework: isOverride ? ROOT_FRAMEWORK : undefined,
},
framework: isOverride ? ROOT_FRAMEWORK : undefined,
};
}
return null;
}
function resolveFrameworkScopedPage(
framework: string,
tail: string,
url: string,
): ResolvedPage | null {
const docsFolder = getDocsFolder(framework);
const docsMode = getDocsMode(framework);
const rootSlugPath = tail;
const frameworkSlugPath = `integrations/${docsFolder}/${tail}`;
// `authored` frameworks own their entire IA — try the per-framework
// tree first. `generated` is the inverse — root wins, framework
// tree is the override, except quickstart where the root file is
// only a routing shim and the page route prefers framework content.
const candidateOrder =
docsMode === "authored" || tail === "quickstart"
? [frameworkSlugPath, rootSlugPath]
: [rootSlugPath, frameworkSlugPath];
for (const candidate of candidateOrder) {
const doc = loadDoc(candidate);
if (!doc) continue;
return {
page: {
url,
title: doc.fm.title,
description: doc.fm.description,
filePath: doc.filePath,
loadSlug: candidate,
framework,
},
framework,
};
}
return null;
}
/**
* Resolve `<root>/<slug>.mdx` or `<root>/<slug>/index.mdx` if present.
* Returns null when neither exists. Constrained to `root` via
* `path.resolve()` + prefix check to keep slug input from escaping the
* content dir.
*/
function findExistingMdx(root: string, slug: string): string | null {
const candidates = [
path.join(root, `${slug}.mdx`),
path.join(root, slug, "index.mdx"),
];
const resolvedRoot = path.resolve(root);
for (const cand of candidates) {
const resolved = path.resolve(cand);
if (!resolved.startsWith(resolvedRoot + path.sep)) {
console.warn(
"[llms-mdx] rejecting candidate outside content root",
cand,
"root:",
root,
);
continue;
}
if (fs.existsSync(resolved)) return resolved;
}
return null;
}

Some files were not shown because too many files have changed in this diff Show More