diff --git a/.changeset/config.json b/.changeset/config.json index 9d1f2747..67613f87 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -15,7 +15,8 @@ "@json-render/redux", "@json-render/jotai", "@json-render/vue", - "@json-render/xstate" + "@json-render/xstate", + "@json-render/image" ] ], "linked": [], diff --git a/.changeset/v0-11-release.md b/.changeset/v0-11-release.md new file mode 100644 index 00000000..13640fc7 --- /dev/null +++ b/.changeset/v0-11-release.md @@ -0,0 +1,17 @@ +--- +"@json-render/core": minor +"@json-render/image": minor +--- + +Image renderer: generate SVG and PNG from JSON specs. + +### New: `@json-render/image` Package + +Server-side image renderer powered by Satori. Turns the same `{ root, elements }` spec format into SVG or PNG output for OG images, social cards, and banners. + +- `renderToSvg(spec, options)` — render spec to SVG string +- `renderToPng(spec, options)` — render spec to PNG buffer (requires `@resvg/resvg-js`) +- 9 standard components: Frame, Box, Row, Column, Heading, Text, Image, Divider, Spacer +- `standardComponentDefinitions` catalog for AI prompt generation +- Server-safe import path: `@json-render/image/server` +- Sub-path exports: `/render`, `/catalog`, `/server` diff --git a/AGENTS.md b/AGENTS.md index 2ff091da..7b60bcbc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -73,6 +73,41 @@ Do **not** add `--port` flags -- portless handles port assignment automatically. - Skills in `skills/*/SKILL.md` (if the package has a corresponding skill) - `AGENTS.md` (if workflow or conventions change) +## Releases + +This monorepo uses [Changesets](https://github.com/changesets/changesets) for versioning and publishing. + +### Fixed version group + +All public `@json-render/*` packages are in a **fixed** group (see `.changeset/config.json`). A changeset that bumps any one of them bumps all of them to the same version. You only need to list the packages that actually changed in the changeset front matter — the fixed group handles the rest. + +### Preparing a release + +When asked to prepare a release (e.g. "prepare v0.12.0"): + +1. **Create a changeset file** at `.changeset/v0--release.md` following the existing pattern: + - YAML front matter listing changed packages with bump type (`minor` for feature releases, `patch` for bug-fix-only releases) + - A one-line summary, then `### New:` / `### Improved:` / `### Fixed:` sections describing each change + - Always list `@json-render/core` plus any packages with actual code changes +2. **Do NOT bump versions** in `package.json` files — CI runs `pnpm ci:version` (which calls `changeset version`) to do that automatically +3. **Do NOT manually write `CHANGELOG.md`** entries — `changeset version` generates them from the changeset file +4. **Add new packages to the fixed group** in `.changeset/config.json` if they should be versioned together with the rest +5. **Fill documentation gaps** — every public package should have: + - A row in the root `README.md` packages table + - A renderer section in the root `README.md` (if it's a renderer) + - An API reference page at `apps/web/app/(main)/docs/api//page.mdx` + - An 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/json-render-/SKILL.md` + - A `packages//README.md` +6. **Run `pnpm type-check`** after all changes to verify nothing is broken + +### CI scripts + +- `pnpm changeset` — interactively create a new changeset +- `pnpm ci:version` — run `changeset version` + lockfile update (CI only) +- `pnpm ci:publish` — build all packages and publish to npm (CI only) + ## Source Code Reference diff --git a/README.md b/README.md index b0a37562..ce702ea3 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,7 @@ function Dashboard({ spec }) { | `@json-render/react-native` | React Native renderer with standard mobile components | | `@json-render/remotion` | Remotion video renderer, timeline schema | | `@json-render/react-pdf` | React PDF renderer for generating PDF documents from specs | +| `@json-render/image` | Image renderer for SVG/PNG output (OG images, social cards) via Satori | | `@json-render/redux` | Redux / Redux Toolkit adapter for `StateStore` | | `@json-render/zustand` | Zustand adapter for `StateStore` | | `@json-render/jotai` | Jotai adapter for `StateStore` | @@ -287,6 +288,35 @@ const spec = { const buffer = await renderToBuffer(spec); ``` +### Image (SVG/PNG) + +```typescript +import { renderToPng } from "@json-render/image/render"; + +const spec = { + root: "frame", + elements: { + frame: { + type: "Frame", + props: { width: 1200, height: 630, backgroundColor: "#1a1a2e" }, + children: ["heading"], + }, + heading: { + type: "Heading", + props: { text: "Hello World", level: "h1", color: "#ffffff" }, + children: [], + }, + }, +}; + +// Render to PNG (requires @resvg/resvg-js) +const png = await renderToPng(spec, { fonts }); + +// Or render to SVG string +import { renderToSvg } from "@json-render/image/render"; +const svg = await renderToSvg(spec, { fonts }); +``` + ## Features ### Streaming (SpecStream) diff --git a/apps/web/app/(main)/docs/api/image/page.mdx b/apps/web/app/(main)/docs/api/image/page.mdx new file mode 100644 index 00000000..8205b032 --- /dev/null +++ b/apps/web/app/(main)/docs/api/image/page.mdx @@ -0,0 +1,364 @@ +import { pageMetadata } from "@/lib/page-metadata" +export const metadata = pageMetadata("docs/api/image") + +# @json-render/image + +Image renderer. Turn JSON specs into SVG and PNG images using [Satori](https://github.com/vercel/satori). + +## Install + +```bash +npm install @json-render/core @json-render/image +``` + +For PNG output, also install the optional peer dependency: + +```bash +npm install @resvg/resvg-js +``` + +See the [Image example](https://github.com/vercel-labs/json-render/tree/main/examples/image) for a full working example. + +## schema + +The image element schema for image specs. Use with `defineCatalog` from core. + +```typescript +import { defineCatalog } from '@json-render/core'; +import { schema, standardComponentDefinitions } from '@json-render/image'; + +const catalog = defineCatalog(schema, { + components: standardComponentDefinitions, +}); +``` + +## Render Functions + +Server-side functions for producing image output. Both accept a spec and optional `RenderOptions`. + +```typescript +import { renderToSvg, renderToPng } from '@json-render/image/render'; + +const svg = await renderToSvg(spec, { fonts }); + +const png = await renderToPng(spec, { fonts }); +await writeFile('output.png', png); +``` + +### RenderOptions + +```typescript +interface RenderOptions { + registry?: ComponentRegistry; + includeStandard?: boolean; // default: true + state?: Record; + fonts?: SatoriOptions['fonts']; + width?: number; + height?: number; +} +``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
OptionTypeDefaultDescription
fonts{"SatoriOptions['fonts']"}[]Font data for text rendering (required for meaningful output)
widthnumberFrame propOverride the output image width
heightnumberFrame propOverride the output image height
registry{"Record"}{"{}"}Custom component map (merged with standard components)
includeStandardbooleantrueInclude built-in standard components
state{"Record"}{"{}"}Initial state for $state / $cond dynamic prop resolution
+ +## Standard Components + +### Root + +#### Frame + +Root image container. Defines the output image dimensions and background. Must be the root element. + +```typescript +{ + width: number; + height: number; + backgroundColor: string | null; + padding: number | null; + display: "flex" | "none" | null; + flexDirection: "row" | "column" | null; + alignItems: "flex-start" | "center" | "flex-end" | "stretch" | null; + justifyContent: "flex-start" | "center" | "flex-end" | "space-between" | "space-around" | null; +} +``` + +### Layout + +#### Box + +Generic container with padding, margin, background, border, and flex alignment. Supports absolute positioning. + +```typescript +{ + padding: number | null; + paddingTop: number | null; + paddingBottom: number | null; + paddingLeft: number | null; + paddingRight: number | null; + margin: number | null; + backgroundColor: string | null; + borderWidth: number | null; + borderColor: string | null; + borderRadius: number | null; + flex: number | null; + width: number | string | null; + height: number | string | null; + alignItems: "flex-start" | "center" | "flex-end" | "stretch" | null; + justifyContent: "flex-start" | "center" | "flex-end" | "space-between" | "space-around" | null; + flexDirection: "row" | "column" | null; + position: "relative" | "absolute" | null; + top: number | null; + left: number | null; + right: number | null; + bottom: number | null; + overflow: "visible" | "hidden" | null; +} +``` + +#### Row + +Horizontal flex layout with optional wrapping. + +```typescript +{ + gap: number | null; + alignItems: "flex-start" | "center" | "flex-end" | "stretch" | null; + justifyContent: "flex-start" | "center" | "flex-end" | "space-between" | "space-around" | null; + padding: number | null; + flex: number | null; + wrap: boolean | null; +} +``` + +#### Column + +Vertical flex layout. + +```typescript +{ + gap: number | null; + alignItems: "flex-start" | "center" | "flex-end" | "stretch" | null; + justifyContent: "flex-start" | "center" | "flex-end" | "space-between" | "space-around" | null; + padding: number | null; + flex: number | null; +} +``` + +### Content + +#### Heading + +Heading text at various levels. h1 is largest, h4 is smallest. + +```typescript +{ + text: string; + level: "h1" | "h2" | "h3" | "h4" | null; + color: string | null; + align: "left" | "center" | "right" | null; + letterSpacing: number | string | null; + lineHeight: number | null; +} +``` + +#### Text + +Body text with configurable size, color, weight, and alignment. + +```typescript +{ + text: string; + fontSize: number | null; + color: string | null; + align: "left" | "center" | "right" | null; + fontWeight: "normal" | "bold" | null; + fontStyle: "normal" | "italic" | null; + lineHeight: number | null; + letterSpacing: number | string | null; + textDecoration: "none" | "underline" | "line-through" | null; +} +``` + +#### Image + +Image from a URL with optional dimensions and fit. + +```typescript +{ + src: string; + width: number | null; + height: number | null; + borderRadius: number | null; + objectFit: "contain" | "cover" | "fill" | "none" | null; +} +``` + +### Decorative + +#### Divider + +Horizontal line separator. + +```typescript +{ + color: string | null; + thickness: number | null; + marginTop: number | null; + marginBottom: number | null; +} +``` + +#### Spacer + +Empty vertical space. + +```typescript +{ + height: number | null; +} +``` + +## Catalog Definitions + +Pre-built definitions for creating image catalogs: + +```typescript +import { standardComponentDefinitions } from '@json-render/image/catalog'; +import { defineCatalog } from '@json-render/core'; +import { schema } from '@json-render/image'; + +const catalog = defineCatalog(schema, { + components: { + ...standardComponentDefinitions, + // Add custom components + }, +}); +``` + +## Server-Safe Import + +Import schema and catalog definitions without pulling in React or Satori: + +```typescript +import { schema, standardComponentDefinitions } from '@json-render/image/server'; +``` + +## Sub-path Exports + + + + + + + + + + + + + + + + + + + + + + + + + + +
ExportDescription
@json-render/imageFull package: schema, renderer, components, render functions
@json-render/image/serverSchema and catalog definitions only (no React or Satori)
@json-render/image/catalogStandard component definitions and types
@json-render/image/renderServer-side render functions only
+ +## Types + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ExportDescription
ImageSchemaSchema type for image specs
ImageSpecSpec type for image output
RenderOptionsOptions for render functions
ComponentRenderPropsProps passed to component render functions
ComponentRendererComponent render function type
ComponentRegistryMap of component names to render functions
StandardComponentDefinitionsType of the standard component definitions object
StandardComponentProps{''}Inferred props type for a standard component by name
diff --git a/apps/web/app/(main)/docs/renderers/page.mdx b/apps/web/app/(main)/docs/renderers/page.mdx new file mode 100644 index 00000000..91bb7577 --- /dev/null +++ b/apps/web/app/(main)/docs/renderers/page.mdx @@ -0,0 +1,197 @@ +import { pageMetadata } from "@/lib/page-metadata" +export const metadata = pageMetadata("docs/renderers") + +# 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. + +All renderers share the same workflow: + +1. Define a catalog with `defineCatalog` +2. AI generates a JSON spec +3. The renderer turns the spec into platform-native output + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
RendererPackageOutput
React@json-render/reactReact component tree
Vue@json-render/vueVue 3 component tree
shadcn/ui@json-render/shadcnPre-built Radix UI + Tailwind components (uses React renderer)
React Native@json-render/react-nativeNative mobile views
Image@json-render/imageSVG / PNG (via Satori)
React PDF@json-render/react-pdfPDF documents
Remotion@json-render/remotionVideo compositions
+ +## React + +Render specs as React component trees in the browser. Supports data binding, streaming, actions, validation, visibility, and computed values. + +```tsx +import { defineRegistry, Renderer } from "@json-render/react"; +import { schema } from "@json-render/react/schema"; + +const { registry } = defineRegistry(catalog, { components }); + +``` + +Use `StateProvider`, `VisibilityProvider`, and `ActionProvider` for full interactivity. See the [@json-render/react API reference](/docs/api/react) for details. + +## Vue + +Vue 3 renderer with full feature parity with React: data binding, visibility, actions, validation, repeat scopes, and streaming. + +```typescript +import { defineRegistry, Renderer } from "@json-render/vue"; +import { schema } from "@json-render/vue/schema"; + +const { registry } = defineRegistry(catalog, { + components: { + Card: ({ props, children }) => + h("div", { class: "card" }, [h("h3", null, props.title), children]), + }, +}); +``` + +Uses composables (`useStateStore`, `useStateBinding`, `useActions`, etc.) instead of React hooks. See the [@json-render/vue API reference](/docs/api/vue) for details. + +## shadcn/ui + +36 pre-built components using Radix UI and Tailwind CSS. Built on top of `@json-render/react` -- no custom renderer needed. + +```tsx +import { defineCatalog } from "@json-render/core"; +import { schema } from "@json-render/react/schema"; +import { defineRegistry, Renderer } from "@json-render/react"; +import { shadcnComponentDefinitions } from "@json-render/shadcn/catalog"; +import { shadcnComponents } from "@json-render/shadcn"; + +const catalog = defineCatalog(schema, { + components: { + Card: shadcnComponentDefinitions.Card, + Button: shadcnComponentDefinitions.Button, + }, +}); + +const { registry } = defineRegistry(catalog, { + components: { + Card: shadcnComponents.Card, + Button: shadcnComponents.Button, + }, +}); +``` + +See the [@json-render/shadcn API reference](/docs/api/shadcn) for the full component list. + +## React Native + +Render specs as native mobile views. Includes 25+ standard components and standard action definitions. + +```tsx +import { defineCatalog } from "@json-render/core"; +import { schema } from "@json-render/react-native/schema"; +import { standardComponentDefinitions, standardActionDefinitions } from "@json-render/react-native/catalog"; +import { defineRegistry, Renderer } from "@json-render/react-native"; + +const catalog = defineCatalog(schema, { + components: { ...standardComponentDefinitions }, + actions: standardActionDefinitions, +}); + +const { registry } = defineRegistry(catalog, { components: {} }); + +``` + +See the [@json-render/react-native API reference](/docs/api/react-native) for details. + +## Image + +Generate SVG and PNG images from JSON specs using Satori. Ideal for OG images, social cards, and banners. + +```typescript +import { renderToSvg, renderToPng } from "@json-render/image/render"; + +const svg = await renderToSvg(spec, { fonts }); +const png = await renderToPng(spec, { fonts }); +``` + +Nine standard components: Frame, Box, Row, Column, Heading, Text, Image, Divider, Spacer. PNG output requires `@resvg/resvg-js` as an optional peer dependency. + +See the [@json-render/image API reference](/docs/api/image) for details. + +## React PDF + +Generate PDF documents from JSON specs using `@react-pdf/renderer`. Render to buffer, stream, or file. + +```typescript +import { renderToBuffer, renderToStream, renderToFile } from "@json-render/react-pdf"; + +const buffer = await renderToBuffer(spec); +const stream = await renderToStream(spec); +await renderToFile(spec, "./output.pdf"); +``` + +Standard components include Document, Page, View, Row, Column, Heading, Text, Image, Table, List, Divider, Spacer, Link, and PageNumber. + +See the [@json-render/react-pdf API reference](/docs/api/react-pdf) for details. + +## Remotion + +Turn JSON timeline specs into video compositions with Remotion. + +```tsx +import { Player } from "@remotion/player"; +import { Renderer } from "@json-render/remotion"; + + +``` + +Uses a timeline spec format with compositions, tracks, and clips. Includes standard components (TitleCard, TypingText, ImageSlide, etc.), transitions (fade, slide, zoom, wipe), and effects. + +See the [@json-render/remotion API reference](/docs/api/remotion) for details. + +## Custom Renderers + +You can build your own renderer for any output target. See the [Custom Schema & Renderer](/docs/custom-schema) guide for how to define a custom schema and wire it to your own rendering logic. diff --git a/apps/web/app/api/docs-chat/route.ts b/apps/web/app/api/docs-chat/route.ts index e53df1e0..af097fcf 100644 --- a/apps/web/app/api/docs-chat/route.ts +++ b/apps/web/app/api/docs-chat/route.ts @@ -16,7 +16,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/remotion, @json-render/codegen +npm packages: @json-render/core, @json-render/react, @json-render/image, @json-render/remotion, @json-render/codegen You have access to the full json-render documentation via the bash and readFile tools. The docs are available as markdown files in the /workspace/docs/ directory. diff --git a/apps/web/lib/docs-navigation.ts b/apps/web/lib/docs-navigation.ts index e5328c1b..2f2a658d 100644 --- a/apps/web/lib/docs-navigation.ts +++ b/apps/web/lib/docs-navigation.ts @@ -36,6 +36,7 @@ export const docsNavigation: NavSection[] = [ { title: "Rendering", items: [ + { title: "Renderers", href: "/docs/renderers" }, { title: "Registry", href: "/docs/registry" }, { title: "Streaming", href: "/docs/streaming" }, { title: "Generation Modes", href: "/docs/generation-modes" }, @@ -69,6 +70,11 @@ export const docsNavigation: NavSection[] = [ href: "https://github.com/vercel-labs/json-render/tree/main/examples/remotion", external: true, }, + { + title: "Image", + href: "https://github.com/vercel-labs/json-render/tree/main/examples/image", + external: true, + }, { title: "Vue", href: "https://github.com/vercel-labs/json-render/tree/main/examples/vue", @@ -106,6 +112,7 @@ export const docsNavigation: NavSection[] = [ { title: "@json-render/react-pdf", href: "/docs/api/react-pdf" }, { title: "@json-render/shadcn", href: "/docs/api/shadcn" }, { title: "@json-render/react-native", href: "/docs/api/react-native" }, + { title: "@json-render/image", href: "/docs/api/image" }, { title: "@json-render/remotion", href: "/docs/api/remotion" }, { title: "@json-render/vue", href: "/docs/api/vue" }, { title: "@json-render/codegen", href: "/docs/api/codegen" }, diff --git a/apps/web/lib/page-titles.ts b/apps/web/lib/page-titles.ts index 10835668..00bc6170 100644 --- a/apps/web/lib/page-titles.ts +++ b/apps/web/lib/page-titles.ts @@ -26,6 +26,7 @@ export const PAGE_TITLES: Record = { "docs/computed-values": "Computed Values", "docs/visibility": "Visibility", "docs/watchers": "Watchers", + "docs/renderers": "Renderers", "docs/generation-modes": "Generation Modes", "docs/code-export": "Code Export", "docs/custom-schema": "Custom Schema & Renderer", @@ -44,6 +45,7 @@ export const PAGE_TITLES: Record = { "docs/api/react-pdf": "@json-render/react-pdf API", "docs/api/react-native": "@json-render/react-native API", "docs/api/codegen": "@json-render/codegen API", + "docs/api/image": "@json-render/image API", "docs/api/remotion": "@json-render/remotion API", "docs/api/shadcn": "@json-render/shadcn API", }; diff --git a/skills/json-render-image/SKILL.md b/skills/json-render-image/SKILL.md new file mode 100644 index 00000000..500f4d6b --- /dev/null +++ b/skills/json-render-image/SKILL.md @@ -0,0 +1,96 @@ +--- +name: json-render-image +description: Image renderer for json-render that turns JSON specs into SVG and PNG images via Satori. Use when working with @json-render/image, generating OG images from JSON, creating social cards, or rendering AI-generated image specs. +--- + +# @json-render/image + +Image renderer that converts JSON specs into SVG and PNG images using Satori. + +## Quick Start + +```typescript +import { renderToPng } from "@json-render/image/render"; +import type { Spec } from "@json-render/core"; + +const spec: Spec = { + root: "frame", + elements: { + frame: { + type: "Frame", + props: { width: 1200, height: 630, backgroundColor: "#1a1a2e" }, + children: ["heading"], + }, + heading: { + type: "Heading", + props: { text: "Hello World", level: "h1", color: "#ffffff" }, + children: [], + }, + }, +}; + +const png = await renderToPng(spec, { + fonts: [{ name: "Inter", data: fontData, weight: 400, style: "normal" }], +}); +``` + +## Using Standard Components + +```typescript +import { defineCatalog } from "@json-render/core"; +import { schema, standardComponentDefinitions } from "@json-render/image"; + +export const imageCatalog = defineCatalog(schema, { + components: standardComponentDefinitions, +}); +``` + +## Adding Custom Components + +```typescript +import { z } from "zod"; + +const catalog = defineCatalog(schema, { + components: { + ...standardComponentDefinitions, + Badge: { + props: z.object({ label: z.string(), color: z.string().nullable() }), + slots: [], + description: "A colored badge label", + }, + }, +}); +``` + +## Standard Components + +| Component | Category | Description | +|-----------|----------|-------------| +| `Frame` | Root | Root container. Defines width, height, background. Must be root. | +| `Box` | Layout | Container with padding, margin, border, absolute positioning | +| `Row` | Layout | Horizontal flex layout | +| `Column` | Layout | Vertical flex layout | +| `Heading` | Content | h1-h4 heading text | +| `Text` | Content | Body text with full styling | +| `Image` | Content | Image from URL | +| `Divider` | Decorative | Horizontal line separator | +| `Spacer` | Decorative | Empty vertical space | + +## Key Exports + +| Export | Purpose | +|--------|---------| +| `renderToSvg` | Render spec to SVG string | +| `renderToPng` | Render spec to PNG buffer (requires `@resvg/resvg-js`) | +| `schema` | Image element schema | +| `standardComponents` | Pre-built component registry | +| `standardComponentDefinitions` | Catalog definitions for AI prompts | + +## Sub-path Exports + +| Export | Description | +|--------|-------------| +| `@json-render/image` | Full package: schema, components, render functions | +| `@json-render/image/server` | Schema and catalog definitions only (no React/Satori) | +| `@json-render/image/catalog` | Standard component definitions and types | +| `@json-render/image/render` | Render functions only |