From d5474298182d2916dc9c10414dbc46d2dc93a279 Mon Sep 17 00:00:00 2001 From: Railly Hugo Date: Thu, 17 Sep 2026 21:57:26 -0300 Subject: [PATCH 1/5] feat(docs): migrate to Geistdocs --- .github/workflows/ci.yml | 21 + AGENTS.md | 4 +- apps/web/.gitignore | 1 + apps/web/app/(main)/docs/layout.tsx | 35 - apps/web/app/(main)/examples/layout.tsx | 8 + apps/web/app/(main)/layout.tsx | 14 +- apps/web/app/[lang]/docs/[[...slug]]/page.tsx | 43 + apps/web/app/[lang]/docs/layout.tsx | 25 + apps/web/app/api/docs-chat/route.ts | 41 +- apps/web/app/api/docs-markdown/route.ts | 58 +- apps/web/app/api/docs-md/[[...slug]]/route.ts | 21 + apps/web/app/api/search/route.ts | 6 + apps/web/app/globals.css | 41 +- apps/web/app/layout.tsx | 37 +- apps/web/app/llms.txt/route.ts | 10 + apps/web/app/playground/layout.tsx | 6 +- apps/web/app/robots.ts | 11 + apps/web/app/sitemap.md/route.ts | 10 + apps/web/app/sitemap.ts | 9 + apps/web/components/docs-chat.tsx | 69 +- apps/web/components/geistdocs-provider.tsx | 13 + apps/web/components/playground.tsx | 12 +- apps/web/components/theme-provider.tsx | 1 + .../a2ui/page.mdx => content/docs/a2ui.mdx} | 7 +- .../docs/adaptive-cards.mdx} | 7 +- .../ag-ui/page.mdx => content/docs/ag-ui.mdx} | 7 +- .../page.mdx => content/docs/ai-sdk.mdx} | 7 +- .../page.mdx => content/docs/api/codegen.mdx} | 7 +- .../page.mdx => content/docs/api/core.mdx} | 7 +- .../docs/api/devtools-react.mdx} | 7 +- .../docs/api/devtools-solid.mdx} | 7 +- .../docs/api/devtools-svelte.mdx} | 7 +- .../docs/api/devtools-vue.mdx} | 7 +- .../docs/api/devtools.mdx} | 7 +- .../docs/api/directives.mdx} | 7 +- .../page.mdx => content/docs/api/image.mdx} | 7 +- .../ink/page.mdx => content/docs/api/ink.mdx} | 7 +- .../page.mdx => content/docs/api/jotai.mdx} | 7 +- .../mcp/page.mdx => content/docs/api/mcp.mdx} | 7 +- apps/web/content/docs/api/meta.json | 34 + .../page.mdx => content/docs/api/next.mdx} | 7 +- .../docs/api/react-email.mdx} | 7 +- .../docs/api/react-native.mdx} | 7 +- .../docs/api/react-pdf.mdx} | 7 +- .../docs/api/react-three-fiber.mdx} | 7 +- .../page.mdx => content/docs/api/react.mdx} | 7 +- .../page.mdx => content/docs/api/redux.mdx} | 7 +- .../docs/api/remotion.mdx} | 7 +- .../docs/api/shadcn-svelte.mdx} | 7 +- .../page.mdx => content/docs/api/shadcn.mdx} | 7 +- .../page.mdx => content/docs/api/solid.mdx} | 7 +- .../page.mdx => content/docs/api/svelte.mdx} | 7 +- .../docs/api/tanstack-start.mdx} | 8 +- .../vue/page.mdx => content/docs/api/vue.mdx} | 7 +- .../page.mdx => content/docs/api/xstate.mdx} | 7 +- .../page.mdx => content/docs/api/yaml.mdx} | 7 +- .../page.mdx => content/docs/api/zustand.mdx} | 7 +- .../page.mdx => content/docs/catalog.mdx} | 7 +- .../page.mdx => content/docs/changelog.mdx} | 7 +- .../page.mdx => content/docs/code-export.mdx} | 7 +- .../docs/computed-values.mdx} | 7 +- .../docs/custom-schema.mdx} | 7 +- .../docs/data-binding.mdx} | 7 +- .../page.mdx => content/docs/devtools.mdx} | 7 +- .../page.mdx => content/docs/directives.mdx} | 7 +- .../docs/generation-modes.mdx} | 7 +- .../docs/page.mdx => content/docs/index.mdx} | 7 +- .../docs/installation.mdx} | 7 +- apps/web/content/docs/meta.json | 40 + .../page.mdx => content/docs/migration.mdx} | 7 +- .../page.mdx => content/docs/openapi.mdx} | 7 +- .../page.mdx => content/docs/quick-start.mdx} | 7 +- .../page.mdx => content/docs/registry.mdx} | 7 +- .../page.mdx => content/docs/renderers.mdx} | 7 +- .../page.mdx => content/docs/schemas.mdx} | 7 +- .../page.mdx => content/docs/skills.mdx} | 8 +- .../specs/page.mdx => content/docs/specs.mdx} | 7 +- .../page.mdx => content/docs/streaming.mdx} | 7 +- .../page.mdx => content/docs/validation.mdx} | 7 +- .../page.mdx => content/docs/visibility.mdx} | 7 +- .../page.mdx => content/docs/watchers.mdx} | 7 +- apps/web/eslint.config.js | 1 + apps/web/lib/docs-response-headers.ts | 26 + apps/web/lib/docs-source.ts | 69 + apps/web/lib/geistdocs/config.tsx | 29 + apps/web/lib/geistdocs/source.ts | 20 + apps/web/lib/page-metadata.ts | 10 + apps/web/lib/remark-legacy-headings.ts | 26 + apps/web/lib/search-index.ts | 18 +- apps/web/lib/site.ts | 6 + apps/web/next.config.js | 26 +- apps/web/package.json | 13 +- apps/web/proxy.ts | 62 + apps/web/scripts/test-routes.mjs | 106 + apps/web/source.config.ts | 42 + apps/web/tests/docs-routes.test.mjs | 354 ++ apps/web/tests/fixtures/docs-baseline.json | 3717 ++++++++++++++ apps/web/tsconfig.json | 7 +- package.json | 2 +- patches/next@16.3.4.patch | 26 + pnpm-lock.yaml | 4347 +++++++++++++++-- pnpm-workspace.yaml | 3 + turbo.json | 21 +- 103 files changed, 9199 insertions(+), 696 deletions(-) delete mode 100644 apps/web/app/(main)/docs/layout.tsx create mode 100644 apps/web/app/(main)/examples/layout.tsx create mode 100644 apps/web/app/[lang]/docs/[[...slug]]/page.tsx create mode 100644 apps/web/app/[lang]/docs/layout.tsx create mode 100644 apps/web/app/api/docs-md/[[...slug]]/route.ts create mode 100644 apps/web/app/llms.txt/route.ts create mode 100644 apps/web/app/robots.ts create mode 100644 apps/web/app/sitemap.md/route.ts create mode 100644 apps/web/app/sitemap.ts create mode 100644 apps/web/components/geistdocs-provider.tsx rename apps/web/{app/(main)/docs/a2ui/page.mdx => content/docs/a2ui.mdx} (98%) rename apps/web/{app/(main)/docs/adaptive-cards/page.mdx => content/docs/adaptive-cards.mdx} (98%) rename apps/web/{app/(main)/docs/ag-ui/page.mdx => content/docs/ag-ui.mdx} (98%) rename apps/web/{app/(main)/docs/ai-sdk/page.mdx => content/docs/ai-sdk.mdx} (97%) rename apps/web/{app/(main)/docs/api/codegen/page.mdx => content/docs/api/codegen.mdx} (95%) rename apps/web/{app/(main)/docs/api/core/page.mdx => content/docs/api/core.mdx} (99%) rename apps/web/{app/(main)/docs/api/devtools-react/page.mdx => content/docs/api/devtools-react.mdx} (91%) rename apps/web/{app/(main)/docs/api/devtools-solid/page.mdx => content/docs/api/devtools-solid.mdx} (81%) rename apps/web/{app/(main)/docs/api/devtools-svelte/page.mdx => content/docs/api/devtools-svelte.mdx} (80%) rename apps/web/{app/(main)/docs/api/devtools-vue/page.mdx => content/docs/api/devtools-vue.mdx} (83%) rename apps/web/{app/(main)/docs/api/devtools/page.mdx => content/docs/api/devtools.mdx} (96%) rename apps/web/{app/(main)/docs/api/directives/page.mdx => content/docs/api/directives.mdx} (98%) rename apps/web/{app/(main)/docs/api/image/page.mdx => content/docs/api/image.mdx} (98%) rename apps/web/{app/(main)/docs/api/ink/page.mdx => content/docs/api/ink.mdx} (98%) rename apps/web/{app/(main)/docs/api/jotai/page.mdx => content/docs/api/jotai.mdx} (94%) rename apps/web/{app/(main)/docs/api/mcp/page.mdx => content/docs/api/mcp.mdx} (97%) create mode 100644 apps/web/content/docs/api/meta.json rename apps/web/{app/(main)/docs/api/next/page.mdx => content/docs/api/next.mdx} (97%) rename apps/web/{app/(main)/docs/api/react-email/page.mdx => content/docs/api/react-email.mdx} (98%) rename apps/web/{app/(main)/docs/api/react-native/page.mdx => content/docs/api/react-native.mdx} (98%) rename apps/web/{app/(main)/docs/api/react-pdf/page.mdx => content/docs/api/react-pdf.mdx} (98%) rename apps/web/{app/(main)/docs/api/react-three-fiber/page.mdx => content/docs/api/react-three-fiber.mdx} (98%) rename apps/web/{app/(main)/docs/api/react/page.mdx => content/docs/api/react.mdx} (98%) rename apps/web/{app/(main)/docs/api/redux/page.mdx => content/docs/api/redux.mdx} (94%) rename apps/web/{app/(main)/docs/api/remotion/page.mdx => content/docs/api/remotion.mdx} (97%) rename apps/web/{app/(main)/docs/api/shadcn-svelte/page.mdx => content/docs/api/shadcn-svelte.mdx} (97%) rename apps/web/{app/(main)/docs/api/shadcn/page.mdx => content/docs/api/shadcn.mdx} (98%) rename apps/web/{app/(main)/docs/api/solid/page.mdx => content/docs/api/solid.mdx} (97%) rename apps/web/{app/(main)/docs/api/svelte/page.mdx => content/docs/api/svelte.mdx} (95%) rename apps/web/{app/(main)/docs/api/tanstack-start/page.mdx => content/docs/api/tanstack-start.mdx} (98%) rename apps/web/{app/(main)/docs/api/vue/page.mdx => content/docs/api/vue.mdx} (98%) rename apps/web/{app/(main)/docs/api/xstate/page.mdx => content/docs/api/xstate.mdx} (91%) rename apps/web/{app/(main)/docs/api/yaml/page.mdx => content/docs/api/yaml.mdx} (97%) rename apps/web/{app/(main)/docs/api/zustand/page.mdx => content/docs/api/zustand.mdx} (94%) rename apps/web/{app/(main)/docs/catalog/page.mdx => content/docs/catalog.mdx} (96%) rename apps/web/{app/(main)/docs/changelog/page.mdx => content/docs/changelog.mdx} (99%) rename apps/web/{app/(main)/docs/code-export/page.mdx => content/docs/code-export.mdx} (96%) rename apps/web/{app/(main)/docs/computed-values/page.mdx => content/docs/computed-values.mdx} (95%) rename apps/web/{app/(main)/docs/custom-schema/page.mdx => content/docs/custom-schema.mdx} (98%) rename apps/web/{app/(main)/docs/data-binding/page.mdx => content/docs/data-binding.mdx} (98%) rename apps/web/{app/(main)/docs/devtools/page.mdx => content/docs/devtools.mdx} (98%) rename apps/web/{app/(main)/docs/directives/page.mdx => content/docs/directives.mdx} (96%) rename apps/web/{app/(main)/docs/generation-modes/page.mdx => content/docs/generation-modes.mdx} (97%) rename apps/web/{app/(main)/docs/page.mdx => content/docs/index.mdx} (97%) rename apps/web/{app/(main)/docs/installation/page.mdx => content/docs/installation.mdx} (93%) create mode 100644 apps/web/content/docs/meta.json rename apps/web/{app/(main)/docs/migration/page.mdx => content/docs/migration.mdx} (98%) rename apps/web/{app/(main)/docs/openapi/page.mdx => content/docs/openapi.mdx} (98%) rename apps/web/{app/(main)/docs/quick-start/page.mdx => content/docs/quick-start.mdx} (97%) rename apps/web/{app/(main)/docs/registry/page.mdx => content/docs/registry.mdx} (98%) rename apps/web/{app/(main)/docs/renderers/page.mdx => content/docs/renderers.mdx} (98%) rename apps/web/{app/(main)/docs/schemas/page.mdx => content/docs/schemas.mdx} (97%) rename apps/web/{app/(main)/docs/skills/page.mdx => content/docs/skills.mdx} (98%) rename apps/web/{app/(main)/docs/specs/page.mdx => content/docs/specs.mdx} (98%) rename apps/web/{app/(main)/docs/streaming/page.mdx => content/docs/streaming.mdx} (97%) rename apps/web/{app/(main)/docs/validation/page.mdx => content/docs/validation.mdx} (98%) rename apps/web/{app/(main)/docs/visibility/page.mdx => content/docs/visibility.mdx} (98%) rename apps/web/{app/(main)/docs/watchers/page.mdx => content/docs/watchers.mdx} (97%) create mode 100644 apps/web/lib/docs-response-headers.ts create mode 100644 apps/web/lib/docs-source.ts create mode 100644 apps/web/lib/geistdocs/config.tsx create mode 100644 apps/web/lib/geistdocs/source.ts create mode 100644 apps/web/lib/remark-legacy-headings.ts create mode 100644 apps/web/lib/site.ts create mode 100644 apps/web/proxy.ts create mode 100644 apps/web/scripts/test-routes.mjs create mode 100644 apps/web/source.config.ts create mode 100644 apps/web/tests/docs-routes.test.mjs create mode 100644 apps/web/tests/fixtures/docs-baseline.json create mode 100644 patches/next@16.3.4.patch diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1e6b3d92..2353451d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -53,6 +53,27 @@ jobs: run: pnpm turbo run build --filter='./packages/*' - run: pnpm test + docs: + name: Docs (${{ matrix.environment }}) + runs-on: ubuntu-latest + strategy: + matrix: + environment: [production, preview] + env: + VERCEL_ENV: ${{ matrix.environment }} + DOCS_EXPECT_NOINDEX: ${{ matrix.environment == 'preview' && '1' || '0' }} + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v4 + with: + node-version-file: .node-version + cache: pnpm + - run: pnpm install --frozen-lockfile + - run: pnpm turbo run build --filter='web^...' + - run: pnpm --filter web build + - run: pnpm --filter web test:routes + typecheck: name: Type Check runs-on: ubuntu-latest diff --git a/AGENTS.md b/AGENTS.md index 8f498b51..31994526 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -66,6 +66,8 @@ Do **not** add `--port` flags -- portless handles port assignment automatically. ## Workflow - Run `pnpm type-check` after each turn to ensure type safety +- Documentation lives in `apps/web/content/docs/` and uses Geistdocs frontmatter. Keep public `/docs` URLs, heading IDs, `lib/page-titles.ts`, `lib/docs-navigation.ts`, and the content `meta.json` files in sync. +- For docs routing or infrastructure changes, run `pnpm turbo run build --filter='web^...'`, `pnpm --filter web build`, and `pnpm --filter web test:routes`. Existing-page source and Markdown parity are covered by `apps/web/tests/fixtures/docs-baseline.json`; update fixtures only when intentionally changing the documented content. - When making user-facing changes (new packages, API changes, new features, renamed exports, changed behavior), update the relevant documentation: - Package `README.md` files in `packages/*/README.md` - Root `README.md` (if packages table, install commands, or examples are affected) @@ -90,7 +92,7 @@ When asked to prepare a release (e.g. "prepare v0.17.0"): 5. **Fill documentation gaps** — every public package should have: - A row in the root `README.md` packages table - A renderer section in the root `README.md` (if it's a renderer) - - An API reference page at `apps/web/app/(main)/docs/api//page.mdx` + - An API reference page at `apps/web/content/docs/api/.mdx` - An entry in `apps/web/lib/page-titles.ts` and `apps/web/lib/docs-navigation.ts` - An entry in the docs-chat system prompt (`apps/web/app/api/docs-chat/route.ts`) - A skill at `skills//SKILL.md` diff --git a/apps/web/.gitignore b/apps/web/.gitignore index 7e049de3..bfaef66d 100644 --- a/apps/web/.gitignore +++ b/apps/web/.gitignore @@ -11,6 +11,7 @@ # next.js /.next/ +/.source/ /out/ # production diff --git a/apps/web/app/(main)/docs/layout.tsx b/apps/web/app/(main)/docs/layout.tsx deleted file mode 100644 index bb25a3dd..00000000 --- a/apps/web/app/(main)/docs/layout.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { DocsMobileNav } from "@/components/docs-mobile-nav"; -import { DocsSidebar } from "@/components/docs-sidebar"; -import { CopyPageButton } from "@/components/copy-page-button"; -import { TableOfContents } from "@/components/table-of-contents"; - -export default function DocsLayout({ - children, -}: { - children: React.ReactNode; -}) { - return ( - <> - -
- {/* Sidebar */} - - - {/* Content */} -
-
- -
-
{children}
-
- - {/* On this page */} - -
- - ); -} diff --git a/apps/web/app/(main)/examples/layout.tsx b/apps/web/app/(main)/examples/layout.tsx new file mode 100644 index 00000000..c4e81548 --- /dev/null +++ b/apps/web/app/(main)/examples/layout.tsx @@ -0,0 +1,8 @@ +import type { ReactNode } from "react"; +import { pageMetadata } from "@/lib/page-metadata"; + +export const metadata = pageMetadata("examples"); + +export default function ExamplesLayout({ children }: { children: ReactNode }) { + return children; +} diff --git a/apps/web/app/(main)/layout.tsx b/apps/web/app/(main)/layout.tsx index 33461837..bf4cd76e 100644 --- a/apps/web/app/(main)/layout.tsx +++ b/apps/web/app/(main)/layout.tsx @@ -1,17 +1,7 @@ -import { Header } from "@/components/header"; -import { getStarCount } from "@/lib/github"; - -export default async function MainLayout({ +export default function MainLayout({ children, }: { children: React.ReactNode; }) { - const stars = await getStarCount(); - - return ( -
-
-
{children}
-
- ); + return
{children}
; } diff --git a/apps/web/app/[lang]/docs/[[...slug]]/page.tsx b/apps/web/app/[lang]/docs/[[...slug]]/page.tsx new file mode 100644 index 00000000..075c5d1e --- /dev/null +++ b/apps/web/app/[lang]/docs/[[...slug]]/page.tsx @@ -0,0 +1,43 @@ +import { MobileDocsBar } from "@vercel/geistdocs/mobile-docs-bar"; +import { createDocsPage } from "@vercel/geistdocs/pages/docs"; +import { notFound } from "next/navigation"; +import { GenerationModesDiagram } from "@/components/generation-modes-diagram"; +import { PackageInstall } from "@/components/package-install"; +import { isSafePathSegments } from "@/lib/docs-source"; +import { config } from "@/lib/geistdocs/config"; +import { geistdocsSource } from "@/lib/geistdocs/source"; +import { pageMetadata } from "@/lib/page-metadata"; + +type PageProps = { params: Promise<{ lang: string; slug?: string[] }> }; + +async function validate(params: PageProps["params"]) { + const resolved = await params; + if (resolved.lang !== "en" || !isSafePathSegments(resolved.slug ?? [])) + notFound(); + try { + if (!geistdocsSource.source.getPage(resolved.slug, resolved.lang)) + notFound(); + } catch (error) { + if (error instanceof URIError) notFound(); + throw error; + } + return resolved; +} + +const docsPage = createDocsPage({ + config, + source: geistdocsSource, + mdx: { GenerationModesDiagram, PackageInstall }, + renderTop: ({ data }) => , +}); + +export default async function Page({ params }: PageProps) { + return ; +} + +export async function generateMetadata({ params }: PageProps) { + const { slug = [] } = await validate(params); + return pageMetadata(["docs", ...slug].join("/")); +} + +export const generateStaticParams = docsPage.generateStaticParams; diff --git a/apps/web/app/[lang]/docs/layout.tsx b/apps/web/app/[lang]/docs/layout.tsx new file mode 100644 index 00000000..0234a6f2 --- /dev/null +++ b/apps/web/app/[lang]/docs/layout.tsx @@ -0,0 +1,25 @@ +import type { ReactNode } from "react"; +import { notFound } from "next/navigation"; +import { GeistdocsDocsLayout } from "@vercel/geistdocs/layout"; +import { config } from "@/lib/geistdocs/config"; +import { geistdocsSource } from "@/lib/geistdocs/source"; + +export default async function DocsLayout({ + children, + params, +}: { + children: ReactNode; + params: Promise<{ lang: string }>; +}) { + const { lang } = await params; + if (lang !== "en") notFound(); + return ( + + {children} + + ); +} diff --git a/apps/web/app/api/docs-chat/route.ts b/apps/web/app/api/docs-chat/route.ts index 09ec13c1..307c441c 100644 --- a/apps/web/app/api/docs-chat/route.ts +++ b/apps/web/app/api/docs-chat/route.ts @@ -1,11 +1,8 @@ -import { readFile } from "fs/promises"; -import { join } from "path"; import { convertToModelMessages, stepCountIs, streamText } from "ai"; import type { ModelMessage, UIMessage } from "ai"; import { createBashTool } from "bash-tool"; import { headers } from "next/headers"; -import { allDocsPages } from "@/lib/docs-navigation"; -import { mdxToCleanMarkdown } from "@/lib/mdx-to-markdown"; +import { loadAllDocsSources } from "@/lib/docs-source"; import { minuteRateLimit, dailyRateLimit } from "@/lib/rate-limit"; export const maxDuration = 60; @@ -32,37 +29,13 @@ When answering questions: - Do NOT use emojis in your responses`; async function loadDocsFiles(): Promise> { - const files: Record = {}; - - const results = await Promise.allSettled( - allDocsPages.map(async (page) => { - const slug = - page.href === "/docs" ? "" : page.href.replace(/^\/docs\/?/, ""); - const filePath = slug - ? join( - process.cwd(), - "app", - "(main)", - "docs", - ...slug.split("/"), - "page.mdx", - ) - : join(process.cwd(), "app", "(main)", "docs", "page.mdx"); - - const raw = await readFile(filePath, "utf-8"); - const md = mdxToCleanMarkdown(raw); - const fileName = slug ? `/docs/${slug}.md` : "/docs/index.md"; - return { fileName, md }; - }), + const pages = await loadAllDocsSources(); + return Object.fromEntries( + pages.map((page) => [ + page.href === "/docs" ? "/docs/index.md" : `${page.href}.md`, + page.markdown, + ]), ); - - for (const result of results) { - if (result.status === "fulfilled") { - files[result.value.fileName] = result.value.md; - } - } - - return files; } function addCacheControl(messages: ModelMessage[]): ModelMessage[] { diff --git a/apps/web/app/api/docs-markdown/route.ts b/apps/web/app/api/docs-markdown/route.ts index f4abb581..340ff76f 100644 --- a/apps/web/app/api/docs-markdown/route.ts +++ b/apps/web/app/api/docs-markdown/route.ts @@ -1,55 +1,25 @@ -import { readFile } from "fs/promises"; -import { join } from "path"; import { NextRequest, NextResponse } from "next/server"; -import { mdxToCleanMarkdown } from "@/lib/mdx-to-markdown"; +import { loadDocsSource } from "@/lib/docs-source"; export async function GET(req: NextRequest) { - const { searchParams } = new URL(req.url); - const docPath = searchParams.get("path"); - - if (!docPath) { + const docPath = req.nextUrl.searchParams.get("path"); + if (!docPath) return NextResponse.json( { error: "Missing ?path= parameter" }, { status: 400 }, ); - } - - // Sanitize path: only allow docs paths, no traversal - const normalized = docPath - .replace(/^\//, "") - .replace(/\.\./g, "") - .replace(/[^a-zA-Z0-9/-]/g, ""); - - if (!normalized.startsWith("docs")) { + const path = docPath.startsWith("/") ? docPath : `/${docPath}`; + if (!/^\/docs(?:\/[a-zA-Z0-9_-]+)*\/?$/.test(path)) { return NextResponse.json({ error: "Invalid path" }, { status: 400 }); } - - // Map URL path to file path - // /docs -> /app/(main)/docs/page.mdx - // /docs/installation -> /app/(main)/docs/installation/page.mdx - const slug = normalized === "docs" ? "" : normalized.replace(/^docs\/?/, ""); - const filePath = slug - ? join( - process.cwd(), - "app", - "(main)", - "docs", - ...slug.split("/"), - "page.mdx", - ) - : join(process.cwd(), "app", "(main)", "docs", "page.mdx"); - - try { - const raw = await readFile(filePath, "utf-8"); - const markdown = mdxToCleanMarkdown(raw); - - return new NextResponse(markdown, { - headers: { - "Content-Type": "text/markdown; charset=utf-8", - "Cache-Control": "public, max-age=3600", - }, - }); - } catch { + const page = await loadDocsSource(path); + if (!page) return NextResponse.json({ error: "Page not found" }, { status: 404 }); - } + return new NextResponse(page.markdown, { + headers: { + "Content-Type": "text/markdown; charset=utf-8", + "Cache-Control": "public, max-age=3600", + Link: `<${page.canonicalUrl}>; rel="canonical"`, + }, + }); } diff --git a/apps/web/app/api/docs-md/[[...slug]]/route.ts b/apps/web/app/api/docs-md/[[...slug]]/route.ts new file mode 100644 index 00000000..97d04aad --- /dev/null +++ b/apps/web/app/api/docs-md/[[...slug]]/route.ts @@ -0,0 +1,21 @@ +import { loadDocsSource, isSafePathSegments } from "@/lib/docs-source"; +import { applyDocsResponseHeaders } from "@/lib/docs-response-headers"; + +export async function GET( + _request: Request, + { params }: { params: Promise<{ slug?: string[] }> }, +) { + const { slug = [] } = await params; + const path = `/docs${slug.length ? `/${slug.join("/")}` : ""}`; + const page = isSafePathSegments(slug) ? await loadDocsSource(path) : null; + const headers = new Headers({ + "Content-Type": "text/markdown; charset=utf-8", + }); + applyDocsResponseHeaders(headers); + if (page) headers.set("Link", `<${page.canonicalUrl}>; rel="canonical"`); + return new Response( + page?.markdown ?? + "# Page Not Found\n\nSee [the documentation index](/llms.txt).\n", + { status: page ? 200 : 404, headers }, + ); +} diff --git a/apps/web/app/api/search/route.ts b/apps/web/app/api/search/route.ts index ae6a3d40..30f5e537 100644 --- a/apps/web/app/api/search/route.ts +++ b/apps/web/app/api/search/route.ts @@ -1,7 +1,13 @@ import { NextRequest, NextResponse } from "next/server"; import { getSearchIndex } from "@/lib/search-index"; +import { createSearchRoute } from "@vercel/geistdocs/routes/search"; +import { geistdocsSource } from "@/lib/geistdocs/source"; +import { config } from "@/lib/geistdocs/config"; + +const docsSearch = createSearchRoute({ config, source: geistdocsSource }); export async function GET(req: NextRequest) { + if (req.nextUrl.searchParams.has("query")) return docsSearch(req); const q = req.nextUrl.searchParams.get("q")?.trim().toLowerCase(); if (!q) { diff --git a/apps/web/app/globals.css b/apps/web/app/globals.css index d3cfa531..3b8db267 100644 --- a/apps/web/app/globals.css +++ b/apps/web/app/globals.css @@ -1,9 +1,16 @@ -@import "tailwindcss"; -@import "tw-animate-css"; +@import "@vercel/geistdocs/styles.css"; + +@theme { + --breakpoint-sm: 40rem; + --breakpoint-md: 48rem; + --breakpoint-lg: 64rem; + --breakpoint-xl: 80rem; + --breakpoint-2xl: 96rem; +} @source "../node_modules/streamdown/dist/index.js"; -@custom-variant dark (&:is(.dark *)); +@custom-variant dark (&:is(.dark-theme *)); :root { --radius: 0.5rem; @@ -31,7 +38,7 @@ --chat-bg: oklch(0.95 0 0); } -.dark { +.dark-theme { --ds-gray-500: oklch(0.39 0 0); /* Monochrome dark theme */ --background: oklch(0.0 0 0); @@ -189,9 +196,31 @@ article table { background-color: var(--shiki-light-bg) !important; } -.dark .shiki, -.dark .shiki span { +.dark-theme .shiki, +.dark-theme .shiki span { color: var(--shiki-dark) !important; background-color: var(--shiki-dark-bg) !important; } +@container (width < 1200px) { + #nd-page { @apply px-6 pt-6; } + #nd-page > [data-mobile-docs-bar], + #nd-page > div:has([data-mobile-toc-trigger]) { display: flex; } + #nd-page [data-mobile-toc-trigger] { width: 44px; height: 44px; } + #nd-page > div:has(> h1) { padding-inline-end: 3.5rem; } + #nd-page > div:has(> h1) + div, + #nd-page > div:has(> h1) + p + div { margin-top: 0; } +} + +@container (961px <= width < 1200px) { + #nd-page > [data-mobile-docs-bar] { display: none; } +} + +header .pointer-events-none.opacity-0 { + visibility: hidden; +} + +#nd-page div:has(> [aria-live="polite"]) > div > .max-sm\:hidden { + display: flex; +} + diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index 86398533..130c8104 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -3,11 +3,16 @@ import localFont from "next/font/local"; import { GeistPixelSquare } from "geist/font/pixel"; import "./globals.css"; import { ThemeProvider } from "@/components/theme-provider"; +import { DocsProvider } from "@/components/geistdocs-provider"; +import { Navbar } from "@vercel/geistdocs/navbar"; +import { Footer } from "@vercel/geistdocs/footer"; +import { config } from "@/lib/geistdocs/config"; import { DocsChat } from "@/components/docs-chat"; import { Analytics } from "@vercel/analytics/next"; import { SpeedInsights } from "@vercel/speed-insights/next"; import { PAGE_TITLES } from "@/lib/page-titles"; import { cookies } from "next/headers"; +import { isPreview, siteUrl, siteDescription } from "@/lib/site"; const geistSans = localFont({ src: "./fonts/GeistVF.woff", @@ -19,7 +24,8 @@ const geistMono = localFont({ }); export const metadata: Metadata = { - metadataBase: new URL("https://json-render.dev"), + metadataBase: new URL(siteUrl), + alternates: { canonical: "/" }, title: { default: `json-render | ${PAGE_TITLES[""]}`, template: "%s | json-render", @@ -64,8 +70,8 @@ export const metadata: Metadata = { images: ["/og"], }, robots: { - index: true, - follow: true, + index: !isPreview, + follow: !isPreview, }, icons: { icon: "/favicon.ico", @@ -79,15 +85,30 @@ export default async function RootLayout({ }>) { const cookieStore = await cookies(); const chatOpen = cookieStore.get("docs-chat-open")?.value === "true"; - const chatWidth = Number(cookieStore.get("docs-chat-width")?.value) || 400; + const chatWidth = Math.min( + 700, + Math.max(300, Number(cookieStore.get("docs-chat-width")?.value) || 400), + ); return ( +