Skip to content

Commit d2e5ce7

Browse files
bloveclaude
andauthored
feat(website): add 'json-render vs A2UI' blog post (#837)
* docs: design spec for json-render vs A2UI blog post Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: implementation plan for json-render vs A2UI post Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(website): add 'json-render vs A2UI' blog post Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(website): tighten json-render vs A2UI post per review Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(website): correct submit-validation claim in A2UI post Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 9064d45 commit d2e5ce7

3 files changed

Lines changed: 305 additions & 0 deletions

File tree

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
---
2+
title: 'json-render vs A2UI: Choosing a Generative UI Contract'
3+
description: 'A fixed spec is easier to validate; A2UI updates over time and sends actions back. Which contract shape fits your surface.'
4+
date: 2026-08-26
5+
tags: [generative-ui, a2ui, json-render, angular, agentic-ui]
6+
author: brian
7+
featured: false
8+
draft: false
9+
---
10+
11+
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.
12+
13+
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.
14+
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?
15+
16+
## What's actually different?
17+
18+
This isn't a renderer shootout. The tradeoff is contract shape.
19+
20+
With json-render, the contract is _application-owned_.
21+
You define the schema, you validate the spec before anything mounts, and your handlers own what every event means.
22+
23+
With A2UI, the surface is _agent-owned_.
24+
The agent creates it, keeps updating it over the life of the conversation, and gets structured actions back when the user interacts.
25+
26+
Let's look at the same order card in both shapes.
27+
First as a json-render spec:
28+
29+
```json
30+
{
31+
"root": "card",
32+
"elements": {
33+
"card": { "type": "Card", "props": {}, "children": ["body"] },
34+
"body": { "type": "Column", "props": {}, "children": ["title", "total"] },
35+
"title": { "type": "Text", "props": { "text": "Order #1042", "variant": "h4" } },
36+
"total": { "type": "Text", "props": { "text": "$118.00" } }
37+
}
38+
}
39+
```
40+
41+
And as an A2UI JSONL stream:
42+
43+
```jsonl
44+
{"version":"v0.9","createSurface":{"surfaceId":"order","catalogId":"https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json"}}
45+
{"version":"v0.9","updateComponents":{"surfaceId":"order","components":[{"id":"root","component":"Text","text":{"path":"/total"}}]}}
46+
{"version":"v0.9","updateDataModel":{"surfaceId":"order","path":"/total","value":"$118.00"}}
47+
```
48+
49+
(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.)
50+
51+
One is a document you can validate before you show it; the other is a conversation you subscribe to.
52+
53+
## When does the fixed spec win?
54+
55+
Whenever the UI is one answer — the agent responds once, the UI renders once, and it's done.
56+
57+
Let's walk two scenarios to a verdict.
58+
59+
A _structured result card_ in chat: the agent looks up an order and answers with a summary card.
60+
Nothing about that card changes after it lands, so there's no ongoing surface to manage.
61+
Verdict: json-render.
62+
You get to validate the whole spec before mount, and your handlers — not the protocol — decide what a click means.
63+
64+
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.
65+
Verdict: json-render again, driven directly through `<render-spec>`.
66+
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.
67+
68+
One security note: in both paths, the registry is doing allowlist duty.
69+
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.
70+
71+
## When does the protocol win?
72+
73+
Whenever the surface has to live past its first render.
74+
75+
Let's take the itinerary case.
76+
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.
77+
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.
78+
79+
Now a form the _agent_ needs back.
80+
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.
81+
Either way, user input flows back through the protocol, not through handlers you wire yourself.
82+
83+
So the reasons to step up: incremental surfaces, data arriving separately from structure, and user actions as first-class protocol messages.
84+
85+
The honest cost is protocol discipline.
86+
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.
87+
88+
## What does it cost to switch?
89+
90+
Inside chat, less than you'd think.
91+
92+
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.
93+
So the choice is per-surface, not per-app — and it's revisable.
94+
95+
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.
96+
You're not locked in either way, so the cheap contract is the right place to begin.
97+
98+
## Conclusion
99+
100+
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.
101+
102+
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.
103+
Pick a surface you're building this week, run it up the ladder, and let me know where it lands.
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
# "json-render vs A2UI: Choosing a Generative UI Contract" Blog Post Implementation Plan
2+
3+
> **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.
4+
5+
**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.
6+
7+
**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.
8+
9+
**Tech Stack:** MDX blog content, Next.js website (`apps/website`), vitest for content validation.
10+
11+
**Spec:** `docs/superpowers/specs/2026-08-26-json-render-vs-a2ui-post-design.md`
12+
**Branch:** `blove/json-render-vs-a2ui-post` (off main at `9064d456`)
13+
14+
---
15+
16+
## Verified facts (source of truth)
17+
18+
- **json-render `Spec`** (`@json-render/core`, `dist/store-utils-*.d.ts:380`): `{ root: string; elements: Record<string, UIElement>; state?: Record<string, unknown> }`. `UIElement`: `{ type: string; props: P; children?: string[]; visible?; on?; repeat? }`. Docs describe it as a "flat UI tree structure (optimized for LLM generation)".
19+
- **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).
20+
- **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."
21+
- **Chat detection** (docs page "Chat Detection"): text → markdown; leading `{` → json-render; leading `---a2ui_JSON---` → A2UI JSONL.
22+
- **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."
23+
- **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.
24+
25+
---
26+
27+
### Task 1: Author the post
28+
29+
**Files:**
30+
- Create: `apps/website/content/blog/2026-08-26-json-render-vs-a2ui-choosing.mdx`
31+
32+
Slug derives from filename minus date → `/blog/json-render-vs-a2ui-choosing`.
33+
34+
- [ ] **Step 1: Create the file with this exact frontmatter**
35+
36+
```yaml
37+
---
38+
title: 'json-render vs A2UI: Choosing a Generative UI Contract'
39+
description: 'A fixed spec is easier to validate; A2UI updates over time and sends actions back. Which contract shape fits your surface.'
40+
date: 2026-08-26
41+
tags: [generative-ui, a2ui, json-render, angular, agentic-ui]
42+
author: brian
43+
featured: false
44+
draft: false
45+
---
46+
```
47+
48+
Description is 120 chars (≤155). **No licensing callout** — dropped per Brian's direction.
49+
50+
- [ ] **Step 2: Write the body**
51+
52+
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.
53+
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:
54+
55+
json-render (one fixed spec):
56+
```json
57+
{
58+
"root": "card",
59+
"elements": {
60+
"card": { "type": "Card", "props": { "title": "Order #1042" }, "children": ["total"] },
61+
"total": { "type": "Text", "props": { "text": "$118.00" } }
62+
}
63+
}
64+
```
65+
66+
A2UI (a stream of envelopes, JSONL):
67+
```json
68+
{"version":"v0.9","createSurface":{"surfaceId":"s1","catalogId":"basic"}}
69+
{"version":"v0.9","updateComponents":{"surfaceId":"s1","components":[...]}}
70+
{"version":"v0.9","updateDataModel":{"surfaceId":"s1","...":"..."}}
71+
```
72+
73+
**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.
74+
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.
75+
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.
76+
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…").
77+
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.
78+
79+
- [ ] **Step 3: Voice pass**
80+
81+
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.
82+
83+
- [ ] **Step 4: Accuracy pass**
84+
85+
- 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`).
86+
- 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.
87+
88+
- [ ] **Step 5: Commit**
89+
90+
```bash
91+
git add apps/website/content/blog/2026-08-26-json-render-vs-a2ui-choosing.mdx
92+
git commit -m "feat(website): add 'json-render vs A2UI' blog post"
93+
```
94+
95+
---
96+
97+
### Task 2: Validate
98+
99+
- [ ] **Step 1: Frontmatter + description length**
100+
101+
```bash
102+
cd apps/website && node -e "
103+
const matter = require('gray-matter');
104+
const fs = require('fs');
105+
const f = matter(fs.readFileSync('content/blog/2026-08-26-json-render-vs-a2ui-choosing.mdx','utf8'));
106+
console.log('desc length:', f.data.description.length);
107+
if (f.data.description.length > 155) throw new Error('description too long');
108+
if (!f.data.title || !f.data.date || f.data.author !== 'brian') throw new Error('frontmatter incomplete');
109+
console.log('OK');
110+
"
111+
```
112+
113+
Expected: length ≤155, `OK`.
114+
115+
- [ ] **Step 2: Website test suite** (`nx test website` does not exist):
116+
117+
```bash
118+
cd apps/website && npx vitest run --config vite.config.mts
119+
```
120+
121+
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).
122+
123+
- [ ] **Step 3: Render check**`npx next dev` on port 3111 from `apps/website` (background), then curl:
124+
- `/blog/json-render-vs-a2ui-choosing` → 200, contains the post `<title>`, both code blocks render as `<pre data-language="json"`, meta description matches frontmatter
125+
- `/blog` lists the post
126+
Kill the server, verify port free, revert any `next-env.d.ts` side-effect edit.
127+
128+
- [ ] **Step 4: Commit fixes** (skip if none):
129+
130+
```bash
131+
git add -A apps/website/content/blog/ && git commit -m "fix(website): render fixes for json-render vs A2UI post"
132+
```
133+
134+
---
135+
136+
### Task 3: PR
137+
138+
- [ ] **Step 1: Push and open PR**
139+
140+
```bash
141+
git push -u origin HEAD
142+
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).
143+
144+
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.
145+
146+
🤖 Generated with [Claude Code](https://claude.com/claude-code)"
147+
```
148+
149+
- [ ] **Step 2: Merge on green + verify**
150+
151+
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.

0 commit comments

Comments
 (0)