mirror of
https://github.com/vercel-labs/json-render.git
synced 2026-10-03 20:38:21 +08:00
Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1710ae6a07 | ||
|
|
fdb8982792 | ||
|
|
3709614de0 | ||
|
|
9f43f07108 | ||
|
|
9164a703fb | ||
|
|
b900f21ce7 | ||
|
|
3ad3818811 | ||
|
|
32e4b095b3 | ||
|
|
d547429818 |
@@ -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
|
||||
|
||||
@@ -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/<name>/page.mdx`
|
||||
- An API reference page at `apps/web/content/docs/api/<name>.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/<name>/SKILL.md`
|
||||
|
||||
+20
-2
@@ -1,11 +1,30 @@
|
||||
# Changelog
|
||||
|
||||
## 0.20.0
|
||||
## 0.21.0
|
||||
|
||||
<!-- release:start -->
|
||||
|
||||
### New Features
|
||||
|
||||
- **TanStack Start renderer:** Added `@json-render/tanstack-start` for JSON-defined applications with file-based routes, reusable layouts, SSR loaders, head metadata, prerender paths, client navigation, and route fallbacks (#334)
|
||||
- **Experimental Jev composition:** Added `experimental_composeSpec` and `experimental_createEvaluator` to compose validated specs from app-owned candidates, plus a Jev model option and iterative composition editing in the playground
|
||||
|
||||
### Improvements
|
||||
|
||||
- **Vue named slots:** Vue registries now support catalog-declared named slots alongside the default `children` slot (#323)
|
||||
- **React streaming stability:** Stabilized streamed React renders and added coverage for incomplete streamed props and nested prop identities (#325)
|
||||
- **Documentation and project status:** Expanded renderer, Jev, and package documentation and added Labs status badges to the project README
|
||||
|
||||
### Contributors
|
||||
|
||||
- @ctate
|
||||
- @Railly
|
||||
<!-- release:end -->
|
||||
|
||||
## 0.20.0
|
||||
|
||||
### New Features
|
||||
|
||||
- **Named slots for React:** Components can declare named slots such as `header` and `footer`, while `children` remains the default slot. Slots are preserved through validation, streaming, nested conversion, code export, playground views, and Devtools navigation (#320). Built from the original contribution by @wotnak in #105
|
||||
- **Nested repeats:** `repeat.statePath` now accepts item-relative paths such as `{ "$item": "employees" }`, enabling nested data rendering across React, React Native, React Email, React PDF, Image, Ink, Solid, Svelte, and Vue (#319). Built from the original contribution by @tmchow in #256
|
||||
- **Harness chat example:** Added a complete Next.js example using the AI SDK 7 harness adapter, agent delegation, sandbox transport, and json-render components (#302)
|
||||
@@ -30,7 +49,6 @@
|
||||
- @Railly
|
||||
- @tmchow
|
||||
- @wotnak
|
||||
<!-- release:end -->
|
||||
|
||||
## 0.19.0
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/.source/
|
||||
/out/
|
||||
|
||||
# production
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<DocsMobileNav />
|
||||
<div className="max-w-7xl mx-auto px-6 py-8 lg:py-12 flex gap-12">
|
||||
{/* Sidebar */}
|
||||
<aside className="w-48 shrink-0 hidden lg:block sticky top-28 h-[calc(100vh-7rem)] overflow-y-auto">
|
||||
<DocsSidebar />
|
||||
</aside>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 min-w-0 max-w-2xl pb-20">
|
||||
<div className="flex justify-end mb-4">
|
||||
<CopyPageButton />
|
||||
</div>
|
||||
<article>{children}</article>
|
||||
</div>
|
||||
|
||||
{/* On this page */}
|
||||
<aside className="w-44 shrink-0 hidden xl:block sticky top-28 h-[calc(100vh-7rem)] overflow-y-auto">
|
||||
<TableOfContents />
|
||||
</aside>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 (
|
||||
<div className="min-h-screen flex flex-col">
|
||||
<Header stars={stars} />
|
||||
<main className="flex-1">{children}</main>
|
||||
</div>
|
||||
);
|
||||
return <main className="min-h-[calc(100dvh-4rem)]">{children}</main>;
|
||||
}
|
||||
|
||||
@@ -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 }) => <MobileDocsBar toc={data.toc} />,
|
||||
});
|
||||
|
||||
export default async function Page({ params }: PageProps) {
|
||||
return <docsPage.Page params={Promise.resolve(await validate(params))} />;
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: PageProps) {
|
||||
const { slug = [] } = await validate(params);
|
||||
return pageMetadata(["docs", ...slug].join("/"));
|
||||
}
|
||||
|
||||
export const generateStaticParams = docsPage.generateStaticParams;
|
||||
@@ -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 (
|
||||
<GeistdocsDocsLayout
|
||||
config={config}
|
||||
tree={geistdocsSource.source.getPageTree(lang)}
|
||||
containerProps={{ className: "mx-auto max-w-[1448px]" }}
|
||||
>
|
||||
{children}
|
||||
</GeistdocsDocsLayout>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -17,7 +14,7 @@ const SYSTEM_PROMPT = `You are a helpful documentation assistant for json-render
|
||||
GitHub repository: https://github.com/vercel-labs/json-render
|
||||
Documentation: https://json-render.dev/docs
|
||||
npm packages: @json-render/core, @json-render/react, @json-render/next, @json-render/tanstack-start, @json-render/ink, @json-render/vue, @json-render/svelte, @json-render/solid, @json-render/shadcn, @json-render/shadcn-svelte, @json-render/react-three-fiber, @json-render/react-native, @json-render/react-email, @json-render/react-pdf, @json-render/image, @json-render/remotion, @json-render/directives, @json-render/codegen, @json-render/devtools, @json-render/devtools-react, @json-render/devtools-vue, @json-render/devtools-svelte, @json-render/devtools-solid, @json-render/mcp, @json-render/redux, @json-render/zustand, @json-render/jotai, @json-render/xstate, @json-render/yaml
|
||||
Skills: json-render ships AI agent skills that teach coding agents how to use each package. Install with "npx skills add vercel-labs/json-render --skill <name>". Available skills: core, react, next, tanstack-start, ink, react-pdf, react-email, react-native, shadcn, shadcn-svelte, react-three-fiber, image, remotion, vue, svelte, solid, directives, codegen, devtools, mcp, redux, zustand, jotai, xstate, yaml. See /docs/skills for details.
|
||||
Skills: json-render ships AI agent skills that teach coding agents how to use each package. Install with "npx skills add vercel-labs/json-render --skill <name>". Available skills: core, react, next, tanstack-start, ink, react-pdf, react-email, react-native, shadcn, shadcn-svelte, react-three-fiber, image, remotion, vue, svelte, solid, directives, codegen, devtools, devtools-react, devtools-vue, devtools-svelte, devtools-solid, mcp, redux, zustand, jotai, xstate, yaml. See /docs/skills for details.
|
||||
|
||||
Experimental Jev composition: core exports experimental_composeSpec and experimental_createEvaluator for app-owned catalogs/candidates through Vercel AI Gateway. See /docs/jev for availability, source-build setup, and limits; do not assume the currently published npm version includes it.
|
||||
|
||||
@@ -34,37 +31,13 @@ When answering questions:
|
||||
- Do NOT use emojis in your responses`;
|
||||
|
||||
async function loadDocsFiles(): Promise<Record<string, string>> {
|
||||
const files: Record<string, string> = {};
|
||||
|
||||
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[] {
|
||||
|
||||
@@ -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"`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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 },
|
||||
);
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+31
-6
@@ -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 (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<head>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: JSON.stringify({
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebSite",
|
||||
name: "json-render",
|
||||
url: siteUrl,
|
||||
description: siteDescription,
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
{chatOpen && (
|
||||
<style
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `@media(min-width:640px){body{padding-right:${chatWidth}px}}`,
|
||||
__html: `@media(min-width:640px){body{padding-right:min(${chatWidth}px, calc(100vw - 320px))}}`,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
@@ -96,7 +117,11 @@ export default async function RootLayout({
|
||||
className={`${geistSans.variable} ${geistMono.variable} ${GeistPixelSquare.variable}`}
|
||||
>
|
||||
<ThemeProvider>
|
||||
{children}
|
||||
<DocsProvider>
|
||||
<Navbar config={config} />
|
||||
{children}
|
||||
<Footer />
|
||||
</DocsProvider>
|
||||
<DocsChat defaultOpen={chatOpen} defaultWidth={chatWidth} />
|
||||
</ThemeProvider>
|
||||
<Analytics />
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { loadAllDocsSources } from "@/lib/docs-source";
|
||||
import { siteDescription, siteUrl } from "@/lib/site";
|
||||
|
||||
export async function GET() {
|
||||
const pages = await loadAllDocsSources();
|
||||
const body = `# json-render\n\n${siteDescription}\n\n## Documentation\n\n${pages.map((page) => `- [${page.title}](${siteUrl}${page.markdownUrl})`).join("\n")}\n`;
|
||||
return new Response(body, {
|
||||
headers: { "Content-Type": "text/plain; charset=utf-8" },
|
||||
});
|
||||
}
|
||||
@@ -3,5 +3,9 @@ export default function PlaygroundLayout({
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return <div className="h-dvh flex flex-col overflow-hidden">{children}</div>;
|
||||
return (
|
||||
<main className="h-[calc(100dvh-4rem)] flex flex-col overflow-hidden">
|
||||
{children}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import type { MetadataRoute } from "next";
|
||||
import { isPreview, siteUrl } from "@/lib/site";
|
||||
|
||||
export default function robots(): MetadataRoute.Robots {
|
||||
return {
|
||||
rules: isPreview
|
||||
? { userAgent: "*", disallow: "/" }
|
||||
: { userAgent: "*", allow: "/" },
|
||||
sitemap: `${siteUrl}/sitemap.xml`,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { docsPages } from "@/lib/docs-source";
|
||||
|
||||
export function GET() {
|
||||
return new Response(
|
||||
`# json-render documentation\n\n${docsPages.map((page) => `- [${page.title}](${page.href})`).join("\n")}\n`,
|
||||
{
|
||||
headers: { "Content-Type": "text/markdown; charset=utf-8" },
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { MetadataRoute } from "next";
|
||||
import { PAGE_TITLES } from "@/lib/page-titles";
|
||||
import { siteUrl } from "@/lib/site";
|
||||
|
||||
export default function sitemap(): MetadataRoute.Sitemap {
|
||||
return Object.keys(PAGE_TITLES).map((slug) => ({
|
||||
url: `${siteUrl}/${slug}`,
|
||||
}));
|
||||
}
|
||||
@@ -141,6 +141,7 @@ export function DocsChat({
|
||||
);
|
||||
const messagesScrollRef = useRef<HTMLDivElement>(null);
|
||||
const inputRef = useRef<HTMLTextAreaElement>(null);
|
||||
const launcherRef = useRef<HTMLButtonElement>(null);
|
||||
const restoredRef = useRef(false);
|
||||
const isDraggingRef = useRef(false);
|
||||
|
||||
@@ -173,13 +174,51 @@ export function DocsChat({
|
||||
}
|
||||
}, [open, hasMounted]);
|
||||
|
||||
useEffect(() => {
|
||||
const launcher = launcherRef.current;
|
||||
if (!hasMounted || open || !launcher) return;
|
||||
const footer = document.querySelector("footer");
|
||||
let frame = 0;
|
||||
const update = () => {
|
||||
frame = 0;
|
||||
const rect = document
|
||||
.querySelector("footer fieldset")
|
||||
?.getBoundingClientRect();
|
||||
const overlap =
|
||||
rect && rect.width > 0 && rect.bottom > 0
|
||||
? Math.max(0, innerHeight - rect.top)
|
||||
: 0;
|
||||
launcher.style.setProperty("--chat-launcher-bottom", `${24 + overlap}px`);
|
||||
};
|
||||
const schedule = () => {
|
||||
if (!frame) frame = requestAnimationFrame(update);
|
||||
};
|
||||
const resize = new ResizeObserver(schedule);
|
||||
resize.observe(document.body);
|
||||
const mutation = new MutationObserver(schedule);
|
||||
if (footer) {
|
||||
resize.observe(footer);
|
||||
mutation.observe(footer, { childList: true, subtree: true });
|
||||
}
|
||||
window.addEventListener("scroll", schedule, { passive: true });
|
||||
window.addEventListener("resize", schedule);
|
||||
update();
|
||||
return () => {
|
||||
cancelAnimationFrame(frame);
|
||||
resize.disconnect();
|
||||
mutation.disconnect();
|
||||
window.removeEventListener("scroll", schedule);
|
||||
window.removeEventListener("resize", schedule);
|
||||
};
|
||||
}, [hasMounted, open]);
|
||||
|
||||
// Push page content on desktop when pane is open.
|
||||
// Use padding on body so the page scrollbar stays at the viewport edge (behind the sidebar)
|
||||
// instead of appearing right next to the sidebar's scrollbar.
|
||||
useEffect(() => {
|
||||
const body = document.body;
|
||||
if (isDesktop && open) {
|
||||
body.style.paddingRight = `${desktopWidth}px`;
|
||||
body.style.paddingRight = `min(${desktopWidth}px, calc(100vw - 320px))`;
|
||||
if (!isDraggingRef.current) {
|
||||
body.style.transition = "padding-right 150ms ease";
|
||||
}
|
||||
@@ -273,7 +312,13 @@ export function DocsChat({
|
||||
return !prev;
|
||||
});
|
||||
}
|
||||
if (e.key === "Escape" && open && isDesktop) {
|
||||
if (
|
||||
e.key === "Escape" &&
|
||||
open &&
|
||||
(isDesktop ||
|
||||
(e.target instanceof Element &&
|
||||
e.target.closest("#json-render-chat-mobile")))
|
||||
) {
|
||||
setOpen(false);
|
||||
}
|
||||
};
|
||||
@@ -459,6 +504,7 @@ export function DocsChat({
|
||||
rows={1}
|
||||
enterKeyHint="send"
|
||||
placeholder="Ask a question..."
|
||||
aria-label="Ask a question"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
@@ -496,12 +542,19 @@ export function DocsChat({
|
||||
{/* Ask AI trigger button */}
|
||||
{!open && (
|
||||
<button
|
||||
ref={launcherRef}
|
||||
data-docs-chat-launcher
|
||||
onClick={() => setOpen(true)}
|
||||
className="fixed z-50 bottom-4 left-1/2 -translate-x-1/2 sm:left-auto sm:translate-x-0 sm:right-4 flex items-center gap-2 px-4 py-2 rounded-lg border border-primary bg-primary text-primary-foreground shadow-lg hover:bg-primary/90 transition-colors text-sm font-medium"
|
||||
className="fixed z-30 bottom-[calc(1rem+env(safe-area-inset-bottom))] left-1/2 -translate-x-1/2 min-[640px]:left-auto min-[640px]:translate-x-0 min-[640px]:right-6 min-[640px]:bottom-[var(--chat-launcher-bottom,24px)] flex h-10 items-center gap-2 px-4 py-2 rounded-lg border border-primary bg-primary text-primary-foreground shadow-lg hover:bg-primary/90 transition-colors text-sm font-medium"
|
||||
aria-label="Ask AI"
|
||||
aria-expanded={open}
|
||||
aria-controls={
|
||||
isDesktop ? "json-render-chat-desktop" : "json-render-chat-mobile"
|
||||
}
|
||||
aria-keyshortcuts="Meta+I Control+I"
|
||||
>
|
||||
Ask AI
|
||||
<kbd className="hidden sm:inline-flex items-center gap-0.5 text-xs opacity-60 font-mono">
|
||||
<kbd className="hidden min-[640px]:inline-flex items-center gap-0.5 text-xs opacity-60 font-mono">
|
||||
<span>⌘</span>I
|
||||
</kbd>
|
||||
</button>
|
||||
@@ -509,9 +562,11 @@ export function DocsChat({
|
||||
|
||||
{/* Desktop: resizable side pane — always rendered, hidden on mobile via CSS */}
|
||||
<aside
|
||||
id="json-render-chat-desktop"
|
||||
inert={!open || !isDesktop}
|
||||
className={`hidden sm:flex fixed top-0 right-0 bottom-0 z-40 border-l bg-background transition-transform duration-150 ease-in-out ${open ? "translate-x-0" : "translate-x-full"}`}
|
||||
style={{ width: desktopWidth }}
|
||||
aria-hidden={!open}
|
||||
style={{ width: `min(${desktopWidth}px, calc(100vw - 320px))` }}
|
||||
aria-hidden={!open || !isDesktop}
|
||||
>
|
||||
{/* Resize handle */}
|
||||
<div
|
||||
@@ -525,6 +580,8 @@ export function DocsChat({
|
||||
{hasMounted && !isDesktop && (
|
||||
<Sheet open={open} onOpenChange={setOpen}>
|
||||
<SheetContent
|
||||
id="json-render-chat-mobile"
|
||||
aria-describedby={undefined}
|
||||
side="right"
|
||||
overlayClassName="!bg-background"
|
||||
className="!inset-0 !w-full !h-full !max-w-none p-0 flex flex-col"
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import { GeistdocsProvider } from "@vercel/geistdocs/layout";
|
||||
import type { ReactNode } from "react";
|
||||
import { config } from "@/lib/geistdocs/config";
|
||||
|
||||
export function DocsProvider({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<GeistdocsProvider config={config} lang="en">
|
||||
{children}
|
||||
</GeistdocsProvider>
|
||||
);
|
||||
}
|
||||
@@ -22,7 +22,6 @@ import {
|
||||
import { CodeBlock } from "./code-block";
|
||||
import { CopyButton } from "./copy-button";
|
||||
import { Toaster } from "./ui/sonner";
|
||||
import { Header } from "./header";
|
||||
import { InfoIcon } from "lucide-react";
|
||||
import {
|
||||
Tooltip,
|
||||
@@ -346,6 +345,11 @@ export function Playground() {
|
||||
const mobileInputRef = useRef<HTMLTextAreaElement>(null);
|
||||
const versionsEndRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const input = inputRef.current;
|
||||
if (input?.getClientRects().length) input.focus({ preventScroll: true });
|
||||
}, []);
|
||||
|
||||
// Track the currently generating version ID
|
||||
const generatingVersionIdRef = useRef<string | null>(null);
|
||||
|
||||
@@ -400,7 +404,8 @@ export function Playground() {
|
||||
|
||||
// Scroll to bottom when versions change
|
||||
useEffect(() => {
|
||||
versionsEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||
const container = versionsEndRef.current?.parentElement;
|
||||
container?.scrollTo({ top: container.scrollHeight, behavior: "smooth" });
|
||||
}, [versions]);
|
||||
|
||||
// Update version when streaming completes
|
||||
@@ -722,7 +727,6 @@ ${jsx}
|
||||
maxLength={model === "typesafe-ai/jev" ? 1000 : undefined}
|
||||
className="w-full bg-background text-base sm:text-sm resize-none outline-none placeholder:text-muted-foreground/50"
|
||||
rows={2}
|
||||
autoFocus
|
||||
/>
|
||||
<div className="flex justify-between items-end gap-2 mt-2">
|
||||
<PlaygroundControls
|
||||
@@ -1067,8 +1071,6 @@ ${jsx}
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col">
|
||||
<Header />
|
||||
|
||||
{/* Desktop: 3-pane resizable layout */}
|
||||
<div className="hidden lg:flex flex-1 min-h-0">
|
||||
<ResizablePanelGroup className="flex-1">
|
||||
|
||||
@@ -6,6 +6,7 @@ export function ThemeProvider({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<NextThemesProvider
|
||||
attribute="class"
|
||||
value={{ dark: "dark-theme", light: "light-theme" }}
|
||||
defaultTheme="dark"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata"
|
||||
export const metadata = pageMetadata("docs/a2ui")
|
||||
|
||||
# A2UI Integration
|
||||
---
|
||||
title: "A2UI Integration"
|
||||
---
|
||||
|
||||
Use `@json-render/core` to support [A2UI](https://a2ui.org) natively.
|
||||
|
||||
+3
-4
@@ -1,7 +1,6 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata"
|
||||
export const metadata = pageMetadata("docs/adaptive-cards")
|
||||
|
||||
# Adaptive Cards Integration
|
||||
---
|
||||
title: "Adaptive Cards Integration"
|
||||
---
|
||||
|
||||
Use json-render to render [Microsoft Adaptive Cards](https://adaptivecards.io) natively.
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata"
|
||||
export const metadata = pageMetadata("docs/ag-ui")
|
||||
|
||||
# AG-UI Integration
|
||||
---
|
||||
title: "AG-UI Integration"
|
||||
---
|
||||
|
||||
Use json-render to support [AG-UI](https://docs.copilotkit.ai/ag-ui) (Agent User Interaction Protocol) from CopilotKit.
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata"
|
||||
export const metadata = pageMetadata("docs/ai-sdk")
|
||||
|
||||
# AI SDK Integration
|
||||
---
|
||||
title: "AI SDK Integration"
|
||||
---
|
||||
|
||||
Use json-render with the [Vercel AI SDK](https://sdk.vercel.ai) for seamless streaming. json-render supports two modes: **Standalone** (standalone UI) and **Inline** (UI embedded in conversation). See [Generation Modes](/docs/generation-modes) for a detailed comparison.
|
||||
|
||||
+3
-4
@@ -1,7 +1,6 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata"
|
||||
export const metadata = pageMetadata("docs/api/codegen")
|
||||
|
||||
# @json-render/codegen
|
||||
---
|
||||
title: "@json-render/codegen"
|
||||
---
|
||||
|
||||
Utilities for generating code from UI trees.
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata"
|
||||
export const metadata = pageMetadata("docs/api/core")
|
||||
|
||||
# @json-render/core
|
||||
---
|
||||
title: "@json-render/core"
|
||||
---
|
||||
|
||||
Core types, schemas, and utilities.
|
||||
|
||||
@@ -551,14 +550,19 @@ const name2 = resolveDynamicValue({ $state: "/user/name" }, state); // "Alice"
|
||||
|
||||
### findFormValue
|
||||
|
||||
Read a value from resolved action parameters or state. A parameter value is literal, including strings with dots such as emails, URLs, and versions. Lookup order: a defined direct parameter, a parameter key ending in `.<fieldName>`, a matching flat state key, then a slash-delimited path in nested state.
|
||||
|
||||
```typescript
|
||||
import { findFormValue } from '@json-render/core';
|
||||
|
||||
// Find form values regardless of path format
|
||||
// Checks: params.name, params["form.name"], state["form.name"], state.form.name
|
||||
const value = findFormValue("name", params, state);
|
||||
findFormValue("email", { email: "john.doe@example.com" }, {});
|
||||
findFormValue("email", { "form.email": "john.doe@example.com" }, {});
|
||||
findFormValue("email", {}, { "form.email": "john.doe@example.com" });
|
||||
findFormValue("/form/email", {}, { form: { email: "john.doe@example.com" } });
|
||||
```
|
||||
|
||||
For action bindings, use `{ $state: "/form/email" }` to read nested state: the action resolver passes the resulting value to the handler. A raw string like `"form.email"` in parameters is not a state reference. A bare `"email"` field name does not search `state.form.email`.
|
||||
|
||||
## buildUserPrompt
|
||||
|
||||
Build structured user prompts for AI generation, with support for refinement and state context.
|
||||
+3
-4
@@ -1,7 +1,6 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata"
|
||||
export const metadata = pageMetadata("docs/api/devtools-react")
|
||||
|
||||
# @json-render/devtools-react
|
||||
---
|
||||
title: "@json-render/devtools-react"
|
||||
---
|
||||
|
||||
React adapter for the json-render devtools. Drop-in `<JsonRenderDevtools />` component.
|
||||
|
||||
+3
-4
@@ -1,7 +1,6 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata"
|
||||
export const metadata = pageMetadata("docs/api/devtools-solid")
|
||||
|
||||
# @json-render/devtools-solid
|
||||
---
|
||||
title: "@json-render/devtools-solid"
|
||||
---
|
||||
|
||||
SolidJS adapter for the json-render devtools. Drop-in `<JsonRenderDevtools />` component.
|
||||
|
||||
+3
-4
@@ -1,7 +1,6 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata"
|
||||
export const metadata = pageMetadata("docs/api/devtools-svelte")
|
||||
|
||||
# @json-render/devtools-svelte
|
||||
---
|
||||
title: "@json-render/devtools-svelte"
|
||||
---
|
||||
|
||||
Svelte adapter for the json-render devtools. Drop-in `<JsonRenderDevtools />` component.
|
||||
|
||||
+3
-4
@@ -1,7 +1,6 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata"
|
||||
export const metadata = pageMetadata("docs/api/devtools-vue")
|
||||
|
||||
# @json-render/devtools-vue
|
||||
---
|
||||
title: "@json-render/devtools-vue"
|
||||
---
|
||||
|
||||
Vue adapter for the json-render devtools. Drop-in `<JsonRenderDevtools />` component.
|
||||
|
||||
+3
-4
@@ -1,7 +1,6 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata"
|
||||
export const metadata = pageMetadata("docs/api/devtools")
|
||||
|
||||
# @json-render/devtools
|
||||
---
|
||||
title: "@json-render/devtools"
|
||||
---
|
||||
|
||||
Framework-agnostic core for the json-render devtools — vanilla TS panel UI, event store, DOM picker, and stream tap utilities. Every framework-specific adapter package depends on this.
|
||||
|
||||
+3
-4
@@ -1,7 +1,6 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata"
|
||||
export const metadata = pageMetadata("docs/api/directives")
|
||||
|
||||
# @json-render/directives
|
||||
---
|
||||
title: "@json-render/directives"
|
||||
---
|
||||
|
||||
Pre-built custom directives for `@json-render/core`. Drop them into your catalog and renderer to add formatting, math, string manipulation, and i18n.
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata"
|
||||
export const metadata = pageMetadata("docs/api/image")
|
||||
|
||||
# @json-render/image
|
||||
---
|
||||
title: "@json-render/image"
|
||||
---
|
||||
|
||||
Image renderer. Turn JSON specs into SVG and PNG images using [Satori](https://github.com/vercel/satori).
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata"
|
||||
export const metadata = pageMetadata("docs/api/ink")
|
||||
|
||||
# @json-render/ink
|
||||
---
|
||||
title: "@json-render/ink"
|
||||
---
|
||||
|
||||
Terminal renderer for [Ink](https://github.com/vadimdemedes/ink) with multiple standard components, providers, hooks, and streaming support.
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata"
|
||||
export const metadata = pageMetadata("docs/api/jotai")
|
||||
|
||||
# @json-render/jotai
|
||||
---
|
||||
title: "@json-render/jotai"
|
||||
---
|
||||
|
||||
Jotai adapter for json-render's `StateStore` interface.
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata"
|
||||
export const metadata = pageMetadata("docs/api/mcp")
|
||||
|
||||
# @json-render/mcp
|
||||
---
|
||||
title: "@json-render/mcp"
|
||||
---
|
||||
|
||||
MCP Apps integration for json-render. Serve json-render UIs as interactive [MCP Apps](https://modelcontextprotocol.io/docs/extensions/apps) inside Claude, ChatGPT, Cursor, VS Code, and other MCP-capable clients.
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"title": "API Reference",
|
||||
"pages": [
|
||||
"core",
|
||||
"react",
|
||||
"next",
|
||||
"tanstack-start",
|
||||
"react-pdf",
|
||||
"react-email",
|
||||
"shadcn",
|
||||
"shadcn-svelte",
|
||||
"react-native",
|
||||
"image",
|
||||
"remotion",
|
||||
"ink",
|
||||
"vue",
|
||||
"svelte",
|
||||
"solid",
|
||||
"react-three-fiber",
|
||||
"directives",
|
||||
"codegen",
|
||||
"devtools",
|
||||
"devtools-react",
|
||||
"devtools-vue",
|
||||
"devtools-svelte",
|
||||
"devtools-solid",
|
||||
"mcp",
|
||||
"redux",
|
||||
"zustand",
|
||||
"jotai",
|
||||
"xstate",
|
||||
"yaml"
|
||||
]
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata"
|
||||
export const metadata = pageMetadata("docs/api/next")
|
||||
|
||||
# @json-render/next
|
||||
---
|
||||
title: "@json-render/next"
|
||||
---
|
||||
|
||||
Next.js renderer. JSON becomes full Next.js applications with routes, layouts, metadata, and SSR.
|
||||
|
||||
+3
-4
@@ -1,7 +1,6 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata"
|
||||
export const metadata = pageMetadata("docs/api/react-email")
|
||||
|
||||
# @json-render/react-email
|
||||
---
|
||||
title: "@json-render/react-email"
|
||||
---
|
||||
|
||||
React Email renderer. Turn JSON specs into HTML or plain-text emails using `@react-email/components` and `@react-email/render`.
|
||||
|
||||
+3
-4
@@ -1,7 +1,6 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata"
|
||||
export const metadata = pageMetadata("docs/api/react-native")
|
||||
|
||||
# @json-render/react-native
|
||||
---
|
||||
title: "@json-render/react-native"
|
||||
---
|
||||
|
||||
React Native renderer with standard components, providers, and hooks.
|
||||
|
||||
+3
-4
@@ -1,7 +1,6 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata"
|
||||
export const metadata = pageMetadata("docs/api/react-pdf")
|
||||
|
||||
# @json-render/react-pdf
|
||||
---
|
||||
title: "@json-render/react-pdf"
|
||||
---
|
||||
|
||||
PDF document renderer. Turn JSON specs into PDFs using `@react-pdf/renderer`.
|
||||
|
||||
+3
-4
@@ -1,7 +1,6 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata"
|
||||
export const metadata = pageMetadata("docs/api/react-three-fiber")
|
||||
|
||||
# @json-render/react-three-fiber
|
||||
---
|
||||
title: "@json-render/react-three-fiber"
|
||||
---
|
||||
|
||||
React Three Fiber renderer for json-render. 20 built-in 3D components for meshes, lights, models, gaussian splats, environments, text, cameras, and controls.
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata"
|
||||
export const metadata = pageMetadata("docs/api/react")
|
||||
|
||||
# @json-render/react
|
||||
---
|
||||
title: "@json-render/react"
|
||||
---
|
||||
|
||||
React components, providers, and hooks.
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata"
|
||||
export const metadata = pageMetadata("docs/api/redux")
|
||||
|
||||
# @json-render/redux
|
||||
---
|
||||
title: "@json-render/redux"
|
||||
---
|
||||
|
||||
Redux / Redux Toolkit adapter for json-render's `StateStore` interface.
|
||||
|
||||
+3
-4
@@ -1,7 +1,6 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata"
|
||||
export const metadata = pageMetadata("docs/api/remotion")
|
||||
|
||||
# @json-render/remotion
|
||||
---
|
||||
title: "@json-render/remotion"
|
||||
---
|
||||
|
||||
Remotion video renderer. Turn JSON timeline specs into video compositions.
|
||||
|
||||
+3
-4
@@ -1,7 +1,6 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata"
|
||||
export const metadata = pageMetadata("docs/api/shadcn-svelte")
|
||||
|
||||
# @json-render/shadcn-svelte
|
||||
---
|
||||
title: "@json-render/shadcn-svelte"
|
||||
---
|
||||
|
||||
Pre-built [shadcn-svelte](https://www.shadcn-svelte.com/) components for json-render. 36 components built on Svelte 5 + Tailwind CSS, ready to use with `defineCatalog` and `defineRegistry`.
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata"
|
||||
export const metadata = pageMetadata("docs/api/shadcn")
|
||||
|
||||
# @json-render/shadcn
|
||||
---
|
||||
title: "@json-render/shadcn"
|
||||
---
|
||||
|
||||
Pre-built [shadcn/ui](https://ui.shadcn.com/) components for json-render. 36 components built on Radix UI + Tailwind CSS, ready to use with `defineCatalog` and `defineRegistry`.
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata";
|
||||
export const metadata = pageMetadata("docs/api/solid");
|
||||
|
||||
# @json-render/solid
|
||||
---
|
||||
title: "@json-render/solid"
|
||||
---
|
||||
|
||||
SolidJS components, providers, and hooks for rendering json-render specs.
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata"
|
||||
export const metadata = pageMetadata("docs/api/svelte")
|
||||
|
||||
# @json-render/svelte
|
||||
---
|
||||
title: "@json-render/svelte"
|
||||
---
|
||||
|
||||
Svelte 5 components, providers, and helpers for rendering json-render specs.
|
||||
|
||||
+3
-5
@@ -1,8 +1,6 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata";
|
||||
|
||||
export const metadata = pageMetadata("docs/api/tanstack-start");
|
||||
|
||||
# @json-render/tanstack-start
|
||||
---
|
||||
title: "@json-render/tanstack-start"
|
||||
---
|
||||
|
||||
TanStack Start renderer for JSON-defined applications with routes, layouts,
|
||||
head metadata, SSR loaders, prerender paths, and client navigation.
|
||||
@@ -1,7 +1,6 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata"
|
||||
export const metadata = pageMetadata("docs/api/vue")
|
||||
|
||||
# @json-render/vue
|
||||
---
|
||||
title: "@json-render/vue"
|
||||
---
|
||||
|
||||
Vue 3 components, providers, and composables.
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata"
|
||||
export const metadata = pageMetadata("docs/api/xstate")
|
||||
|
||||
# @json-render/xstate
|
||||
---
|
||||
title: "@json-render/xstate"
|
||||
---
|
||||
|
||||
[XState Store](https://stately.ai/docs/xstate-store) adapter for json-render's `StateStore` interface.
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata"
|
||||
export const metadata = pageMetadata("docs/api/yaml")
|
||||
|
||||
# @json-render/yaml
|
||||
---
|
||||
title: "@json-render/yaml"
|
||||
---
|
||||
|
||||
YAML wire format for json-render. Progressive rendering and surgical edits via streaming YAML.
|
||||
|
||||
+3
-4
@@ -1,7 +1,6 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata"
|
||||
export const metadata = pageMetadata("docs/api/zustand")
|
||||
|
||||
# @json-render/zustand
|
||||
---
|
||||
title: "@json-render/zustand"
|
||||
---
|
||||
|
||||
Zustand adapter for json-render's `StateStore` interface.
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata";
|
||||
export const metadata = pageMetadata("docs/catalog");
|
||||
|
||||
# Catalog
|
||||
---
|
||||
title: "Catalog"
|
||||
---
|
||||
|
||||
The catalog defines what AI can generate. It's your guardrail.
|
||||
|
||||
@@ -1,10 +1,35 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata"
|
||||
export const metadata = pageMetadata("docs/changelog")
|
||||
|
||||
# Changelog
|
||||
---
|
||||
title: "Changelog"
|
||||
---
|
||||
|
||||
Notable changes and updates to json-render.
|
||||
|
||||
## v0.21.0
|
||||
|
||||
September 18, 2026
|
||||
|
||||
### New: TanStack Start Renderer
|
||||
|
||||
Added `@json-render/tanstack-start` for JSON-defined applications with file-based routes, reusable layouts, SSR loaders, head metadata, prerender paths, client navigation, and route fallbacks.
|
||||
|
||||
See the [TanStack Start API reference](/docs/api/tanstack-start) for setup and route configuration.
|
||||
|
||||
### New: Experimental Jev Composition
|
||||
|
||||
Added `experimental_composeSpec` and `experimental_createEvaluator` to `@json-render/core`. Apps can provide their own catalogs and bounded component candidates, then stream validated compositions through an evaluation model. The playground now includes a Jev model option and supports iterative composition edits.
|
||||
|
||||
These APIs are experimental and may change in any release. See the [Jev guide](/docs/jev) for the source-build workflow, examples, and current limitations.
|
||||
|
||||
### Improved: Vue Named Slots
|
||||
|
||||
Vue registries now support catalog-declared named slots alongside the default `children` slot.
|
||||
|
||||
### Fixed: React Streaming Stability
|
||||
|
||||
Stabilized streamed React renders and added coverage for incomplete streamed props and nested prop identities.
|
||||
|
||||
---
|
||||
|
||||
## v0.20.0
|
||||
|
||||
August 15, 2026
|
||||
+3
-4
@@ -1,7 +1,6 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata"
|
||||
export const metadata = pageMetadata("docs/code-export")
|
||||
|
||||
# Code Export
|
||||
---
|
||||
title: "Code Export"
|
||||
---
|
||||
|
||||
Export generated UI as standalone code for your framework.
|
||||
|
||||
+3
-4
@@ -1,7 +1,6 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata"
|
||||
export const metadata = pageMetadata("docs/computed-values")
|
||||
|
||||
# Computed Values
|
||||
---
|
||||
title: "Computed Values"
|
||||
---
|
||||
|
||||
Derive dynamic prop values using registered functions or string templates.
|
||||
|
||||
+3
-4
@@ -1,7 +1,6 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata"
|
||||
export const metadata = pageMetadata("docs/custom-schema")
|
||||
|
||||
# Custom Schema & Renderer
|
||||
---
|
||||
title: "Custom Schema & Renderer"
|
||||
---
|
||||
|
||||
Build your own schema and renderer with `@json-render/core`.
|
||||
|
||||
+3
-4
@@ -1,7 +1,6 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata"
|
||||
export const metadata = pageMetadata("docs/data-binding")
|
||||
|
||||
# Data Binding
|
||||
---
|
||||
title: "Data Binding"
|
||||
---
|
||||
|
||||
Connect UI elements to dynamic data using expressions in your JSON specs.
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata"
|
||||
export const metadata = pageMetadata("docs/devtools")
|
||||
|
||||
# Devtools
|
||||
---
|
||||
title: "Devtools"
|
||||
---
|
||||
|
||||
A drop-in inspector panel for any json-render app. See the spec tree, edit state inline, watch dispatched actions, follow stream patches live, browse your catalog, and pick DOM elements to map them back to spec keys.
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata"
|
||||
export const metadata = pageMetadata("docs/directives")
|
||||
|
||||
# Directives
|
||||
---
|
||||
title: "Directives"
|
||||
---
|
||||
|
||||
Extend the spec language with custom `$`-prefixed dynamic values. Directives let you add formatting, math, string manipulation, i18n, and any other transformation without modifying core.
|
||||
|
||||
+3
-4
@@ -1,7 +1,6 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata"
|
||||
export const metadata = pageMetadata("docs/generation-modes")
|
||||
|
||||
# Generation Modes
|
||||
---
|
||||
title: "Generation Modes"
|
||||
---
|
||||
|
||||
json-render supports two modes for AI-generated UI: **Standalone mode** for standalone UI and **Inline mode** for inline UI within a conversation.
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata"
|
||||
export const metadata = pageMetadata("docs")
|
||||
|
||||
# Introduction
|
||||
---
|
||||
title: "Introduction"
|
||||
---
|
||||
|
||||
json-render is a framework for **Generative UI** — AI-generated interfaces that are safe, predictable, and render natively on any platform.
|
||||
|
||||
+3
-4
@@ -1,7 +1,6 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata"
|
||||
export const metadata = pageMetadata("docs/installation")
|
||||
|
||||
# Installation
|
||||
---
|
||||
title: "Installation"
|
||||
---
|
||||
|
||||
Install the core package plus your renderer of choice.
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata"
|
||||
export const metadata = pageMetadata("docs/jev")
|
||||
|
||||
# Jev (Experimental)
|
||||
|
||||
---
|
||||
title: "Jev (Experimental)"
|
||||
---
|
||||
**Experimental:** `experimental_composeSpec` and `experimental_createEvaluator` are reusable APIs in `@json-render/core`. Like AI SDK's experimental APIs, names prefixed with `experimental_` or `Experimental_` may change in any release. Pin exact package versions (no `^` or `~`) and review release notes before upgrading.
|
||||
|
||||
**Availability:** these APIs are unreleased. You can try the source build below before they appear in a published npm version.
|
||||
@@ -151,8 +149,6 @@ Initialize your renderer's state from `spec.state`. Keep user interaction disabl
|
||||
|
||||
On `complete`, inspect `stopReason`: `finish` means composition finished; `unavailable` means the evaluator could not fulfill the request; `limit` means a call, element, or depth budget prevented completion. A complete event can contain a partial spec, or `null` when no root was added. Completion is not a correctness guarantee. Errors and cancellation throw; retain the last snapshot and label it incomplete. Each batched trace is one evaluation (`select` or `layout`), with the individual choices in `step.answers` and usage/timing counted once.
|
||||
|
||||
To avoid visible rearrangement, the playground waits for a successful `complete` event before revealing a new tree in one render. Decision metadata still streams while composing. Follow-up edits continue to update the existing preview incrementally. Apps can choose the same display policy while retaining the core API's provisional snapshots for inspection.
|
||||
|
||||
The playground is a reference implementation: [candidates and server wrapper](https://github.com/vercel-labs/json-render/tree/main/apps/web/lib/jev), [streaming route](https://github.com/vercel-labs/json-render/blob/main/apps/web/app/api/generate/route.ts), and [client](https://github.com/vercel-labs/json-render/blob/main/apps/web/components/playground.tsx).
|
||||
|
||||
## Validation and v1 limits
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"title": "Documentation",
|
||||
"pages": [
|
||||
"---Getting Started---",
|
||||
"index",
|
||||
"installation",
|
||||
"quick-start",
|
||||
"skills",
|
||||
"migration",
|
||||
"changelog",
|
||||
"---Core---",
|
||||
"specs",
|
||||
"schemas",
|
||||
"catalog",
|
||||
"data-binding",
|
||||
"computed-values",
|
||||
"visibility",
|
||||
"watchers",
|
||||
"validation",
|
||||
"directives",
|
||||
"---Rendering---",
|
||||
"renderers",
|
||||
"registry",
|
||||
"streaming",
|
||||
"generation-modes",
|
||||
"---Examples---",
|
||||
"[Browse All Examples](/examples)",
|
||||
"---Guides---",
|
||||
"custom-schema",
|
||||
"code-export",
|
||||
"devtools",
|
||||
"---Experimental---",
|
||||
"jev",
|
||||
"---Integrations---",
|
||||
"ai-sdk",
|
||||
"a2ui",
|
||||
"adaptive-cards",
|
||||
"ag-ui",
|
||||
"openapi",
|
||||
"api"
|
||||
]
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata"
|
||||
export const metadata = pageMetadata("docs/migration")
|
||||
|
||||
# Migration Guide
|
||||
---
|
||||
title: "Migration Guide"
|
||||
---
|
||||
|
||||
This guide covers breaking changes introduced in v0.6.0 and how to update your code.
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata"
|
||||
export const metadata = pageMetadata("docs/openapi")
|
||||
|
||||
# OpenAPI Integration
|
||||
---
|
||||
title: "OpenAPI Integration"
|
||||
---
|
||||
|
||||
Use json-render to generate dynamic forms and UIs from [OpenAPI/Swagger](https://swagger.io/specification/) schemas.
|
||||
|
||||
+3
-4
@@ -1,7 +1,6 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata"
|
||||
export const metadata = pageMetadata("docs/quick-start")
|
||||
|
||||
# Quick Start
|
||||
---
|
||||
title: "Quick Start"
|
||||
---
|
||||
|
||||
Get up and running with json-render in 5 minutes.
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata";
|
||||
export const metadata = pageMetadata("docs/registry");
|
||||
|
||||
# Registry
|
||||
---
|
||||
title: "Registry"
|
||||
---
|
||||
|
||||
A registry maps your [catalog](/docs/catalog) definitions to platform-specific implementations. The catalog defines _what_ AI can generate; the registry provides the _how_.
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata";
|
||||
export const metadata = pageMetadata("docs/renderers");
|
||||
|
||||
# Renderers
|
||||
---
|
||||
title: "Renderers"
|
||||
---
|
||||
|
||||
json-render supports multiple output targets. Each renderer takes the same core concept -- a JSON spec constrained to a catalog -- and renders it natively on a different platform or into a different format.
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata"
|
||||
export const metadata = pageMetadata("docs/schemas")
|
||||
|
||||
# Schemas
|
||||
---
|
||||
title: "Schemas"
|
||||
---
|
||||
|
||||
Schemas define the structure and validation rules for your UI specs.
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata";
|
||||
|
||||
export const metadata = pageMetadata("docs/skills");
|
||||
|
||||
# Skills
|
||||
---
|
||||
title: "Skills"
|
||||
---
|
||||
|
||||
json-render ships with skills that teach AI coding agents how to use each package. Install a skill and your agent in Cursor, Claude Code, or Codex can generate json-render UIs without manual guidance.
|
||||
|
||||
@@ -11,6 +9,11 @@ json-render ships with skills that teach AI coding agents how to use each packag
|
||||
- **core** — Core schemas, catalogs, and AI prompt generation.
|
||||
- **react** — React renderer that turns JSON specs into React component trees.
|
||||
- **tanstack-start** — Full TanStack Start applications with routes, layouts, SSR loaders, and head metadata.
|
||||
- **devtools** — Framework-agnostic inspector panel for specs, state, actions, streams, catalogs, and DOM picking.
|
||||
- **devtools-react** — React adapter for the json-render devtools panel.
|
||||
- **devtools-vue** — Vue adapter for the json-render devtools panel.
|
||||
- **devtools-svelte** — Svelte adapter for the json-render devtools panel.
|
||||
- **devtools-solid** — SolidJS adapter for the json-render devtools panel.
|
||||
- **react-pdf** — PDF renderer using `@react-pdf/renderer`.
|
||||
- **react-email** — Email renderer that produces HTML or plain-text emails.
|
||||
- **react-native** — React Native renderer for native mobile UIs.
|
||||
@@ -33,6 +36,11 @@ json-render ships with skills that teach AI coding agents how to use each packag
|
||||
npx skills add vercel-labs/json-render --skill core
|
||||
npx skills add vercel-labs/json-render --skill react
|
||||
npx skills add vercel-labs/json-render --skill tanstack-start
|
||||
npx skills add vercel-labs/json-render --skill devtools
|
||||
npx skills add vercel-labs/json-render --skill devtools-react
|
||||
npx skills add vercel-labs/json-render --skill devtools-vue
|
||||
npx skills add vercel-labs/json-render --skill devtools-svelte
|
||||
npx skills add vercel-labs/json-render --skill devtools-solid
|
||||
npx skills add vercel-labs/json-render --skill react-pdf
|
||||
npx skills add vercel-labs/json-render --skill react-email
|
||||
npx skills add vercel-labs/json-render --skill react-native
|
||||
@@ -64,6 +72,26 @@ Teaches agents how to render JSON specs as React component trees using `JsonRend
|
||||
|
||||
Teaches agents how to build JSON-defined TanStack Start applications with splat routes, reusable layouts, SSR-safe loaders, head metadata, prerender paths, and client navigation.
|
||||
|
||||
## devtools
|
||||
|
||||
Teaches agents how to add and configure the framework-agnostic json-render devtools panel, including spec inspection, state editing, action and stream timelines, catalog browsing, and DOM picking.
|
||||
|
||||
## devtools-react
|
||||
|
||||
Teaches agents how to mount and control `@json-render/devtools-react` inside a React json-render provider, including the imperative devtools hook.
|
||||
|
||||
## devtools-vue
|
||||
|
||||
Teaches agents how to mount and configure `@json-render/devtools-vue` inside a Vue json-render provider.
|
||||
|
||||
## devtools-svelte
|
||||
|
||||
Teaches agents how to mount and configure `@json-render/devtools-svelte` inside a Svelte 5 json-render provider.
|
||||
|
||||
## devtools-solid
|
||||
|
||||
Teaches agents how to mount and configure `@json-render/devtools-solid` inside a SolidJS json-render provider.
|
||||
|
||||
## react-pdf
|
||||
|
||||
Teaches agents how to generate PDFs from JSON specs using `@react-pdf/renderer`. Covers the PDF-specific component registry, page layout, and styling.
|
||||
@@ -1,7 +1,6 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata";
|
||||
export const metadata = pageMetadata("docs/specs");
|
||||
|
||||
# Specs
|
||||
---
|
||||
title: "Specs"
|
||||
---
|
||||
|
||||
A spec is a JSON document that describes your UI.
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata"
|
||||
export const metadata = pageMetadata("docs/streaming")
|
||||
|
||||
# Streaming
|
||||
---
|
||||
title: "Streaming"
|
||||
---
|
||||
|
||||
Progressively render UI as AI generates it.
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata"
|
||||
export const metadata = pageMetadata("docs/validation")
|
||||
|
||||
# Validation
|
||||
---
|
||||
title: "Validation"
|
||||
---
|
||||
|
||||
Validate form inputs with built-in and custom functions.
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata"
|
||||
export const metadata = pageMetadata("docs/visibility")
|
||||
|
||||
# Visibility
|
||||
---
|
||||
title: "Visibility"
|
||||
---
|
||||
|
||||
Conditionally show or hide components based on state values and logic.
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { pageMetadata } from "@/lib/page-metadata"
|
||||
export const metadata = pageMetadata("docs/watchers")
|
||||
|
||||
# Watchers
|
||||
---
|
||||
title: "Watchers"
|
||||
---
|
||||
|
||||
React to state changes by triggering actions when watched paths update.
|
||||
|
||||
@@ -2,6 +2,7 @@ import { nextJsConfig } from "@internal/eslint-config/next-js";
|
||||
|
||||
/** @type {import("eslint").Linter.Config[]} */
|
||||
export default [
|
||||
{ ignores: [".source/**"] },
|
||||
...nextJsConfig,
|
||||
{
|
||||
rules: {
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
const negotiationHeaders = [
|
||||
"Accept",
|
||||
"User-Agent",
|
||||
"Signature-Agent",
|
||||
"Sec-Fetch-Mode",
|
||||
"Sec-Fetch-Dest",
|
||||
"RSC",
|
||||
"Next-Router-Prefetch",
|
||||
"Next-Router-Segment-Prefetch",
|
||||
"Purpose",
|
||||
"Sec-Purpose",
|
||||
];
|
||||
|
||||
export function applyDocsResponseHeaders(headers: Headers) {
|
||||
const tokens = new Map<string, string>();
|
||||
for (const token of [
|
||||
...(headers.get("Vary") ?? "").split(/\s*,\s*/),
|
||||
...negotiationHeaders,
|
||||
]) {
|
||||
if (token) tokens.set(token.toLowerCase(), token);
|
||||
}
|
||||
headers.set("Vary", [...tokens.values()].join(", "));
|
||||
headers.set("Cache-Control", "private, no-store");
|
||||
headers.set("CDN-Cache-Control", "no-store");
|
||||
headers.set("Vercel-CDN-Cache-Control", "no-store");
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { parse } from "yaml";
|
||||
import { allDocsPages } from "./docs-navigation";
|
||||
import { mdxToCleanMarkdown } from "./mdx-to-markdown";
|
||||
import { siteUrl } from "./site";
|
||||
|
||||
export const docsPages = allDocsPages.filter(
|
||||
(page) => page.href === "/docs" || page.href.startsWith("/docs/"),
|
||||
);
|
||||
const inventory = new Map(docsPages.map((page) => [page.href, page]));
|
||||
const pending = new Map<string, Promise<DocsSource>>();
|
||||
|
||||
export type DocsSource = {
|
||||
href: string;
|
||||
title: string;
|
||||
markdown: string;
|
||||
markdownUrl: string;
|
||||
canonicalUrl: string;
|
||||
};
|
||||
|
||||
export function isSafePathSegments(segments: readonly string[]) {
|
||||
return segments.every(
|
||||
(part) =>
|
||||
part.length > 0 &&
|
||||
part !== "." &&
|
||||
part !== ".." &&
|
||||
!part.includes("/") &&
|
||||
!part.includes("\\"),
|
||||
);
|
||||
}
|
||||
|
||||
export function loadDocsSource(pathname: string): Promise<DocsSource> | null {
|
||||
const href = pathname.endsWith("/") ? pathname.slice(0, -1) : pathname;
|
||||
if (!inventory.has(href)) return null;
|
||||
let result = pending.get(href);
|
||||
if (!result) {
|
||||
const slug = href === "/docs" ? "index" : href.slice("/docs/".length);
|
||||
result = readFile(
|
||||
join(process.cwd(), "content", "docs", `${slug}.mdx`),
|
||||
"utf8",
|
||||
).then((raw) => {
|
||||
const frontmatter = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n/);
|
||||
if (!frontmatter) throw new Error(`Missing frontmatter for ${href}`);
|
||||
const metadata = parse(frontmatter[1] ?? "") as {
|
||||
title?: unknown;
|
||||
} | null;
|
||||
if (typeof metadata?.title !== "string")
|
||||
throw new Error(`Missing title for ${href}`);
|
||||
const title = metadata.title;
|
||||
return {
|
||||
href,
|
||||
title,
|
||||
markdown: mdxToCleanMarkdown(
|
||||
`# ${title}\n${raw.slice(frontmatter[0].length)}`,
|
||||
),
|
||||
markdownUrl: `${href}.md`,
|
||||
canonicalUrl: `${siteUrl}${href}`,
|
||||
};
|
||||
});
|
||||
pending.set(href, result);
|
||||
result.catch(() => pending.delete(href));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function loadAllDocsSources() {
|
||||
return Promise.all(docsPages.map((page) => loadDocsSource(page.href)!));
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { defineConfig } from "@vercel/geistdocs/config";
|
||||
import { siteUrl } from "@/lib/site";
|
||||
|
||||
export const config = defineConfig({
|
||||
title: "json-render",
|
||||
siteUrl,
|
||||
defaultLanguage: "en",
|
||||
logo: <span className="font-medium">json-render</span>,
|
||||
navbarActiveProduct: "json-render",
|
||||
navbarBrand: "labs",
|
||||
github: {
|
||||
owner: "vercel-labs",
|
||||
repo: "json-render",
|
||||
branch: "main",
|
||||
editPath: "apps/web/content/docs",
|
||||
},
|
||||
content: [
|
||||
{ id: "docs", label: "Documentation", dir: "content/docs", route: "/docs" },
|
||||
],
|
||||
nav: [
|
||||
{ label: "Docs", href: "/docs" },
|
||||
{ label: "Playground", href: "/playground" },
|
||||
{ label: "Examples", href: "/examples" },
|
||||
],
|
||||
ai: { enabled: false },
|
||||
feedback: { enabled: false },
|
||||
language: { enabled: false },
|
||||
pageActions: { askAI: false, openInChat: false },
|
||||
webmcp: { enabled: true },
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
import {
|
||||
createSource,
|
||||
type GeistdocsSourceBundle,
|
||||
} from "@vercel/geistdocs/source";
|
||||
import { docs } from "@/.source/server";
|
||||
import { loadDocsSource } from "@/lib/docs-source";
|
||||
import { config } from "./config";
|
||||
|
||||
const bundle = createSource({ docs, config, baseUrl: "/docs" });
|
||||
|
||||
export const geistdocsSource: GeistdocsSourceBundle = {
|
||||
...bundle,
|
||||
async getPageMarkdown(page) {
|
||||
const source = await loadDocsSource(
|
||||
`/docs${page.slugs.length ? `/${page.slugs.join("/")}` : ""}`,
|
||||
);
|
||||
if (!source) throw new Error(`Documentation source not found: ${page.url}`);
|
||||
return source.markdown;
|
||||
},
|
||||
};
|
||||
@@ -21,10 +21,10 @@ Select Jev, choose Create account settings, and send the request. Edit the name,
|
||||
Jev exposes Choice, Boolean, and Score outputs. It does not produce free-form JSON or prose. We express new UI construction as two batches of finite choices:
|
||||
|
||||
1. Offer the root and independent component membership questions in one evaluation. Exclusive resource variants share a question; reusable recipes get bounded counts. Candidate values include state/action bindings owned by the app.
|
||||
2. Assemble and validate the selected content. The core API emits a provisional snapshot in catalog order and the root's default slot; the playground streams its decision metadata but waits to display new content. Root selection takes precedence over speculative membership for the same recipe/resource.
|
||||
3. Ask final parent slots and sibling positions in a second evaluation against the actual selected set. Validate the combined tree, including depth and cycles, before revealing the finished spec in one JSON edit message. Equal positions retain catalog order. A single root or one child in a single slot needs no second call. No separate finish call is needed.
|
||||
2. Assemble and validate the selected content, then stream a preview immediately. This preview uses catalog order and the root's default slot. Root selection takes precedence over speculative membership for the same recipe/resource.
|
||||
3. Ask final parent slots and sibling positions in a second evaluation against the actual selected set. Validate the combined tree, including depth and cycles, before streaming it. Equal positions retain catalog order. A single root or one child in a single slot needs no second call. No separate finish call is needed.
|
||||
4. On follow-ups, use the selected spec with the sequential edit protocol: add, replace, remove, or move/reorder. Replacements and moves select a target, then choose a valid recipe or destination in a second evaluation. Preserve unchanged elements and earlier versions.
|
||||
5. Each trace represents one evaluation. Batched traces use `select`/`layout` with the independent decisions in `answers`; timing and usage are counted once per call. Provider errors or invalid combined layouts report failure. New trees remain hidden until successful completion; follow-up edits retain their last valid preview.
|
||||
5. Each trace represents one evaluation. Batched traces use `select`/`layout` with the independent decisions in `answers`; timing and usage are counted once per call. Provider errors or invalid combined layouts preserve the last valid preview and report failure.
|
||||
|
||||
There are **no complete UI templates** and no generative-model calls. The example prompt buttons only populate the request text. Jev chooses which elements to include, their order, grouping, and which offered action bindings to use. The registry owns appearance and behavior.
|
||||
|
||||
@@ -48,7 +48,7 @@ The composer validates tree structure and candidate values; it does not guarante
|
||||
|
||||
Name required sections explicitly. For example, request an orders table at the top, revenue/orders/customer metrics in a row, then a weekly revenue chart. The shorter request "a dashboard with the table at the top" can select only a table. Follow-up requests can move an existing table without reconstructing its data.
|
||||
|
||||
The code bounds new batches to 14 elements, each request to 14 evaluation calls, nesting depth four, ten seconds per provider request, and 55 seconds overall. The selected seed may contain up to 100 elements. A limit, cancellation, or error retains the current preview for edits and labels the result partial; new trees remain hidden. The shared endpoint uses the web app's request rate limiters. Both models edit the selected version; Clear starts fresh. The stream tab exposes construction decisions alongside spec patches. Provider calls and spec assembly never execute the selected UI actions.
|
||||
The code bounds new batches to 14 elements, each request to 14 evaluation calls, nesting depth four, ten seconds per provider request, and 55 seconds overall. The selected seed may contain up to 100 elements. A limit, cancellation, or error retains the current preview and labels it partial. The shared endpoint uses the web app's request rate limiters. Both models edit the selected version; Clear starts fresh. The stream tab exposes construction decisions alongside spec patches. Provider calls and spec assembly never execute the selected UI actions.
|
||||
|
||||
Try `Design a user profile card`, then `Remove the bio` or `Make the avatar smaller`. For settings, try `Remove the email notifications switch`, `Change the heading to "Account settings"`, or `Move the email field above the name field`. The server shares existing display labels and matching candidate descriptions to identify edit targets, without sharing raw state or entered field values. Existing specs must use the supported expression subset and form a valid tree. Edits retain state from the selected spec, as in the default model flow; interactive preview state is not saved into version history.
|
||||
|
||||
@@ -64,7 +64,7 @@ The server uses Gateway's experimental v4 evaluation transport with model `types
|
||||
- `packages/core/src/experimental-evaluator.ts`: public Gateway evaluator adapter.
|
||||
- `compose.ts`: public API consumer with playground instructions and cost display.
|
||||
- `../../app/api/generate/route.ts`: shared rate-limited endpoint, dispatching the selected model.
|
||||
- `response.ts`: reveals finished new trees atomically using the JSON edit protocol, streams follow-up spec patches, and preserves decision metadata.
|
||||
- `response.ts`: adapts composition snapshots into the playground's JSONL spec patches and decision metadata.
|
||||
- `../../components/playground.tsx`: shared model toggle, experimental info tooltip, prompt, version history, live preview, and inspectors.
|
||||
- `compose.test.ts`: structure, action boundaries, unknown usage, cancellation, and limits.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @vitest-environment node
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { deepMergeSpec, type Spec } from "@json-render/core";
|
||||
import { type JsonPatch, type Spec } from "@json-render/core";
|
||||
import { applySpecPatch } from "../spec-patch";
|
||||
import { createCompositionResponse } from "./response";
|
||||
import { composeUI } from "./compose";
|
||||
@@ -58,7 +58,7 @@ describe("playground composition response", () => {
|
||||
expect(initialSpec.elements.card!.props.title).toBe("Before");
|
||||
});
|
||||
|
||||
it("reveals a new tree atomically after layout, preserving every decision", async () => {
|
||||
it("adapts snapshots to the existing patch stream, including the final decision", async () => {
|
||||
vi.stubEnv("JEV_AI_GATEWAY_API_KEY", "test");
|
||||
const spec: Spec = {
|
||||
root: "card",
|
||||
@@ -76,31 +76,12 @@ describe("playground composition response", () => {
|
||||
elapsedMs: 10,
|
||||
inputTokens: null,
|
||||
};
|
||||
const provisional: Spec = {
|
||||
...spec,
|
||||
elements: {
|
||||
card: { ...spec.elements.card!, props: { title: "Provisional" } },
|
||||
},
|
||||
};
|
||||
vi.mocked(composeUI).mockImplementation(async function* () {
|
||||
yield {
|
||||
type: "step",
|
||||
spec: provisional,
|
||||
step: { ...step, choice: "select" },
|
||||
};
|
||||
yield {
|
||||
type: "step",
|
||||
spec,
|
||||
step: { ...step, index: 1, choice: "layout" },
|
||||
};
|
||||
yield { type: "step", spec, step };
|
||||
yield {
|
||||
type: "complete",
|
||||
spec,
|
||||
steps: [
|
||||
{ ...step, choice: "select" },
|
||||
{ ...step, index: 1, choice: "layout" },
|
||||
{ ...step, index: 2, choice: "finish" },
|
||||
],
|
||||
steps: [step, { ...step, index: 1, choice: "finish" }],
|
||||
stopReason: "finish",
|
||||
elapsedMs: 20,
|
||||
inputTokens: null,
|
||||
@@ -115,59 +96,23 @@ describe("playground composition response", () => {
|
||||
.trim()
|
||||
.split("\n")
|
||||
.map((line) => JSON.parse(line));
|
||||
expect(lines.slice(0, 2).map((line) => line.__meta)).toEqual([
|
||||
"decision",
|
||||
"decision",
|
||||
]);
|
||||
const updates = lines.filter((line) => line.op || line.__json_edit);
|
||||
expect(updates).toEqual([{ __json_edit: true, ...spec }]);
|
||||
const { __json_edit, ...snapshot } = updates[0];
|
||||
expect(deepMergeSpec({ root: "", elements: {} }, snapshot)).toEqual(spec);
|
||||
let actual: Spec = { root: "", elements: {} };
|
||||
for (const line of lines)
|
||||
if (line.op) actual = applySpecPatch(actual, line as JsonPatch);
|
||||
expect(actual).toEqual(spec);
|
||||
expect(
|
||||
lines
|
||||
.filter((line) => line.__meta === "decision")
|
||||
.map((line) => line.choice),
|
||||
).toEqual(["select", "layout", "finish"]);
|
||||
).toEqual(["card", "finish"]);
|
||||
expect(lines.at(-1)).toMatchObject({
|
||||
__meta: "composition",
|
||||
stopReason: "finish",
|
||||
calls: 3,
|
||||
calls: 2,
|
||||
inputTokens: null,
|
||||
});
|
||||
});
|
||||
|
||||
it.each(["limit", "unavailable"] as const)(
|
||||
"does not reveal provisional content when creation ends with %s",
|
||||
async (stopReason) => {
|
||||
vi.stubEnv("JEV_AI_GATEWAY_API_KEY", "test");
|
||||
vi.mocked(composeUI).mockImplementation(async function* () {
|
||||
yield {
|
||||
type: "complete",
|
||||
spec: {
|
||||
root: "card",
|
||||
elements: { card: { type: "Card", props: {} } },
|
||||
},
|
||||
steps: [],
|
||||
stopReason,
|
||||
elapsedMs: 1,
|
||||
inputTokens: null,
|
||||
estimatedCostUsd: null,
|
||||
};
|
||||
});
|
||||
const response = createCompositionResponse(
|
||||
new Request("https://example.com/api/generate"),
|
||||
"Create a card",
|
||||
);
|
||||
const lines = (await response.text())
|
||||
.trim()
|
||||
.split("\n")
|
||||
.map((line) => JSON.parse(line));
|
||||
expect(lines).toEqual([
|
||||
expect.objectContaining({ __meta: "composition", stopReason }),
|
||||
]);
|
||||
},
|
||||
);
|
||||
|
||||
it("retains unavailable outcomes and sends failures in the shared protocol", async () => {
|
||||
vi.stubEnv("JEV_AI_GATEWAY_API_KEY", "test");
|
||||
vi.mocked(composeUI).mockImplementation(async function* () {
|
||||
|
||||
@@ -83,20 +83,11 @@ export function createCompositionResponse(
|
||||
)) {
|
||||
if (event.type === "error") throw new Error(event.message);
|
||||
if (event.type === "step") {
|
||||
// New trees have a provisional catalog order until layout finishes.
|
||||
// Stream its decision metadata without displaying a moving UI.
|
||||
if (initialSpec) sendSpec(event.spec);
|
||||
sendSpec(event.spec);
|
||||
send({ __meta: "decision", ...event.step });
|
||||
decisions++;
|
||||
} else {
|
||||
if (event.spec) {
|
||||
if (initialSpec) sendSpec(event.spec);
|
||||
else if (event.stopReason === "finish") {
|
||||
// The existing JSON edit protocol applies a full new tree in
|
||||
// one render, including its state and final child order.
|
||||
send({ __json_edit: true, ...event.spec });
|
||||
}
|
||||
}
|
||||
if (event.spec) sendSpec(event.spec);
|
||||
for (const step of event.steps.slice(decisions))
|
||||
send({ __meta: "decision", ...step });
|
||||
send({
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Metadata } from "next";
|
||||
import { PAGE_TITLES } from "./page-titles";
|
||||
import { isPreview, siteUrl } from "./site";
|
||||
|
||||
const DESCRIPTION =
|
||||
"The Generative UI framework. Generate dashboards, widgets, and apps from prompts — safely constrained to components you define.";
|
||||
@@ -14,7 +15,16 @@ export function pageMetadata(slug: string): Metadata {
|
||||
|
||||
return {
|
||||
title: displayTitle,
|
||||
description: DESCRIPTION,
|
||||
robots: { index: !isPreview, follow: !isPreview },
|
||||
alternates: {
|
||||
canonical: `/${slug}`,
|
||||
...(slug === "docs" || slug.startsWith("docs/")
|
||||
? { types: { "text/markdown": `/${slug}.md` } }
|
||||
: {}),
|
||||
},
|
||||
openGraph: {
|
||||
url: `${siteUrl}/${slug}`,
|
||||
type: "website",
|
||||
locale: "en_US",
|
||||
siteName: "json-render",
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
type MarkdownNode = {
|
||||
type: string;
|
||||
depth?: number;
|
||||
value?: string;
|
||||
children?: MarkdownNode[];
|
||||
data?: { hProperties?: Record<string, unknown> };
|
||||
};
|
||||
|
||||
function text(node: MarkdownNode): string {
|
||||
return node.children ? node.children.map(text).join("") : (node.value ?? "");
|
||||
}
|
||||
|
||||
export function remarkLegacyHeadings() {
|
||||
return function walk(node: MarkdownNode) {
|
||||
if (node.type === "heading" && (node.depth === 2 || node.depth === 3)) {
|
||||
node.data ??= {};
|
||||
node.data.hProperties ??= {};
|
||||
node.data.hProperties.id = text(node)
|
||||
.toLowerCase()
|
||||
.replace(/[^\w\s-]/g, "")
|
||||
.replace(/\s+/g, "-")
|
||||
.trim();
|
||||
}
|
||||
node.children?.forEach(walk);
|
||||
};
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
import { readFile } from "fs/promises";
|
||||
import { join } from "path";
|
||||
import { docsNavigation } from "./docs-navigation";
|
||||
import { mdxToCleanMarkdown } from "./mdx-to-markdown";
|
||||
import { loadDocsSource } from "./docs-source";
|
||||
|
||||
export type IndexEntry = {
|
||||
title: string;
|
||||
@@ -33,15 +31,6 @@ function stripMarkdown(md: string): string {
|
||||
);
|
||||
}
|
||||
|
||||
function mdxFileForSlug(slug: string): string {
|
||||
const docsRoot = join(process.cwd(), "app", "(main)", "docs");
|
||||
if (slug === "/docs") {
|
||||
return join(docsRoot, "page.mdx");
|
||||
}
|
||||
const rest = slug.replace(/^\/docs\/?/, "");
|
||||
return join(docsRoot, ...rest.split("/"), "page.mdx");
|
||||
}
|
||||
|
||||
export async function getSearchIndex(): Promise<IndexEntry[]> {
|
||||
if (cached) return cached;
|
||||
|
||||
@@ -51,9 +40,8 @@ export async function getSearchIndex(): Promise<IndexEntry[]> {
|
||||
for (const item of section.items) {
|
||||
if (item.external) continue;
|
||||
try {
|
||||
const raw = await readFile(mdxFileForSlug(item.href), "utf-8");
|
||||
const md = mdxToCleanMarkdown(raw);
|
||||
const content = stripMarkdown(md);
|
||||
const source = await loadDocsSource(item.href);
|
||||
const content = stripMarkdown(source?.markdown ?? "");
|
||||
entries.push({
|
||||
title: item.title,
|
||||
href: item.href,
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
export const siteUrl = "https://json-render.dev";
|
||||
export const siteDescription =
|
||||
"The Generative UI framework. Generate dashboards, widgets, and apps from prompts — safely constrained to components you define.";
|
||||
export const isPreview = Boolean(
|
||||
process.env.VERCEL_ENV && process.env.VERCEL_ENV !== "production",
|
||||
);
|
||||
+24
-2
@@ -1,9 +1,31 @@
|
||||
import createMDX from "@next/mdx";
|
||||
import process from "node:process";
|
||||
import { createGeistdocs } from "@vercel/geistdocs/next";
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
serverExternalPackages: ["bash-tool", "just-bash", "@mongodb-js/zstd"],
|
||||
pageExtensions: ["js", "jsx", "ts", "tsx", "md", "mdx"],
|
||||
skipProxyUrlNormalize: true,
|
||||
outputFileTracingIncludes: { "/*": ["./content/docs/**/*.mdx"] },
|
||||
async headers() {
|
||||
return process.env.VERCEL_ENV && process.env.VERCEL_ENV !== "production"
|
||||
? [
|
||||
{
|
||||
source: "/:path*",
|
||||
headers: [{ key: "X-Robots-Tag", value: "noindex, nofollow" }],
|
||||
},
|
||||
]
|
||||
: [];
|
||||
},
|
||||
async rewrites() {
|
||||
return {
|
||||
beforeFiles: [
|
||||
{ source: "/docs.md", destination: "/api/docs-md" },
|
||||
{ source: "/docs/index.md", destination: "/api/docs-md" },
|
||||
{ source: "/docs/:path*.md", destination: "/api/docs-md/:path*" },
|
||||
],
|
||||
};
|
||||
},
|
||||
async redirects() {
|
||||
return [
|
||||
{
|
||||
@@ -20,7 +42,7 @@ const nextConfig = {
|
||||
},
|
||||
};
|
||||
|
||||
const withMDX = createMDX({});
|
||||
const withMDX = createGeistdocs();
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const config = withMDX(nextConfig);
|
||||
|
||||
@@ -7,10 +7,13 @@
|
||||
"scripts": {
|
||||
"predev": "command -v portless >/dev/null 2>&1 || (echo '\\nportless is required but not installed. Run: npm i -g portless\\nSee: https://github.com/vercel-labs/portless\\n' && exit 1)",
|
||||
"dev": "portless json-render next dev --turbopack",
|
||||
"postinstall": "fumadocs-mdx",
|
||||
"prebuild": "fumadocs-mdx",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"test:routes": "node scripts/test-routes.mjs",
|
||||
"lint": "eslint --max-warnings 0",
|
||||
"check-types": "next typegen && tsc --noEmit"
|
||||
"check-types": "fumadocs-mdx && next typegen && tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/gateway": "^3.0.13",
|
||||
@@ -21,16 +24,14 @@
|
||||
"@json-render/devtools-react": "workspace:*",
|
||||
"@json-render/react": "workspace:*",
|
||||
"@json-render/yaml": "workspace:*",
|
||||
"@mdx-js/loader": "^3.1.1",
|
||||
"@mdx-js/mdx": "^3.1.1",
|
||||
"@mdx-js/react": "^3.1.1",
|
||||
"@next/mdx": "^16.1.6",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"@upstash/ratelimit": "^2.0.8",
|
||||
"@upstash/redis": "^1.36.1",
|
||||
"@vercel/analytics": "^1.6.1",
|
||||
"@vercel/geistdocs": "2.4.1",
|
||||
"@vercel/speed-insights": "^1.3.1",
|
||||
"@visual-json/react": "0.1.1",
|
||||
"ai": "^6.0.33",
|
||||
@@ -39,9 +40,11 @@
|
||||
"clsx": "^2.1.1",
|
||||
"diff": "^8.0.3",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"fumadocs-core": "16.2.2",
|
||||
"fumadocs-mdx": "14.0.4",
|
||||
"geist": "1.7.0",
|
||||
"lucide-react": "^0.562.0",
|
||||
"next": "16.1.1",
|
||||
"next": "16.3.4",
|
||||
"next-themes": "^0.4.6",
|
||||
"radix-ui": "^1.4.3",
|
||||
"react": "19.2.3",
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import { createProxy } from "@vercel/geistdocs/proxy";
|
||||
import { createI18nMiddleware } from "fumadocs-core/i18n/middleware";
|
||||
import {
|
||||
NextResponse,
|
||||
type NextRequest,
|
||||
type NextFetchEvent,
|
||||
} from "next/server";
|
||||
import { config as geistdocsConfig } from "@/lib/geistdocs/config";
|
||||
import { applyDocsResponseHeaders } from "@/lib/docs-response-headers";
|
||||
|
||||
const localeProxy = createI18nMiddleware({
|
||||
defaultLanguage: "en",
|
||||
languages: ["en"],
|
||||
hideLocale: "default-locale",
|
||||
});
|
||||
const geistdocsProxy = createProxy({
|
||||
config: geistdocsConfig,
|
||||
markdownRoutes: [{ from: "/docs/*path", to: "/api/docs-md/*path" }],
|
||||
before: async ({ request, context }) => {
|
||||
if (
|
||||
request.headers.get("rsc") === "1" ||
|
||||
request.headers.has("next-router-prefetch") ||
|
||||
request.headers.has("next-router-segment-prefetch") ||
|
||||
/\bprefetch\b/i.test(request.headers.get("purpose") ?? "") ||
|
||||
/\bprefetch\b/i.test(request.headers.get("sec-purpose") ?? "")
|
||||
) {
|
||||
return (await localeProxy(request, context)) ?? NextResponse.next();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export default async function proxy(
|
||||
request: NextRequest,
|
||||
event: NextFetchEvent,
|
||||
) {
|
||||
const { pathname } = request.nextUrl;
|
||||
try {
|
||||
decodeURIComponent(pathname);
|
||||
} catch (error) {
|
||||
if (!(error instanceof URIError)) throw error;
|
||||
return new NextResponse("Not Found", { status: 404 });
|
||||
}
|
||||
let response: Response;
|
||||
if (pathname === "/en/docs" || pathname.startsWith("/en/docs/")) {
|
||||
const destination = request.nextUrl.clone();
|
||||
destination.pathname = pathname.slice(3);
|
||||
response = NextResponse.redirect(destination, 308);
|
||||
} else if (
|
||||
(pathname === "/docs" || pathname.startsWith("/docs/")) &&
|
||||
!pathname.includes(".")
|
||||
) {
|
||||
response = await geistdocsProxy(request, event);
|
||||
} else {
|
||||
return NextResponse.next();
|
||||
}
|
||||
applyDocsResponseHeaders(response.headers);
|
||||
return response;
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: ["/docs/:path*", "/en/docs/:path*", "/(.*%.*)"],
|
||||
};
|
||||
@@ -0,0 +1,106 @@
|
||||
import process from "node:process";
|
||||
import { spawn } from "node:child_process";
|
||||
import { createServer } from "node:net";
|
||||
import { setTimeout as sleep } from "node:timers/promises";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const cwd = fileURLToPath(new URL("../", import.meta.url));
|
||||
const socket = createServer();
|
||||
await new Promise((resolve, reject) => {
|
||||
socket.once("error", reject);
|
||||
socket.listen(0, "127.0.0.1", resolve);
|
||||
});
|
||||
const port = socket.address().port;
|
||||
await new Promise((resolve, reject) =>
|
||||
socket.close((error) => (error ? reject(error) : resolve())),
|
||||
);
|
||||
|
||||
function launch(args, env) {
|
||||
const child = spawn(process.execPath, args, { cwd, stdio: "inherit", env });
|
||||
const result = { child, done: false, exited: undefined };
|
||||
result.exited = new Promise((resolve) => {
|
||||
child.once("error", (error) => {
|
||||
console.error(error.message);
|
||||
result.done = true;
|
||||
resolve(1);
|
||||
});
|
||||
child.once("exit", (code) => {
|
||||
result.done = true;
|
||||
resolve(code ?? 1);
|
||||
});
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
const server = launch(
|
||||
[
|
||||
fileURLToPath(import.meta.resolve("next/dist/bin/next")),
|
||||
"start",
|
||||
"--hostname",
|
||||
"127.0.0.1",
|
||||
],
|
||||
{ ...process.env, NODE_ENV: "production", PORT: String(port) },
|
||||
);
|
||||
const url = `http://127.0.0.1:${port}`;
|
||||
let tests;
|
||||
let shutdown;
|
||||
let stopping = false;
|
||||
const stop = () => {
|
||||
stopping = true;
|
||||
shutdown ??= Promise.all(
|
||||
[tests, server].map(async (owned) => {
|
||||
if (!owned || owned.done) return;
|
||||
owned.child.kill("SIGTERM");
|
||||
const deadline = setTimeout(() => owned.child.kill("SIGKILL"), 5000);
|
||||
try {
|
||||
await owned.exited;
|
||||
} finally {
|
||||
clearTimeout(deadline);
|
||||
}
|
||||
}),
|
||||
);
|
||||
return shutdown;
|
||||
};
|
||||
for (const [signal, code] of [
|
||||
["SIGINT", 130],
|
||||
["SIGTERM", 143],
|
||||
]) {
|
||||
process.once(signal, () => {
|
||||
void stop().finally(() => process.exit(code));
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const deadline = Date.now() + 60000;
|
||||
let ready = false;
|
||||
while (Date.now() < deadline && !stopping) {
|
||||
if (server.done)
|
||||
throw new Error(`Docs server exited with ${await server.exited}`);
|
||||
try {
|
||||
const response = await fetch(`${url}/robots.txt`, {
|
||||
signal: AbortSignal.timeout(1000),
|
||||
});
|
||||
await response.body?.cancel();
|
||||
if (response.ok) {
|
||||
ready = true;
|
||||
break;
|
||||
}
|
||||
} catch {
|
||||
ready = false;
|
||||
}
|
||||
await sleep(100);
|
||||
}
|
||||
if (!ready && !stopping)
|
||||
throw new Error(
|
||||
"Docs server did not become ready. Run the docs build first.",
|
||||
);
|
||||
if (!stopping) {
|
||||
tests = launch(["--test", "tests/docs-routes.test.mjs"], {
|
||||
...process.env,
|
||||
DOCS_TEST_URL: url,
|
||||
});
|
||||
process.exitCode = await tests.exited;
|
||||
}
|
||||
} finally {
|
||||
await stop();
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import {
|
||||
defineGeistdocsSourceConfig,
|
||||
geistdocsFrontmatterSchema,
|
||||
geistdocsMetaSchema,
|
||||
} from "@vercel/geistdocs/source-config";
|
||||
import { defineConfig, defineDocs } from "fumadocs-mdx/config";
|
||||
import { remarkLegacyHeadings } from "./lib/remark-legacy-headings";
|
||||
|
||||
export const docs = defineDocs({
|
||||
dir: "content/docs",
|
||||
docs: {
|
||||
schema: geistdocsFrontmatterSchema,
|
||||
postprocess: { includeProcessedMarkdown: true },
|
||||
},
|
||||
meta: { schema: geistdocsMetaSchema },
|
||||
});
|
||||
|
||||
const config = defineGeistdocsSourceConfig({
|
||||
mdxOptions: {
|
||||
remarkStructureOptions: {
|
||||
types: [
|
||||
"heading",
|
||||
"paragraph",
|
||||
"blockquote",
|
||||
"tableCell",
|
||||
"mdxJsxFlowElement",
|
||||
"mdxJsxTextElement",
|
||||
"text",
|
||||
"inlineCode",
|
||||
"code",
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default defineConfig({
|
||||
...config,
|
||||
mdxOptions: {
|
||||
...config.mdxOptions,
|
||||
remarkPlugins: [remarkLegacyHeadings],
|
||||
},
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user