Skip to content

feat!: composition builder — JSON PressTree replaces the dynamic-zone mechanism#13

Open
odenirdev wants to merge 31 commits into
mainfrom
feat/columns-block
Open

feat!: composition builder — JSON PressTree replaces the dynamic-zone mechanism#13
odenirdev wants to merge 31 commits into
mainfrom
feat/columns-block

Conversation

@odenirdev

Copy link
Copy Markdown
Contributor

Composition builder — a JSON PressTree replaces the dynamic-zone mechanism engine-wide

Per-page layout root (header/footer/children slots), rows with 1–4 ratio-bound columns, full recursion, adopter custom-* blocks anywhere. Stored via one plugin::press-cms.builder JSON custom field; a structural tree-editor drives it in the admin.

BREAKING (wire + palette + API), no data migration (pre-release). 25 commits, executed task-by-task from docs/superpowers/plans/2026-07-20-composition-builder.md.

Highlights

  • Storage: page.body (full PressTree) and site-setting.pageDefaults ({header,footer} slots) are JSON custom fields; the page-body + chrome Dynamic Zones and dz-populate are gone.
  • @ogs-tech/press-shared is now a published runtime dependency: it ships the tree types + the sanitizing validatePressTree/validateNodeArray validators (cms bundles into dist; web depends + host transpiles). Strict-write (cms lifecycle rejects errors OR warnings) / tolerant-read (web sanitizes).
  • Palette: new preset-molecule.link (label/page/url/newTab) referenced by button/hero/cta/navbar; nav-item/molecule.column/organism.columns retired. Text atoms store curated plain text (content: text) — the Strapi blocks AST leaves the wire. preset-layout.{container,row,column} descriptors drive the builder's layout forms.
  • Serve-time hydration: media {assetId} → url/dims, page {documentId} → slug — resolved server-side (rename-safe internal links, no populate depth to fight), batched one query pair per request.
  • Web: TreeRenderer (<header>/<main>/<footer> shell) replaces BlockRenderer; PressLink/resolveLink is the one link resolver (dangerous-protocol-safe); prose rail rescoped to direct <main> children; PageBody generates as PressTree; PressPageRef added; HeaderBlocks/FooterBlocks unions gone. Block-override contract components={{ 'preset-organism.hero': MyHero }} unchanged.

Automated verification (all green)

  • pnpm build 6/6 incl. the playground Next build (SSG 3/3)
  • pnpm -r test — web 260, cms 67, cli 29, shared 11
  • typechecks clean (shared/cli/web + cms test:ts:back/test:ts:front)
  • pnpm pack:check exit 0 — no *.test.ts in any tarball

Plan gaps caught & fixed during execution

  • Batched hydratePageDocs to one query pair (was a plan-mandated N+1 on the unpaginated /pages route) + added serve-hydrated.test.ts.
  • Moved press-shared devDeps→deps in web (Decision 3 gap); host transpiles it.
  • footer.tsx inline-typed after ResolvedChromeFooter deletion (Task 14 migrated navbar but not footer).
  • publish-readiness made target-aware (press-shared now published; workspace: allowed to co-published targets).
  • Dropped files allowlist from press-shared so .npmignore keeps tests out of the tarball (surfaced by pack:check).
  • Generator keys Page.body off type==='customField' alone (serialize strips the id) — production-shape guard test added.

Manual review checklist (not covered by automated tests — please verify on pnpm dev)

  • /api/press/schema has tree:{version:1}; /api/pages/home body.version===1, hero image carries a hydrated url, navbar pageDefaults.header item has page:{documentId,slug:"home"}.
  • localhost:3000 renders navbar / hero+image / prose atoms at ~72ch / the recursive 50-50 row / cta / callout / footer.
  • Admin Composition field: add a paragraph in a column → Save → change appears (≤60s ISR). Pick a hero image via the media dialog (if the dialog key mismatches, the assetId fallback input must show).
  • Site Settings pageDefaults slots editor; emptying header → inheriting pages render bare.
  • CSS: prose-rail direct-child rescope, cell verticalAlign center/end, no main[data-press-stack] ↔ organism margin-block double-gap.

Known cosmetic note

The playground regen changed the cms Strapi uuid (the plan expected it preserved) — harmless dev instance id; can restore on request.

🤖 Generated with Claude Code

odenirdev and others added 30 commits July 20, 2026 16:04
…youts

