# 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 `