Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
d960b10
feat: add preset-organism.columns for editor-composed multi-column la…
odenirdev Jul 20, 2026
fbcec58
docs: composition-builder design spec (JSON tree builder replaces DZ …
odenirdev Jul 20, 2026
71f32ec
docs(spec): unify layout-node attrs into one shared curated container…
odenirdev Jul 20, 2026
56012ab
feat(shared): publishable press-shared + PressTree wire contract
odenirdev Jul 20, 2026
660a57a
feat(shared): sanitizing PressTree runtime validator
odenirdev Jul 20, 2026
5c2f582
feat(cms): preset-molecule.link descriptor + curated plain-text atoms…
odenirdev Jul 20, 2026
0e93a11
feat(cms)!: page.body + site-setting.pageDefaults become the builder …
odenirdev Jul 20, 2026
651c71d
feat(cms): preset-layout descriptors + full-palette schema serializat…
odenirdev Jul 20, 2026
46d1071
test(cms): cover preset-layout descriptor registration + container/ro…
odenirdev Jul 20, 2026
a0f1f7d
feat(cms): serve-time hydration of media assetId and page documentId …
odenirdev Jul 20, 2026
1dc540f
fix(cms): batch page-list hydration into one query pair + serve-hydra…
odenirdev Jul 21, 2026
8e6537a
feat(cms): lifecycle validation backstop for composition-tree writes
odenirdev Jul 21, 2026
29e3add
feat(cms): seed pageDefaults once with bare navbar/footer block nodes
odenirdev Jul 21, 2026
8025316
feat(cms-admin): pure immutable tree operations with structural invar…
odenirdev Jul 21, 2026
d8565f4
feat(cms-admin): registry-driven form model (descriptors, container a…
odenirdev Jul 21, 2026
e2721a7
feat(cms-admin): structural composition-builder custom field (tree + …
odenirdev Jul 21, 2026
f4f92f2
feat(web): PressLink + resolveLink — the engine's one link resolver
odenirdev Jul 21, 2026
28013ba
feat(web)!: plain-text atoms + link-descriptor button; blocks AST lea…
odenirdev Jul 21, 2026
0e1a4c8
refactor(web)!: hero/cta/navbar consume the shared link descriptor vi…
odenirdev Jul 21, 2026
1910964
feat(web): curated container-attr → layout-primitive mappings for the…
odenirdev Jul 21, 2026
39e8aec
feat(web)!: pageDefaults slots + one-point engine-block hydration (re…
odenirdev Jul 21, 2026
331efed
feat(web)!: TreeRenderer replaces BlockRenderer; columns organism and…
odenirdev Jul 21, 2026
66282c8
feat(web)!: host renders the tree shell; prose rail rescoped to direc…
odenirdev Jul 21, 2026
06e2071
feat(web)!: generator emits tree-world types (PageBody = PressTree, P…
odenirdev Jul 21, 2026
de9707f
feat(cli)!: seed the demo home as a PressTree + pageDefaults navigati…
odenirdev Jul 21, 2026
1a454ad
fix(cli): publish-readiness reflects press-shared as a published runt…
odenirdev Jul 21, 2026
3291338
fix(shared): drop files allowlist so .npmignore excludes tests from t…
odenirdev Jul 22, 2026
c178078
docs+chore!: playground regenerated for the composition builder; chan…
odenirdev Jul 22, 2026
37e5b12
fix(cms): serialize-schema enumerates the registry via keys()+get(), …
odenirdev Jul 22, 2026
7b64cc5
fix(cms): externalize react/react-dom so the admin builder uses the h…
odenirdev Jul 22, 2026
a75dac9
chore(repo)!: pin the workspace to React 19 to match Strapi 5's admin
odenirdev Jul 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions .changeset/columns-organism.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
---
'@ogs-tech/press-cms': minor
'@ogs-tech/press-web': minor
---

feat: `preset-organism.columns` — the first editor-composed layout block

`@ogs-tech/press-cms`:

- **New page-body organism `preset-organism.columns`** (admitted statically in
`page/schema.json`, body only — the hero/cta placement rule). Layout controls
are flat CLOSED enums on the organism (the `hero.align` precedent):
`ratio` (`50-50 | 33-67 | 67-33 | 33-33-33 | 25-25-25-25`), `gap`
(`compact | normal | spacious`), `verticalAlign` (`top | center | bottom`).
The raw `GridGap`/`Span` scales are never exposed to the CMS.
- **New nested-only molecule `preset-molecule.column`** (rich text + optional
image + optional `preset-atom.button` — the `navbar.cta` nesting pattern);
repeatable 2–4 per block, never a DZ member. `preset-layout` stays reserved
and empty: the top-level block is a content ORGANISM that consumes the grid
internally, so "layout is never an editor-placed top-level block" holds.
- **`buildBodyPopulate` deep-populates the columns chain.** `populate: '*'` is
shallow; a column's `image`/`button` sit TWO levels below the DZ member and
silently came back empty — the exact shape that already forced the navbar's
deep populate in the chrome zones.
- Serializer and generator needed **zero code change** — the BFS follows the
two-level chain (`columns → column → button`) generically; new tests pin that.

`@ogs-tech/press-web`:

- **New `Columns` renderer** (`preset-organism.columns` in `organismBlocks`,
overridable like every organism) built on Container/Grid/Column, mirroring
the Hero: stacks at base, `25-25-25-25` goes 2×2 on md before 4-up on lg,
`spacious` gap is tier-scaled (never a flat `lg` — the 11-gap 528px floor).
Tolerant: no columns renders nothing; an empty column keeps its cell; a
column beyond the ratio's slots reuses the last span instead of dropping.
- Cells render rich text via `renderBlocks` + raw `<img>`/`<a>` — NOT the atom
components, whose `data-block="preset-atom.*"` would match the prose-width
rule at any depth and fight the cell's own width.
- theme.css: columns section styles + the per-column button joins the shared
button family (all variants and states).
- New exported types `PresetOrganismColumns` / `PresetMoleculeColumn`; the CLI
seed demonstrates a `33-33-33` block with a nested secondary button.
32 changes: 32 additions & 0 deletions .changeset/composition-builder.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
---
'@ogs-tech/press-shared': major
'@ogs-tech/press-cms': major
'@ogs-tech/press-web': major
'@ogs-tech/create-press': major
---

feat!: composition builder — a JSON PressTree replaces the dynamic-zone mechanism engine-wide

BREAKING (wire + palette + API), no data migration (pre-release):

- `page.body` and the new `site-setting.pageDefaults` store a `PressTree` /
`Node[]` slots via the `plugin::press-cms.builder` JSON custom field; the
page-body and chrome Dynamic Zones (and `dz-populate`) are gone.
- Per-page layout root: header/footer slots (`inherit | none | custom`), rows
with 1–4 ratio-bound columns, full recursion, adopter `custom-*` blocks
anywhere. Curated `container` attrs (`width`/`gap`/`verticalAlign`).
- `@ogs-tech/press-shared` is now a PUBLISHED runtime dependency of press-web:
it ships the tree types + the sanitizing `validatePressTree` validator used
by the cms write path and the web render path.
- Palette: new `preset-molecule.link` (label/page/url/newTab) referenced by
button/hero/cta/navbar; `preset-molecule.nav-item`, `preset-molecule.column`,
`preset-organism.columns` removed. Text atoms store curated PLAIN TEXT
(`content: text`) — the Strapi blocks AST leaves the wire (`renderBlocks`
removed). `preset-layout.{container,row,column}` descriptors drive the
builder's layout forms.
- Serve-time hydration: media `{ assetId }` and page `{ documentId }` refs are
resolved server-side (fresh URLs/slugs; rename-safe internal links).
- Web: `TreeRenderer` (header/main/footer shell) replaces `BlockRenderer`;
`PressLink`/`resolveLink` are the one link resolver; prose rail rescoped to
direct `<main>` children; `PageBody` generates as `PressTree`; the
`HeaderBlocks`/`FooterBlocks` unions are gone.
357 changes: 257 additions & 100 deletions CLAUDE.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion apps/playground/packages/cms/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,6 @@
"npm": ">=6.0.0"
},
"strapi": {
"uuid": "3df03067-2b3d-45b9-a1e5-f942710a84a4"
"uuid": "302c4e43-d61c-44bd-8649-494d125f6d90"
}
}
126 changes: 126 additions & 0 deletions apps/playground/packages/cms/scripts/seed-content.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
// cms/scripts/seed-content.mjs — the demo content as PURE DATA, importable by
// both seed.mjs (to write it) and the engine's CLI test suite (to validate the
// tree shape against the shared validator — the seed-regression guard).
// Plain-text content throughout (curated `content: text` decision 2026-07-20).
import { randomUUID } from 'node:crypto';

export const REPO_URL = 'https://github.com/ogs-tech/press';
export const PRESS_SITE_URL = 'https://useogs.com/press';
export const NPM_CREATE_URL = 'https://www.npmjs.com/package/@ogs-tech/create-press';

export const SITE_SETTINGS = {
name: 'Press',
url: 'http://localhost:3000',
locale: 'en',
seo: {
titleTemplate: '%s · Press',
title: 'Press',
description: 'A press-powered site, server-rendered end-to-end.',
},
themeColors: {
primary: '#119350',
accent: '#D9A12C',
secondary: '#3D5CC2',
ink: '#142036',
surface: '#FAF8F3',
muted: '#7A7E89',
danger: '#C0392B',
onPrimary: '#FFFFFF',
border: 'rgba(20,32,54,0.12)',
},
themeRadius: { xs: '6px', sm: '10px', md: '14px', lg: '20px' },
};

const block = (component, data = {}) => ({ id: randomUUID(), type: 'block', component, data });
const column = (children, container) => ({ id: randomUUID(), type: 'column', children, ...(container ? { container } : {}) });
const row = (ratio, children, container) => ({ id: randomUUID(), type: 'row', ratio, children, ...(container ? { container } : {}) });

/**
* The demo home as a PressTree (Spec §4 seeds): hero → prose atoms → a 50-50
* row whose right column nests ANOTHER row (the recursion demo) → separator/
* button/spacer → cta banner → adopter callout. Chrome inherits pageDefaults.
*/
export const buildHomeBody = ({ heroAssetId }) => ({
version: 1,
root: {
type: 'layout',
header: { mode: 'inherit' },
footer: { mode: 'inherit' },
children: [
block('preset-organism.hero', {
eyebrow: 'Press engine',
title: 'Hello from press',
subtitle: 'A press-powered site, server-rendered end-to-end.',
image: { assetId: heroAssetId },
cta: { label: 'Read the docs', url: REPO_URL },
align: 'left',
}),
block('preset-atom.heading', { text: 'What ships in the box', level: '2' }),
block('preset-atom.paragraph', {
content: 'This prose lives in the CMS and renders as static HTML — no client hydration.',
}),
block('preset-atom.list', {
format: 'unordered',
content: [
'Atomic text blocks — paragraph, heading, list and quote.',
'Media & structure — image, button, separator and spacer.',
'Rows and columns — recursive layout composed in the admin.',
'Your own custom-* blocks, usable anywhere in the tree.',
].join('\n'),
}),
block('preset-atom.quote', {
content: 'The contract is HTML on the server.',
citation: 'The press engine',
}),
// The composition mechanism itself: a 50-50 row whose RIGHT column nests
// another 50-50 row — full recursion on the demo page.
row('50-50', [
column([
block('preset-atom.paragraph', {
content: 'Editor-composed layout — rows and columns arranged in the admin, rendered on the engine grid.',
}),
]),
column([
row('50-50', [
column([block('preset-atom.paragraph', { content: 'Columns nest rows.' })]),
column([block('preset-atom.paragraph', { content: 'Rows nest columns.' })]),
]),
], { verticalAlign: 'center' }),
], { gap: 'normal' }),
block('preset-atom.separator', { variant: 'line' }),
block('preset-atom.button', {
link: { label: 'Star on GitHub', url: REPO_URL, newTab: true },
variant: 'secondary',
}),
block('preset-atom.spacer', { size: 'md' }),
block('preset-organism.cta', {
title: 'Ready to press publish?',
subtitle: 'Scaffold a site, open the admin, and ship your first page in minutes.',
button: { label: 'Scaffold your site', url: PRESS_SITE_URL },
align: 'center',
}),
block('custom-organism.callout', {
message: 'Adopter callout renders via the Project-zone block map',
variant: 'success',
}),
],
},
});

/**
* Demo navigation for Site Settings pageDefaults: the Home item is a PAGE REF
* ({ documentId }) — exercising the reference-hydration path end-to-end — plus
* an external link and a CTA button. The footer keeps the bare seeded node.
*/
export const buildPageDefaults = ({ homeDocumentId }) => ({
header: [
block('preset-organism.navbar', {
items: [
{ label: 'Home', page: { documentId: homeDocumentId } },
{ label: 'GitHub', url: REPO_URL, newTab: true },
],
cta: { link: { label: 'Get started', url: NPM_CREATE_URL }, variant: 'primary' },
}),
],
footer: [block('preset-organism.footer', {})],
});
Loading
Loading