- Introduced `preset-organism.columns`, allowing editors to create 2-4 column layouts with rich text, images, and buttons.
- Added `preset-molecule.column` as a nested component within `preset-organism.columns`, supporting rich text, optional images, and buttons.
- Implemented deep population in `buildBodyPopulate` to ensure nested components are correctly populated.
- Updated serializers and generators to handle the new two-level nesting without requiring code changes.
- Created a new `Columns` renderer that manages layout and rendering of columns based on specified ratios and gaps.
- Enhanced theme styles to accommodate the new columns layout and button styles.
- Added comprehensive tests for the new columns functionality, ensuring correct rendering and behavior.
…mechanism)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… group

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…; retire nav-item/column/columns

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…JSON custom field; Dynamic Zones and dz-populate removed

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ion with tree contract version

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

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…refs in composition trees

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

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

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…pplicability, palette groups)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…slots modes, registry-driven forms)

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

Task 13 of the composition-builder refactor: rewrites the text atoms
(paragraph/list/quote) to render curated plain-text strings instead of the
Strapi blocks AST, moves the button atom onto the shared PressLink/PressLinkData
resolver, deletes the in-house blocks-AST renderer, and rewrites types/base.ts
so PageBody/Page describe the PressTree wire shape from @ogs-tech/press-shared.
Every PresetAtom*/PresetOrganism* type drops __component/id (they are tree
`data` objects now, not DZ rows).

Also fixes heading.test.ts and map-page.test.ts fixtures to match the new type
contracts (PresetAtomHeading, PageBody) — both are direct, mechanical fallout of
this task's own produced types, not part of the Task 14/17 migration debt.

The full pnpm typecheck still reports errors in sections/hero.tsx, cta.tsx,
columns.tsx, columns.test.ts (and their downstream block-renderer.test.tsx
runtime failure) — expected, deferred to Tasks 14/17 per the brief.

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

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

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

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

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…t main children; cell/stack CSS

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ressPageRef, no DZ unions)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…on; seed-shape regression guard

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ime dep (workspace: allowed to co-published targets)

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

A `files` allowlist makes npm stop honoring .npmignore, so the vitest suite
shipped in the published tarball. Match press-web's proven pattern (no `files`
field; .npmignore alone) — the CLI publish-readiness guard documents this trap.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…geset + CLAUDE.md architecture rewrite

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

GET /api/press/schema 500'd at runtime ("registry.entries is not a function"):
Strapi's components registry is not a Map — it exposes keys()/get()/getAll(), no
entries(). The `as Map<string, any>` cast let `registry.entries()` typecheck AND
pass a Map-based unit fake, so the bug only surfaced on boot (type-sync). Iterate
with keys()+get(); type the registry structurally so a future .entries() fails to
compile; reshape the test fake to the real registry API so this class can't regress.

Verified end-to-end: schema 200 (tree.version=1), /api/pages/home hydrates the
hero image assetId -> url/dims.

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

The builder custom field crashed the Strapi admin with "Cannot read properties of
null (reading 'useState')": @strapi/sdk-plugin's vite build externalizes only the
plugin's dependencies + peerDependencies, and cms declared neither react nor
react-dom, so the admin chunk BUNDLED the workspace-hoisted React 18 — a second
React instance beside Strapi 5.48's admin React 19 → null hooks dispatcher.
Declaring them as peerDependencies externalizes them; the rebuilt chunk now
imports react (not a bundled copy). Added a guard test since nothing in
build/test/typecheck catches this — only opening the admin does.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Root pnpm.overrides pinned React 18, while Strapi 5.48's admin runs React 19 — a
split that (with the plugin bundling its own React) crashed the admin. Pin
react/react-dom to Strapi's 19.2.7 across the workspace so the Next host, engine
packages, and Strapi admin all share ONE React (single 19.2.7 resolved). Bump
press-web dev/@types to 19; regenerate the CLI scaffold's react pin to ^19.2.7.
React 19 fallout was minimal: two `JSX.Element` refs needed `import type { JSX }`
(@types/react 19 dropped the global JSX namespace). Full gate green under React 19
(web 260, cms 68, cli 29, shared 11; all typechecks; pack:check). CLAUDE.md
documents the pin + the plugin react-externalization contract.

Co-Authored-By: Claude Opus 4.8 (1M context) <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