Skip to content

feat(landing): AI landing-page builder (json-render + Puck + streaming AI)#324

Draft
guillermoscript wants to merge 9 commits into
masterfrom
chore/add-skill-creator-skill
Draft

feat(landing): AI landing-page builder (json-render + Puck + streaming AI)#324
guillermoscript wants to merge 9 commits into
masterfrom
chore/add-skill-creator-skill

Conversation

@guillermoscript

@guillermoscript guillermoscript commented Jun 8, 2026

Copy link
Copy Markdown
Owner

Summary

AI-assisted landing-page builder for school admins: conversational AI panel, streaming page generation, a json-render layer, and Puck editor refinements.

Scope note: the skill-creator chore that was previously bundled into this branch has been split into its own clean PR — #325. Once #325 merges to master, the duplicate skill-creator commit on this branch becomes a content-identical no-op and drops out of this diff automatically (no force-push needed).

Changes (landing-builder)

  • app/api/landing/generate/route.ts — streaming AI generation endpoint
  • lib/json-render/* — json-render layer
  • lib/puck/*, components/admin/landing-page/{ai-chat-panel,puck-editor}.tsx — Puck editor + AI chat panel
  • components/{call-to-action,faqs,stats}.tsx, components/ui/{animated-shiny-text,marquee,number-ticker}.tsx
  • .claude/skills/ai-landing-builder/* — the landing-builder skill (distinct from skill-creator)
  • messages/{en,es}.json — i18n strings
  • supabase/seed.sql, docs/adr/0001-json-render-puck-landing-builder.md, app/globals.css, components.json, package.json / lockfile, scripts/*

Test plan

  • npm run build passes
  • Admin can open the landing builder and generate a page via the AI panel (streaming)
  • Generated page renders in Puck and persists
  • en/es strings resolve
  • CI green (no checks have run yet)

🤖 Generated with Claude Code

guillermoscript and others added 4 commits June 6, 2026 22:23
Add an AI landing-page generation layer (json-render) in front of the existing
Puck visual editor. Creators describe a page in a sentence; an LLM generates it
constrained to a catalog of our blocks; the result opens in Puck for hand editing.
One component vocabulary, two flows.

Registries & blocks:
- Wire @tailark, @MagicUI, @shadcnblocks, @motion-primitives into components.json
- Add 7 registry-sourced Puck blocks (LogoMarquee, AnimatedStats, ShinyEyebrow,
  StatsBand, FaqSplit, CtaBanner, ContentFeature) + installed source components

json-render pipeline:
- lib/json-render/from-puck-fields.ts: puckFieldsToZod() derives Zod props from
  Puck fields (text/textarea/number/radio/select/array incl. nested)
- scripts/gen-puck-fields-manifest.ts -> puck-fields.generated.json: pure-data
  field manifest so the server-side catalog never bundles the client Puck tree
- lib/json-render/catalog.ts: auto-derives catalog for all 30 generatable blocks
- lib/json-render/to-puck.ts: normalizeSpec() + specToPuckData() bridge
- app/api/landing/generate/route.ts: auth + tenant scoped; prompt -> generateObject
  -> catalog.validate (anti-hallucination) -> Puck Data

Editor wiring:
- components/admin/landing-page/generate-with-ai.tsx: button + dialog, injects the
  generated page via dispatch({type:'setData'}); i18n under puck.ai (en/es)

Docs & scripts:
- docs/adr/0001: architecture decision record
- scripts/json-render-proof.ts, json-render-live-test.ts: pipeline verification

Verified: npm run build passes, tsc 0 errors, proof scripts green. Live OpenAI
call untested (invalid key in env, external).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…builder skill

Improve the json-render → Puck AI landing-page pipeline and harden it after
end-to-end testing with a real model call.

Generation quality:
- Add lib/json-render/authoring-guide.ts (LANDING_AUTHORING_GUIDE) — a shared
  landing-specific prompt suffix that overrides json-render's generic JSONL
  output format and coaches the model to build a complete 6–9 section page
  (Hero → … → CtaBanner) with real copy and consistent accent theming.
  Wired into both the route and the live-test script so they can't drift.
  Turns a thin 5-block stub into a full 8-section page.

OpenAI structured-output fixes (route + live-test):
- elements is an ARRAY of {id,type,propsJson,children}; props are a JSON string
  parsed server-side. Works around OpenAI rejecting z.record (propertyNames)
  and .optional() (strict mode requires every key in `required`).

Crash-proofing the bridge:
- Manifest now carries each block's defaultProps; catalog exports
  DEFAULT_PROPS_BY_TYPE; specToPuckData merges defaults UNDER the AI props so a
  block never receives `undefined` for an array it maps over (Puck's Render does
  not reliably backfill top-level defaults). Fixes the `items.length` render
  crash on generated pages.
- StatsBand guards `items ?? []` as the reference defense-in-depth pattern.

Testing/infra:
- Seed Default School on the enterprise plan (persists across db reset).

Skill:
- Add .claude/skills/ai-landing-builder — SKILL.md (architecture, the 7
  critical invariants, add-a-block steps, testing, debugging cheatsheet,
  production backlog) + references/gotchas.md (the full why behind each gotcha
  with exact code shapes).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Turn the fire-and-forget "Generate with AI" button into a multi-turn chat
assistant inside the Puck editor, and make generation stream.

Chat assistant (two-intent protocol):
- New right-side chat panel (components/admin/landing-page/ai-chat-panel.tsx),
  persistent conversation, applies changes immediately (Puck undo reverts).
- PAGE intent (nothing selected): build/rewrite the whole page → setData.
- BLOCK intent (a block selected in Puck): edit ONLY that block's props →
  replace that one item by id, leaving every other block untouched. The context
  auto-targets the selected block; falls back to whole-page when none selected.
- Both intents validate against the SAME catalog and get the defaultProps
  backfill, so anti-hallucination + crash-proofing apply to block edits too.

Streaming:
- Route uses streamObject and emits NDJSON: `progress` events per block while a
  page builds (first progress ~2.4s vs ~15s blank before), then a single
  `page`/`block` result. Block edits use a fast generateObject.

Hardening (AI SDK v6 best practices, verified against bundled docs):
- abortSignal wired from the client (closing the panel cancels the paid call),
  NoObjectGeneratedError handling (logs cause/text/usage), token usage logged,
  input length + message-count caps.

- Replace generate-with-ai.tsx with the chat panel; export cleanProps from
  to-puck.ts; new chat i18n keys (en/es); add json-render:stream-test script.

Verified: tsc clean; in-browser whole-page chat turn AND block-only hero edit
both applied correctly with other blocks untouched; zero console errors on a
clean build.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add the anthropics/skills skill-creator skill and register it in
skills-lock.json.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@guillermoscript guillermoscript changed the title chore: add skill-creator skill feat(landing): AI landing-page builder (json-render + Puck + streaming AI) Jun 8, 2026
End-to-end browser recording (Claude Chrome extension): admin opens the
Puck builder → "Generate with AI" → prompt → streamed 3-section page
(hero + features + CTA) renders live in the canvas.
@guillermoscript

Copy link
Copy Markdown
Owner Author

✅ AI landing-page generation — verified end-to-end

Recorded live against this branch (dev server on code-academy.lvh.me:3000, real OpenAI key). Admin opens the Puck builder → Generate with AI → types a prompt → the assistant streams a 3-section page (hero + features + final CTA) that renders straight into the canvas.

AI landing-page generation

What was checked

Check Result
Builder page loads for admin GET /dashboard/admin/landing-page200
Page generation (streamObject → OpenAI → json-render → Puck) 200, ~8.6s — streamed progress then a valid 3-block page
Block refinement (selectedBlock → generateObject) 200, ~5.4s — edited the selected block only
Auth guard (no session) 401
Input guard (empty messages) 400
Production build (npm run build) passes (TS strict + 171/171 static pages)

Generated output was well-formed Puck data ({content:[…], root:{props:{}}}): HeroBlock + FeaturesGrid (3 items) + CtaBanner.

GIF recorded with the Claude Chrome extension; committed under docs/screenshots/.

guillermoscript and others added 3 commits June 8, 2026 22:57
The /api/landing/generate endpoint checked auth + tenant but had no plan
or rate-limit guard, so any authenticated admin — including free-plan
tenants — could call it directly and burn OpenAI tokens, bypassing the
UI's PAID_PLANS gate.

- Enforce the same PAID_PLANS gate server-side via get_plan_features →
  403 for free-plan tenants (mirrors landing-pages-client canUseBuilder).
- Add a per-user rate limit (20 / 5 min) using lib/rate-limit.ts, checked
  before the plan gate and the model call so it actually caps cost.
- seed.sql: keep the Default tenant on the FREE plan (was bumped to
  enterprise) so plan-gating is exercised in local dev / E2E; use a paid
  tenant (Code Academy, pro) when gated features are needed.

Verified: paid→200, free→403, unauth→401, >20/window→429; build passes.
…es (#324)

- Add lib/json-render/to-puck.test.ts (node:test via tsx, new `test:unit`
  script): 15 cases covering arraySpecToSpec / normalizeSpec / cleanProps
  and specToPuckData — including the fallbacks that turn the
  valid-but-misstructured specs LLMs emit (missing root id, a real section
  used as root) into a full ordered page, plus defaults backfill/override.
- Localize the AI assistant's replies/errors: the route's `reply`/`error`
  strings were English-only. The client now builds them from the structured
  stream events (page `blocks`, block `blockType`, error `status`) via
  next-intl, with new puck.ai keys (builtPage, updatedBlock, errorPlan,
  errorRateLimit, errorInvalid) in en + es.

Build passes; test:unit 15/15 green.
The landing-ai-generation-324.gif (1.4MB) was committed to the branch only
to embed QA evidence in the PR comment. That comment references the GIF via a
permanent commit-SHA raw URL (commit e555275), so the embed survives this
removal — but the binary doesn't need to ride into master via the feature diff.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant