diff --git a/apps/website/content/blog/2026-08-26-json-render-vs-a2ui-choosing.mdx b/apps/website/content/blog/2026-08-26-json-render-vs-a2ui-choosing.mdx new file mode 100644 index 000000000..924cdca9b --- /dev/null +++ b/apps/website/content/blog/2026-08-26-json-render-vs-a2ui-choosing.mdx @@ -0,0 +1,103 @@ +--- +title: 'json-render vs A2UI: Choosing a Generative UI Contract' +description: 'A fixed spec is easier to validate; A2UI updates over time and sends actions back. Which contract shape fits your surface.' +date: 2026-08-26 +tags: [generative-ui, a2ui, json-render, angular, agentic-ui] +author: brian +featured: false +draft: false +--- + +Threadplane gives you two ways to let an agent build UI — a json-render spec or an A2UI surface — and this post is about how to pick. + +There's a ladder here: markdown when text is the best UI → a fixed spec when you can validate the whole thing up front → a live protocol when the surface keeps changing. +The [mechanical comparison](/docs/render/concepts/json-render-vs-a2ui) covers what each layer does; this post answers the question that page ends on: which rung is yours? + +## What's actually different? + +This isn't a renderer shootout. The tradeoff is contract shape. + +With json-render, the contract is _application-owned_. +You define the schema, you validate the spec before anything mounts, and your handlers own what every event means. + +With A2UI, the surface is _agent-owned_. +The agent creates it, keeps updating it over the life of the conversation, and gets structured actions back when the user interacts. + +Let's look at the same order card in both shapes. +First as a json-render spec: + +```json +{ + "root": "card", + "elements": { + "card": { "type": "Card", "props": {}, "children": ["body"] }, + "body": { "type": "Column", "props": {}, "children": ["title", "total"] }, + "title": { "type": "Text", "props": { "text": "Order #1042", "variant": "h4" } }, + "total": { "type": "Text", "props": { "text": "$118.00" } } + } +} +``` + +And as an A2UI JSONL stream: + +```jsonl +{"version":"v0.9","createSurface":{"surfaceId":"order","catalogId":"https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json"}} +{"version":"v0.9","updateComponents":{"surfaceId":"order","components":[{"id":"root","component":"Text","text":{"path":"/total"}}]}} +{"version":"v0.9","updateDataModel":{"surfaceId":"order","path":"/total","value":"$118.00"}} +``` + +(I trimmed this one to the total line — the full card is just more components in the `updateComponents` envelope. The point is the shape: structure in one message, data in another.) + +One is a document you can validate before you show it; the other is a conversation you subscribe to. + +## When does the fixed spec win? + +Whenever the UI is one answer — the agent responds once, the UI renders once, and it's done. + +Let's walk two scenarios to a verdict. + +A _structured result card_ in chat: the agent looks up an order and answers with a summary card. +Nothing about that card changes after it lands, so there's no ongoing surface to manage. +Verdict: json-render. +You get to validate the whole spec before mount, and your handlers — not the protocol — decide what a click means. + +A _dashboard or results panel_ outside chat: your application already has the data and wants a model (or a config file, honestly) to describe the layout. +Verdict: json-render again, driven directly through ``. +The fixed contract is the feature here: explicit inputs on your own custom components, a schema you can lint, a spec you can snapshot in a test. + +One security note: in both paths, the registry is doing allowlist duty. +A component name the model emits that isn't registered falls back instead of executing — json-render and A2UI share that posture, so it's not a reason to pick either. + +## When does the protocol win? + +Whenever the surface has to live past its first render. + +Let's take the itinerary case. +The agent proposes a three-day trip mid-conversation: the component structure arrives first, prices and times fill in as `updateDataModel` messages land, and two turns later the agent swaps day two entirely. +That's not one spec becoming one component tree — it's a surface being edited over time, and that's exactly what the envelope stream models. + +Now a form the _agent_ needs back. +A _valid_ submit goes back to the agent as a structured action message on its own; create the surface with `sendDataModel` and the current data model rides along. +Either way, user input flows back through the protocol, not through handlers you wire yourself. + +So the reasons to step up: incremental surfaces, data arriving separately from structure, and user actions as first-class protocol messages. + +The honest cost is protocol discipline. +Envelopes have to be valid and arrive in a sensible order, the catalog has to support every component type the agent names, and someone has to actually design the action semantics — a fixed spec asks for none of that. + +## What does it cost to switch? + +Inside chat, less than you'd think. + +Let's look at why. The same `[views]` catalog feeds both paths, and chat detects the contract from the first bytes: `{` means a json-render spec, `---a2ui_JSON---` means A2UI JSONL. +So the choice is per-surface, not per-app — and it's revisable. + +For me, the default is: start with json-render, and step up to A2UI only when a surface genuinely needs to live past its first render. +You're not locked in either way, so the cheap contract is the right place to begin. + +## Conclusion + +The heuristic is short: if you can validate the entire UI before it renders, start with json-render; if the surface keeps changing after it lands — data trickling in, actions coming back, edits across turns — use A2UI. + +The [mechanical comparison](/docs/render/concepts/json-render-vs-a2ui) has the layer-by-layer details, the [generative UI guide](/docs/chat/guides/generative-ui) wires up the json-render path end to end, and the [A2UI overview](/docs/chat/a2ui/overview) does the same for surfaces. +Pick a surface you're building this week, run it up the ladder, and let me know where it lands. diff --git a/docs/superpowers/plans/2026-08-26-json-render-vs-a2ui-post.md b/docs/superpowers/plans/2026-08-26-json-render-vs-a2ui-post.md new file mode 100644 index 000000000..0ab765bef --- /dev/null +++ b/docs/superpowers/plans/2026-08-26-json-render-vs-a2ui-post.md @@ -0,0 +1,151 @@ +# "json-render vs A2UI: Choosing a Generative UI Contract" Blog Post Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Publish an opinionated decision essay for the `json render vs a2ui` query cluster (highest-intent traffic on the site, already ranking #3) that answers "which should I pick," complementing the mechanical docs comparison. + +**Architecture:** One new MDX file in `apps/website/content/blog/`. No code changes. Decision essay with exactly one paired snippet; links the docs comparison page for all mechanics. + +**Tech Stack:** MDX blog content, Next.js website (`apps/website`), vitest for content validation. + +**Spec:** `docs/superpowers/specs/2026-08-26-json-render-vs-a2ui-post-design.md` +**Branch:** `blove/json-render-vs-a2ui-post` (off main at `9064d456`) + +--- + +## Verified facts (source of truth) + +- **json-render `Spec`** (`@json-render/core`, `dist/store-utils-*.d.ts:380`): `{ root: string; elements: Record; state?: Record }`. `UIElement`: `{ type: string; props: P; children?: string[]; visible?; on?; repeat? }`. Docs describe it as a "flat UI tree structure (optimized for LLM generation)". +- **A2UI envelopes** (`libs/a2ui/src/lib/parser.ts:4`): recognized keys `createSurface`, `updateComponents`, `updateDataModel`, `deleteSurface`; JSONL example from source: `{"version":"v0.9","createSurface":{"surfaceId":"s1","catalogId":"basic"}}`. Official vendored JSON schemas live in `libs/a2ui` — verify any envelope fields the post shows against them (memory: verify props against official schemas; surface owns liveStore). +- **Shared catalog claim:** chat's `[views]` input feeds both paths — json-render via `ViewRegistry`→`AngularRegistry` conversion, A2UI via the same catalog shape (`apps/website/content/docs/render/concepts/json-render-vs-a2ui.mdx` "Registries And Catalogs"; chat-side code in `libs/chat/src/lib/a2ui/views.ts` and `surface.component.ts`). The docs page states: "The same `views` input is used by both paths." +- **Chat detection** (docs page "Chat Detection"): text → markdown; leading `{` → json-render; leading `---a2ui_JSON---` → A2UI JSONL. +- **Positioning lines available to reuse (docs page):** "the tradeoff is not 'which renderer is better.' The tradeoff is contract shape"; "the registry is the allowlist"; "Use markdown when the best UI is text." +- **A2UI origin:** Google's agent-to-UI protocol; we implement the v0.9/v0.9.1 line with vendored official schemas. Don't speculate beyond what we implement. + +--- + +### Task 1: Author the post + +**Files:** +- Create: `apps/website/content/blog/2026-08-26-json-render-vs-a2ui-choosing.mdx` + +Slug derives from filename minus date → `/blog/json-render-vs-a2ui-choosing`. + +- [ ] **Step 1: Create the file with this exact frontmatter** + +```yaml +--- +title: 'json-render vs A2UI: Choosing a Generative UI Contract' +description: 'A fixed spec is easier to validate; A2UI updates over time and sends actions back. Which contract shape fits your surface.' +date: 2026-08-26 +tags: [generative-ui, a2ui, json-render, angular, agentic-ui] +author: brian +featured: false +draft: false +--- +``` + +Description is 120 chars (≤155). **No licensing callout** — dropped per Brian's direction. + +- [ ] **Step 2: Write the body** + +1. **Lede** (no header): one sentence restating the decision. Then the two-line ladder framing (markdown when text is the best UI → a fixed spec when you can validate up front → a live protocol when the surface keeps changing). Link the docs comparison page (`/docs/render/concepts/json-render-vs-a2ui`) early, labeled as the mechanical comparison; this post is the decision. +2. **`## What's actually different?`** — answer in the first line: contract shape, not renderer quality. Ownership framing: json-render is an application-owned contract (you define the schema, validate before mount, own event semantics); A2UI is an agent-owned surface (the agent creates it, updates it over time, and receives structured actions back). Then the ONE paired snippet — verify both halves before committing: + + json-render (one fixed spec): + ```json + { + "root": "card", + "elements": { + "card": { "type": "Card", "props": { "title": "Order #1042" }, "children": ["total"] }, + "total": { "type": "Text", "props": { "text": "$118.00" } } + } + } + ``` + + A2UI (a stream of envelopes, JSONL): + ```json + {"version":"v0.9","createSurface":{"surfaceId":"s1","catalogId":"basic"}} + {"version":"v0.9","updateComponents":{"surfaceId":"s1","components":[...]}} + {"version":"v0.9","updateDataModel":{"surfaceId":"s1","...":"..."}} + ``` + + **Verification required:** the `updateComponents`/`updateDataModel` field shapes above are PLACEHOLDERS — before writing, read the vendored official schemas in `libs/a2ui` (and `libs/a2ui/src/lib/parser.ts` + types) and write real, schema-valid minimal envelopes; ellipses are not acceptable in the published post. The json-render half must match `Spec`/`UIElement` from the verified facts (it does; keep prop names if `Card`/`Text` exist in the basic catalog — check `a2uiBasicCatalog`/render examples and substitute real component names if not). One sentence after the snippet lands the point: one is a document you can validate; the other is a conversation you subscribe to. +3. **`## When does the fixed spec win?`** — answer first line, then the scenarios walked to verdicts: a structured result card (agent answers once, UI renders once) and a dashboard/results panel outside chat. The reasons: validate-before-mount, application-owned handlers, custom components with explicit inputs. Include the allowlist point (registry decides what can render) in one line, crediting it as the security posture both share. +4. **`## When does the protocol win?`** — answer first line, then: a live itinerary that updates mid-conversation (structure arrives, data fills in later, agent keeps editing), and a form whose submission returns to the agent as a structured action (with the data model attached when `sendDataModel` is set). The reasons: incremental surfaces, data separate from structure, actions as protocol messages. The cost, stated honestly: protocol discipline — valid envelopes, right order, catalog support, action semantics. +5. **`## What does it cost to switch?`** — answer first line: less than you'd think inside chat. The same `[views]` catalog feeds both paths, and chat detects which contract is streaming (leading `{` vs `---a2ui_JSON---`), so the choice is per-surface, not per-app, and revisable. Flag the default as opinion: start with json-render and step up to A2UI when a surface genuinely needs to live past its first render ("For me…" / "I think…"). +6. **`## Conclusion`** — one short paragraph: the one-rule heuristic (if you can validate the whole UI before showing it, start with json-render; if it's a live conversation artifact with partial data, actions, and updates, use A2UI). Forward links: the docs comparison page, `/docs/chat/guides/generative-ui`, `/docs/chat/a2ui/overview`. Closing line is an invitation or forward link, no CTA. + +- [ ] **Step 3: Voice pass** + +Same gate as post #11 — `docs/gtm/voice.md` with the 2026 technical override: title-restating lede, no "Introduction" header, contractions, 1–3-line paragraphs, H2-as-question answered in the first line, ≥1 "Let's" per major section, opinions flagged, no anecdotes/emoji/hype/CTAs. Additional: don't copy docs-page sentences verbatim except the deliberately reused positioning line ("the tradeoff is contract shape") — paraphrase everything else. + +- [ ] **Step 4: Accuracy pass** + +- Every mechanism claim checked against the docs page and, where behavioral, source (`libs/chat/src/lib/a2ui/*`, `libs/render/src/lib/*`, `libs/a2ui/src/lib/parser.ts`). +- Published-release check: `npm pack @threadplane/chat@latest @threadplane/render@latest @threadplane/a2ui@latest` into the scratchpad; confirm any named public member (e.g. `a2uiBasicCatalog`, `views`, `sendDataModel` on the surface types) exists in the published `.d.ts`. Drop main-only members. + +- [ ] **Step 5: Commit** + +```bash +git add apps/website/content/blog/2026-08-26-json-render-vs-a2ui-choosing.mdx +git commit -m "feat(website): add 'json-render vs A2UI' blog post" +``` + +--- + +### Task 2: Validate + +- [ ] **Step 1: Frontmatter + description length** + +```bash +cd apps/website && node -e " +const matter = require('gray-matter'); +const fs = require('fs'); +const f = matter(fs.readFileSync('content/blog/2026-08-26-json-render-vs-a2ui-choosing.mdx','utf8')); +console.log('desc length:', f.data.description.length); +if (f.data.description.length > 155) throw new Error('description too long'); +if (!f.data.title || !f.data.date || f.data.author !== 'brian') throw new Error('frontmatter incomplete'); +console.log('OK'); +" +``` + +Expected: length ≤155, `OK`. + +- [ ] **Step 2: Website test suite** (`nx test website` does not exist): + +```bash +cd apps/website && npx vitest run --config vite.config.mts +``` + +Expected: `src/lib/blog.spec.ts` and `src/lib/sitemap-dates.spec.ts` pass. Known pre-existing failures (do NOT fix, just confirm unchanged): `PostCard.spec.tsx` (1), `Differentiator.spec.tsx` (1), `thanks/page.spec.tsx` (3). + +- [ ] **Step 3: Render check** — `npx next dev` on port 3111 from `apps/website` (background), then curl: + - `/blog/json-render-vs-a2ui-choosing` → 200, contains the post ``, both code blocks render as `<pre data-language="json"`, meta description matches frontmatter + - `/blog` lists the post + Kill the server, verify port free, revert any `next-env.d.ts` side-effect edit. + +- [ ] **Step 4: Commit fixes** (skip if none): + +```bash +git add -A apps/website/content/blog/ && git commit -m "fix(website): render fixes for json-render vs A2UI post" +``` + +--- + +### Task 3: PR + +- [ ] **Step 1: Push and open PR** + +```bash +git push -u origin HEAD +gh pr create --title "feat(website): add 'json-render vs A2UI' blog post" --body "Second post of the GSC-driven blog sequence (spec: docs/superpowers/specs/2026-08-26-json-render-vs-a2ui-post-design.md). + +Targets the \`json render vs a2ui\` comparison cluster — the highest-intent traffic on the site (already #3, 25% CTR on one phrasing) — with the decision-intent post; links to (does not replace) the docs comparison page. + +🤖 Generated with [Claude Code](https://claude.com/claude-code)" +``` + +- [ ] **Step 2: Merge on green + verify** + +Per Brian's standing instruction for this sequence: arm auto-merge (`gh pr merge <n> --squash --auto`) once the PR is open; only `Vercel – threadplane` gates. After merge, verify the post exists on `origin/main` and report the production URL (`https://threadplane.ai/blog/json-render-vs-a2ui-choosing`) once the main deploy completes. The Vercel preview URL is SSO-protected — verify locally + via build success, hand Brian the URL. diff --git a/docs/superpowers/specs/2026-08-26-json-render-vs-a2ui-post-design.md b/docs/superpowers/specs/2026-08-26-json-render-vs-a2ui-post-design.md new file mode 100644 index 000000000..de0e04cce --- /dev/null +++ b/docs/superpowers/specs/2026-08-26-json-render-vs-a2ui-post-design.md @@ -0,0 +1,51 @@ +# Post #9 design: "json-render vs A2UI: Choosing a Generative UI Contract" + +**Date:** 2026-08-26 +**Status:** Approved (angle A approved in session; second post of the four-post sequence in `docs/superpowers/specs/2026-08-26-blog-sequence-inject-agent-design.md`) + +## Intent and evidence + +Comparison queries where we already rank: `json render vs a2ui` (16 impressions, position 3.2), `a2ui vs json-render` (14, 11.4), `json-render vs a2ui` (8, 3.5, 25% CTR). The searcher has a decision to make — the highest-intent traffic on the site. Today it lands on the docs page `apps/website/content/docs/render/concepts/json-render-vs-a2ui.mdx`, a neutral mechanical comparison. + +**The post's job:** answer "which one should I pick," not "how do the two differ." Opinionated decision essay; links the docs page for every mechanism it names; must not duplicate its mechanics. + +**Slug:** `json-render-vs-a2ui-choosing` +**File:** `apps/website/content/blog/2026-08-26-json-render-vs-a2ui-choosing.mdx` +**Meta description (≤155 chars):** "A fixed spec is easier to validate; A2UI updates over time and sends actions back. Which contract shape fits your surface." +**No licensing callout** (per Brian's direction on post #11 — dropped as unnecessary). + +## Angle + +Decision essay built on the docs page's central line — *the tradeoff is contract shape, not which renderer is better* — expanded into judgment the neutral reference can't carry: + +- Who owns the UI: the application (fixed spec, validate-before-mount) vs. the agent (live surface, protocol discipline). +- Concrete scenarios walked to a verdict: structured result card → json-render; live itinerary updating mid-conversation → A2UI; form whose submission returns to the agent as a structured action → A2UI; dashboard outside chat → json-render. +- The "ladder" (markdown → fixed spec → live protocol) appears only as a two-line framing device in the intro. +- Exactly ONE paired snippet: a ~5-line json-render spec next to a ~5-line A2UI envelope, to make "contract shape" visible. No other code. Not a tutorial. +- Switching-cost section lowers the stakes: in `@threadplane/chat`, both paths render through the same `views` catalog, so the choice is not a one-way door. + +Rejected angles: same-surface-built-both-ways (code-heavy, re-treads docs mechanics) and a full three-rung-ladder frame (dilutes the head-to-head the queries ask for). + +## Structure + +1. **Lede** (no header): restate the decision; two-line ladder framing; link the docs page (`/docs/render/concepts/json-render-vs-a2ui`) as "the mechanical comparison" early. +2. `## What's actually different?` — contract shape, ownership framing, the single paired snippet. +3. `## When does the fixed spec win?` — validate-before-mount, application-owned event semantics, the card/dashboard scenarios. +4. `## When does the protocol win?` — incremental surfaces, data arriving apart from structure, structured actions back; the itinerary/form scenarios. +5. `## What does it cost to switch?` — shared `views` catalog in chat; choice is revisable; opinionated default flagged as opinion. +6. `## Conclusion` — the one-rule heuristic (if you can validate the whole UI before showing it, start with json-render; live conversation artifact → A2UI), forward links: docs comparison page, `/docs/chat/guides/generative-ui`, `/docs/chat/a2ui/overview`. + +## Voice and register + +Same as post #11: `docs/gtm/voice.md` with the 2026 technical override — H2-as-question, contractions, 1–3-line paragraphs, "Let's" transitions, opinions flagged ("For me," "I think"), no anecdotes, no emoji, no hype, no CTAs. + +## Accuracy requirements (drafting gate) + +- Every mechanism claim verified against the docs page AND source (`libs/render`, `libs/a2ui`, `libs/chat`): the paired snippet must be a valid minimal spec / valid v0.9 envelope per the vendored official A2UI schemas; the shared-`views`-catalog claim verified in `libs/chat` source. +- Verify named members against published 0.0.58 tarballs (main may be ahead of npm). +- Frontmatter conventions per existing posts; tags along the lines of [generative-ui, a2ui, json-render, angular, agentic-ui]. +- A2UI is Google's protocol; if the post names its origin, match how the docs/repo describe it — no speculation about the spec beyond what we implement (v0.9/v0.9.1 line). + +## Out of scope + +- Docs page changes; posts #1 and #12 (own passes later).