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>
85 lines
3.3 KiB
JavaScript
85 lines
3.3 KiB
JavaScript
// 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}`
|
|
}
|