# CMS Seed

Headless CMS domain demonstrating media library, SVG sanitization, block editor
content, custom block types, singleton resources, icon/font/color types,
scheduled publishing, and media reference tracking with orphan detection.

## Resources

### MediaItem

Represents a single uploaded file in the domain media library. Created via
`POST /media/upload`. Deletion is blocked (409 CONFLICT) if the item is
referenced by any other resource.

| Field        | Type     | Notes                          |
|--------------|----------|--------------------------------|
| id           | uuid     | Primary key                    |
| filename     | string   | Required, immutable, max 255   |
| mimeType     | string   | Required, immutable, max 100   |
| size         | integer  | Required, immutable (bytes)    |
| width        | integer  | Image width in pixels          |
| height       | integer  | Image height in pixels         |
| altText      | string   | Max 255                        |
| caption      | text     |                                |
| folder       | string   | Default "/", max 255           |
| uploadedById | string   | Immutable, max 100             |
| createdAt    | datetime | Auto-set on create             |
| updatedAt    | datetime | Auto-set on update             |

**Custom API path:** `/media`

**Features:** search

### Category

Article taxonomy with icon and color. Admin-managed vocabulary with caching.

| Field       | Type     | Notes                    |
|-------------|----------|--------------------------|
| id          | uuid     | Primary key              |
| name        | string   | Required, unique, max 100|
| slug        | slug     | Auto-generated from name, unique |
| description | text     | Max 500                  |
| icon        | icon     | e.g. "lucide:tag"        |
| color       | color    |                          |
| createdAt   | datetime | Auto-set on create       |
| updatedAt   | datetime | Auto-set on update       |

**Features:** caching (600s TTL)

### Article

Primary content resource. Rich block editor body supporting both built-in blocks
(paragraph, heading, image, code, quote, list, table, embed, divider) and custom
blocks (Callout, PromoCard).

| Field         | Type      | Notes                                    |
|---------------|-----------|------------------------------------------|
| id            | uuid      | Primary key                              |
| title         | string    | Required, max 200                        |
| slug          | slug      | Auto-generated from title, unique        |
| excerpt       | text      | Max 400                                  |
| categoryId    | uuid      | FK to Category                           |
| authorId      | string    | Required, immutable, max 100             |
| authorName    | string    | Max 200                                  |
| status        | enum      | ArticleStatus, default "draft"           |
| scheduledFor  | datetime  | Auto-publish date for "scheduled" status |
| featuredImage | media     | JPEG, PNG, WebP; max 10mb                |
| gallery       | array     | Up to 20 media images                    |
| attachments   | array     | Up to 10 files (PDF, DOCX, XLSX, ZIP); max 50mb each |
| body          | blocks    | Required, max 300 blocks                 |
| seo           | component | SeoMeta (metaTitle, metaDescription, ogTitle, ogImage, noIndex) |
| createdAt     | datetime  | Auto-set on create                       |
| updatedAt     | datetime  | Auto-set on update                       |
| deletedAt     | datetime  | Soft delete marker                       |

**Computed fields:**
- `readingTimeMinutes` (virtual) -- estimated reading time based on body word count (250 wpm).

**Features:** soft delete, audit, versioning, search, export

**Audit tracking:** title, status, body, featuredImage, scheduledFor

**Validation rules:**
- `scheduledFor` must be in the future (if set)
- `scheduledFor` requires status to be "scheduled"

**Field-level permissions:**
- `status` -- writable only by editor, admin
- `scheduledFor` -- writable only by editor, admin
- `seo` -- readable by authenticated; writable only by editor, admin

### SiteSettings (Singleton)

Global site configuration. Only one record exists. No list, create, or delete
endpoints. Read via `GET /site-settings/current`, update via `PATCH /site-settings/current`.

| Field           | Type      | Notes                              |
|-----------------|-----------|------------------------------------|
| id              | uuid      | Primary key                        |
| siteName        | string    | Required, max 100                  |
| tagline         | string    | Max 200                            |
| baseUrl         | url       |                                    |
| logo            | svg       | Max 100kb, sanitized on write      |
| favicon         | media     | PNG, ICO, SVG; max 1mb             |
| typography      | component | Typography (headingFont, bodyFont, monoFont) |
| brandColors     | component | BrandColors (primary, secondary, accent, background, text) |
| defaultSeo      | component | SeoMeta                            |
| analyticsId     | string    | Max 50                             |
| maintenanceMode | boolean   | Default false                      |
| createdAt       | datetime  | Auto-set on create                 |
| updatedAt       | datetime  | Auto-set on update                 |

**Singleton controls:** disable_list, disable_create, disable_delete

## Relationships

```
Article *──1 Category    (Article.category belongsTo Category)
```

MediaItem is referenced by media fields across resources (featuredImage, gallery,
attachments, ogImage, logo, favicon) but these are value references, not foreign
key relationships. The engine tracks references and prevents orphan deletion.

## Enums

### ArticleStatus

| Value     | Label       | Color   |
|-----------|-------------|---------|
| draft     | Draft       | #6b7280 |
| review    | In Review   | #f59e0b |
| scheduled | Scheduled   | #8b5cf6 |
| published | Published   | #22c55e |
| archived  | Archived    | #ef4444 |

## Custom Block Types

### Callout

A highlighted content block with a style variant and optional title.

| Prop  | Type | Values                       | Default |
|-------|------|------------------------------|---------|
| style | enum | info, warning, tip, danger   | info    |
| title | string | Max 100                    |         |

### PromoCard

A promotional card with heading, image, CTA button, and theme.

| Prop    | Type   | Notes             | Default      |
|---------|--------|-------------------|--------------|
| heading | string | Required          |              |
| subtext | text   |                   |              |
| image   | media  | Images only       |              |
| ctaText | string |                   | "Learn More" |
| ctaUrl  | url    | Required          |              |
| theme   | enum   | light, dark       | light        |

## Shared Objects

- **Timestamp** -- `createdAt` (auto on create) and `updatedAt` (auto on update)
- **SoftDelete** -- `deletedAt` datetime marker
- **SeoMeta** -- metaTitle, metaDescription, ogTitle, ogImage, noIndex
- **Typography** -- headingFont, bodyFont, monoFont
- **BrandColors** -- primary, secondary, accent, background, text

## Roles and Permissions

| Role   | Description                                              |
|--------|----------------------------------------------------------|
| admin  | Full system access, manages site settings, can delete articles |
| editor | Can publish and schedule articles, manages all content    |
| author | Can create and edit own articles, cannot publish or manage settings |
| reader | Read-only access, demonstrates public/authenticated boundaries |

**Permission matrix (summary):**

| Resource     | list           | read           | create        | update              | delete |
|--------------|----------------|----------------|---------------|---------------------|--------|
| MediaItem    | authenticated  | authenticated  | authenticated | owner,editor,admin  | admin  |
| Category     | *              | *              | admin         | admin               | admin  |
| Article      | *              | *              | authenticated | owner,editor,admin  | admin  |
| SiteSettings | (disabled)     | *              | (disabled)    | admin               | (disabled) |

## Seed Data

The seed file (`seed.yaml`) provisions:

- **4 roles:** admin, editor, author, reader
- **4 users:** admin (Admin123!), alice (editor), bob (author), carol (reader) -- all Demo123!
- **4 categories:** Engineering, Design, Product, Culture -- each with icon and color
- **1 SiteSettings record:** "Kalos Dev Blog" with typography, brand colors, default SEO, analytics ID
- **5 articles:** spanning published, review, draft, and scheduled statuses with block editor content, media references, and SEO metadata

## Assets

The `assets/` directory contains SVG placeholder files referenced by the seed data:
- `logo.svg` -- site logo used by SiteSettings
- `placeholder-hero.svg` -- placeholder hero image used by articles

## Usage

Point your kalosd config at this seed:

```json
{
  "adl": {
    "domain_file": "seeds/cms/domain.yaml"
  },
  "seeder": {
    "seed_file": "seeds/cms/seed.yaml"
  }
}
```
