diff --git a/docs/domain-model.md b/docs/domain-model.md new file mode 100644 index 0000000..d3415c1 --- /dev/null +++ b/docs/domain-model.md @@ -0,0 +1,80 @@ +# Domain model + +## Site (§12.1) + +``` +Site: id UUID, name TEXT, description TEXT NULL, canonical_url TEXT, + default_theme_id TEXT, created_at/updated_at TIMESTAMPTZ +``` + +Even though v1 proposes one site per installation, `site_id` scopes persisted data, leaving a migration path to multi-site without designing tenancy now. + +## Content entry (§12.2) + +``` +ContentEntry: id UUID, site_id UUID, content_type TEXT, slug TEXT, title TEXT, + status TEXT, excerpt TEXT NULL, published_at TIMESTAMPTZ NULL, + current_revision_id UUID NULL, created_at/updated_at, deleted_at NULL +``` + +Initial statuses: `draft`, `published`. Initial registered content type: `core.post`. Future content types are registry contributions, not `if type === ...` branches. + +## Content revision (§12.3) + +``` +ContentRevision: id UUID, content_id UUID, revision_number BIGINT, + document_version INTEGER, document JSONB, created_by UUID, created_at +``` + +Unique `(content_id, revision_number)`. + +## Identifier policy (§12.4) + +PostgreSQL 18 provides UUIDv7 generation — use UUIDv7 for major aggregates (globally unique opaque IDs with better temporal/index locality than UUIDv4). + +## Canonical content document (§13) + +A post body is a versioned semantic block document: + +```json +{ "version": 1, "blocks": [ { "id": "019d...", "type": "core.paragraph", "version": 1, "props": { "text": "Hello world." } } ] } +``` + +Every block persists block instance ID, globally unique block type ID, block schema version, and validated properties. Theme CSS classes / concrete theme markup must not be stored as semantic content. + +## `core.markdown` block (§13.1, v1.1) + +Markdown authoring was a locked product decision omitted from v1.0; it is restored as a block inside the block model. `props.source` is CommonMark + GFM (tables, strikethrough, task lists, autolinks) + footnotes; raw HTML disabled. Rules: + +1. Exactly **one** server-side Markdown renderer ending in one sanitiser with an explicit allowlist; the admin preview calls it (no second client-side renderer). +2. A v0.1 post is a single `core.markdown` block by default — editor is a Markdown text editor with server-rendered preview. +3. When the block editor arrives, new content splits into finer blocks; existing `core.markdown` blocks are never bulk-converted. +4. Export emits `props.source` as `.md` files with frontmatter — the portability guarantee. + +## Initial blocks (§14) + +`core.heading`, `core.paragraph`, `core.quote`, `core.code`, `core.divider`, `core.image` (once media ships). **v1.1:** `core.markdown` joins the set as the primary authoring block. + +Missing block behaviour (§14.1): public page still renders with a safe placeholder (no raw props, error logs include IDs); admin shows the missing dependency and preserves the original payload (no silent deletion). + +## Page composition (§15) + +Home is an ordered list of versioned section instances: + +```json +{ "version": 1, "sections": [ { "id": "home-intro", "type": "core.site-intro", "version": 1, "enabled": true, "settings": {} }, { "id": "home-posts", "type": "core.post-list", "version": 1, "enabled": true, "settings": { "limit": 20 } } ] } +``` + +Initial composition UI supports only enable / disable / reorder / configure. Explicitly excluded: arbitrary pixel positioning, nested no-code canvas, user-authored raw HTML layouts, raw CSS editor. + +## Navigation (§28) + +Persisted model: id, site_id, label, destination_type, destination, position, enabled, open_in_new_context, created_at, updated_at. Initial destination types: `internal_route`, `external_url`. Semantic `