chore: import upstream snapshot with attribution
@@ -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=
|
||||
@@ -0,0 +1,6 @@
|
||||
.next/
|
||||
tsconfig.tsbuildinfo
|
||||
node_modules/
|
||||
.env*.local
|
||||
*.log
|
||||
.vercel
|
||||
@@ -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"]
|
||||
@@ -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/`.
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
After Width: | Height: | Size: 163 KiB |
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:dc08002f0fd2792bb1d24523800d26c654146e4142b8654b745a35bf1e1c0bfa
|
||||
size 408807
|
||||
|
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
|
||||
|
After Width: | Height: | Size: 357 KiB |
|
After Width: | Height: | Size: 218 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">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(");
|
||||
});
|
||||
});
|
||||
@@ -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"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
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("/");
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||