Add pages table + block registry scaffold (page builder step 2)
New `pages` table: slug/title/blocks(JSON-as-text)/status/protected, author FK, grouped SEO metadata + layout/nav settings columns (added up front per spec — cheap now, painful to retrofit), published_at mirroring posts. Block registry scaffold, server and client, defining the pattern without any block types yet (Wave 1 lands in step 3): - server/src/blocks: registry (register/get/list, reserved envelope keys, container metadata) + validateBlocks (authoritative save-time gate: envelope, registered-type, per-block schema, one-level nesting cap) + index entrypoint that will register Wave 1 defs. - client/src/blocks: mirror registry carrying renderer/editor/palette + makeBlockId, plus index entrypoint. Verified: schema applies idempotently against the dev DB (pages table + indexes present); validator exercised for empty/non-array/unknown-type/ bad-envelope/duplicate-id/nested-container cases. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
19
client/src/blocks/index.js
Normal file
19
client/src/blocks/index.js
Normal file
@@ -0,0 +1,19 @@
|
||||
// Client block registry entrypoint. Importing this module registers every
|
||||
// browser-side block definition (renderer + editor + palette entry) exactly
|
||||
// once, then re-exports the registry API. The page builder and the public page
|
||||
// renderer should import from HERE, not ./registry, so the definitions are
|
||||
// loaded before anything reads the registry.
|
||||
//
|
||||
// Wave 1 definitions are registered below as each block is built (spec build
|
||||
// order step 3), one import per block.
|
||||
|
||||
export * from './registry'
|
||||
|
||||
// ── Wave 1 block definitions ──────────────────────────────────────────
|
||||
// import './types/heading' // added in step 3
|
||||
// import './types/richText'
|
||||
// import './types/image'
|
||||
// import './types/twoColumn'
|
||||
// import './types/cta'
|
||||
// import './types/divider'
|
||||
// import './types/quote'
|
||||
84
client/src/blocks/registry.js
Normal file
84
client/src/blocks/registry.js
Normal file
@@ -0,0 +1,84 @@
|
||||
// Block registry (client side) — mirrors the server registry
|
||||
// (server/src/blocks/registry.js) but carries the browser-only concerns: the
|
||||
// React renderer, the admin edit form, and the palette icon/label. The page
|
||||
// builder's palette, drag-reorder canvas, per-block edit panel, and the public
|
||||
// page renderer all read from this registry, so adding a block later is one
|
||||
// entry here (plus its server-side schema entry) rather than edits scattered
|
||||
// across the builder and renderer.
|
||||
//
|
||||
// A registered definition looks like:
|
||||
// {
|
||||
// type: 'heading', // must match the server registry type
|
||||
// version: 1, // must match the server schema version
|
||||
// label: 'Heading', // palette display name
|
||||
// icon: 'heading', // palette icon key
|
||||
// component: HeadingBlock, // renderer: (props) => JSX
|
||||
// editor: HeadingEditor, // admin edit form: ({ props, onChange }) => JSX
|
||||
// defaults: () => ({ ... }), // starting props when a block is added
|
||||
// container: false, // true only for two_column
|
||||
// containerSlots: [], // ['left','right'] for two_column
|
||||
// }
|
||||
//
|
||||
// This module only defines the pattern; Wave 1 definitions register via
|
||||
// ./index.js as each block is built (spec build order step 3).
|
||||
|
||||
const registry = new Map()
|
||||
|
||||
// Kept in sync with the server's RESERVED_KEYS — the only top-level keys on a
|
||||
// stored block object. Exported so the builder can construct envelopes without
|
||||
// hard-coding the shape.
|
||||
export const RESERVED_KEYS = ['id', 'type', 'version', 'visible', 'props']
|
||||
|
||||
/**
|
||||
* Register a block definition. Throws on a duplicate type — a programmer error
|
||||
* caught at module load, not runtime.
|
||||
* @param {object} def
|
||||
* @returns {object} the stored definition
|
||||
*/
|
||||
export function registerBlock(def) {
|
||||
if (!def || typeof def.type !== 'string' || def.type.length === 0) {
|
||||
throw new Error('registerBlock: a block definition needs a string `type`')
|
||||
}
|
||||
if (registry.has(def.type)) {
|
||||
throw new Error(`registerBlock: block type already registered: ${def.type}`)
|
||||
}
|
||||
const entry = {
|
||||
type: def.type,
|
||||
version: Number.isInteger(def.version) ? def.version : 1,
|
||||
label: def.label || def.type,
|
||||
icon: def.icon || null,
|
||||
component: def.component || null,
|
||||
editor: def.editor || null,
|
||||
defaults: typeof def.defaults === 'function' ? def.defaults : () => ({}),
|
||||
container: Boolean(def.container),
|
||||
containerSlots: def.containerSlots ? [...def.containerSlots] : [],
|
||||
}
|
||||
registry.set(entry.type, entry)
|
||||
return entry
|
||||
}
|
||||
|
||||
/** @returns {object|null} the definition for `type`, or null if unknown. */
|
||||
export function getBlock(type) {
|
||||
return registry.get(type) || null
|
||||
}
|
||||
|
||||
/** @returns {boolean} whether `type` is a registered block. */
|
||||
export function hasBlock(type) {
|
||||
return registry.has(type)
|
||||
}
|
||||
|
||||
/** @returns {object[]} all registered definitions (registration order). */
|
||||
export function listBlocks() {
|
||||
return [...registry.values()]
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a stable block id. Called once when a block is added to the canvas;
|
||||
* never derived from array position, so a reorder keeps ids intact (they are the
|
||||
* React key and the future revision-history join point).
|
||||
* @returns {string}
|
||||
*/
|
||||
export function makeBlockId() {
|
||||
const rand = Math.random().toString(36).slice(2, 8).toUpperCase()
|
||||
return `b_${rand}`
|
||||
}
|
||||
Reference in New Issue
Block a user