← All seeds
CMS ADL v0.3.1

Headless CMS

A content platform with a media library, block-editor articles, singleton site settings, scheduled publishing, SVG sanitization, and orphan-media detection.

Resources
MediaItem, Category, Article, SiteSettings
Roles
admin, editor, author, viewer
Features (5)
soft_deleteauditpermissionsmedia_librarysingleton

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.

FieldTypeNotes
iduuidPrimary key
filenamestringRequired, immutable, max 255
mimeTypestringRequired, immutable, max 100
sizeintegerRequired, immutable (bytes)
widthintegerImage width in pixels
heightintegerImage height in pixels
altTextstringMax 255
captiontext
folderstringDefault ”/”, max 255
uploadedByIdstringImmutable, max 100
createdAtdatetimeAuto-set on create
updatedAtdatetimeAuto-set on update

Custom API path: /media

Features: search

Category

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

FieldTypeNotes
iduuidPrimary key
namestringRequired, unique, max 100
slugslugAuto-generated from name, unique
descriptiontextMax 500
iconicone.g. “lucide:tag”
colorcolor
createdAtdatetimeAuto-set on create
updatedAtdatetimeAuto-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).

FieldTypeNotes
iduuidPrimary key
titlestringRequired, max 200
slugslugAuto-generated from title, unique
excerpttextMax 400
categoryIduuidFK to Category
authorIdstringRequired, immutable, max 100
authorNamestringMax 200
statusenumArticleStatus, default “draft”
scheduledFordatetimeAuto-publish date for “scheduled” status
featuredImagemediaJPEG, PNG, WebP; max 10mb
galleryarrayUp to 20 media images
attachmentsarrayUp to 10 files (PDF, DOCX, XLSX, ZIP); max 50mb each
bodyblocksRequired, max 300 blocks
seocomponentSeoMeta (metaTitle, metaDescription, ogTitle, ogImage, noIndex)
createdAtdatetimeAuto-set on create
updatedAtdatetimeAuto-set on update
deletedAtdatetimeSoft 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.

FieldTypeNotes
iduuidPrimary key
siteNamestringRequired, max 100
taglinestringMax 200
baseUrlurl
logosvgMax 100kb, sanitized on write
faviconmediaPNG, ICO, SVG; max 1mb
typographycomponentTypography (headingFont, bodyFont, monoFont)
brandColorscomponentBrandColors (primary, secondary, accent, background, text)
defaultSeocomponentSeoMeta
analyticsIdstringMax 50
maintenanceModebooleanDefault false
createdAtdatetimeAuto-set on create
updatedAtdatetimeAuto-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

ValueLabelColor
draftDraft#6b7280
reviewIn Review#f59e0b
scheduledScheduled#8b5cf6
publishedPublished#22c55e
archivedArchived#ef4444

Custom Block Types

Callout

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

PropTypeValuesDefault
styleenuminfo, warning, tip, dangerinfo
titlestringMax 100

PromoCard

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

PropTypeNotesDefault
headingstringRequired
subtexttext
imagemediaImages only
ctaTextstring”Learn More”
ctaUrlurlRequired
themeenumlight, darklight

Shared Objects

  • TimestampcreatedAt (auto on create) and updatedAt (auto on update)
  • SoftDeletedeletedAt datetime marker
  • SeoMeta — metaTitle, metaDescription, ogTitle, ogImage, noIndex
  • Typography — headingFont, bodyFont, monoFont
  • BrandColors — primary, secondary, accent, background, text

Roles and Permissions

RoleDescription
adminFull system access, manages site settings, can delete articles
editorCan publish and schedule articles, manages all content
authorCan create and edit own articles, cannot publish or manage settings
readerRead-only access, demonstrates public/authenticated boundaries

Permission matrix (summary):

Resourcelistreadcreateupdatedelete
MediaItemauthenticatedauthenticatedauthenticatedowner,editor,adminadmin
Category**adminadminadmin
Article**authenticatedowner,editor,adminadmin
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:

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

Domain definition

View raw (492 lines)
Show domain.yaml
# seeds/cms/domain.yaml
#
# Headless CMS with Media Library — ADL Domain Definition
# ADL version: 0.3.1
#
# Resources:  MediaItem, Category, Article, SiteSettings
# Relations:  Article belongsTo Category
#
# ADL features exercised:
#   Phase 1      — CRUD generation, REST route mapping, RBAC permissions
#   Phase 6      — belongsTo, ?include=
#   Phase 7      — filter, sort, paginate, search, count
#   Phase 8      — Rich types: media, svg, blocks, icon, font, color, url, slug
#   Phase 9      — $merge (Timestamp, SoftDelete), $ref objects/enums/blocks
#   Phase 10     — UUID primary keys, custom indexes
#   Phase 12     — Soft delete on Article, audit trail on Article
#   Phase 14     — Field-level permissions (status, scheduledFor, seo)
#
# New features (first exercised in POC-04):
#   media type     — Featured images, gallery arrays, file attachments
#   svg type       — Brand logo with sanitization
#   blocks type    — Article body (paragraph, heading, image, code, etc.)
#   custom blocks  — Callout and PromoCard domain-level block types
#   icon type      — Category icon reference
#   font type      — SiteSettings typography
#   singleton      — SiteSettings (disable_create, disable_delete, disable_list)
#   media library  — Upload action, references tracking, orphan detection
#   SVG sanitize   — Script injection stripped on write
#   scheduled pub  — scheduledFor field + cross-field validation

domain:
  name: cms
  version: "1.0.0"
  description: >
    Headless CMS 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.

  api: {}

  # ─── Shared objects ────────────────────────────────────────────
  objects:
    Timestamp:
      createdAt: { type: datetime, autoNow: create }
      updatedAt: { type: datetime, autoNow: update }

    SoftDelete:
      deletedAt: { type: datetime }

    SeoMeta:
      metaTitle:       { type: string, maxLength: 60 }
      metaDescription: { type: text,   maxLength: 160 }
      ogTitle:         { type: string, maxLength: 60 }
      ogImage:         { type: media,  accept: ["image/*"] }
      noIndex:         { type: boolean, default: false }

    Typography:
      headingFont: { type: font }
      bodyFont:    { type: font }
      monoFont:    { type: font }

    BrandColors:
      primary:    { type: color }
      secondary:  { type: color }
      accent:     { type: color }
      background: { type: color }
      text:       { type: color }

  # ─── Shared enums ──────────────────────────────────────────────
  enums:
    ArticleStatus:
      values:
        draft:
          label: "Draft"
          description: "Work in progress, not visible to public"
          color: "#6b7280"
        review:
          label: "In Review"
          description: "Submitted for editorial review"
          color: "#f59e0b"
        scheduled:
          label: "Scheduled"
          description: "Approved, will auto-publish at scheduledFor date"
          color: "#8b5cf6"
        published:
          label: "Published"
          description: "Live and publicly visible"
          color: "#22c55e"
        archived:
          label: "Archived"
          description: "Removed from public listing"
          color: "#ef4444"

  # ─── Custom blocks ────────────────────────────────────────────
  blocks:
    Callout:
      label: "Callout"
      hasContent: true
      props:
        style:
          type: enum
          values: [info, warning, tip, danger]
          default: info
        title:
          type: string
          maxLength: 100
          required: false

    PromoCard:
      label: "Promo Card"
      hasContent: false
      props:
        heading:  { type: string,  required: true }
        subtext:  { type: text,    required: false }
        image:    { type: media,   accept: ["image/*"] }
        ctaText:  { type: string,  default: "Learn More" }
        ctaUrl:   { type: url,     required: true }
        theme:    { type: enum,    values: [light, dark], default: light }

  # ─── Shared properties ────────────────────────────────────────
  properties:
    slug:
      type: slug
      unique: true
      maxLength: 255

  # ─── Resources ─────────────────────────────────────────────────
  resources:

    # ╔══════════════════════════════════════════════════════════════╗
    # ║  MediaItem                                                   ║
    # ╚══════════════════════════════════════════════════════════════╝
    MediaItem:
      description: >
        Represents a single uploaded file in the domain media library.
        Created automatically on upload via POST /media/upload.
        Deletion is blocked if the item is referenced by any other resource
        (returns 409 CONFLICT).

      api:
        path: /media

      object:
        id:
          type: uuid
          primaryKey: true

        filename:
          type: string
          required: true
          immutable: true
          maxLength: 255

        mimeType:
          type: string
          required: true
          immutable: true
          maxLength: 100

        size:
          type: integer
          required: true
          immutable: true
          description: "File size in bytes"

        width:
          type: integer
          description: "Image width in pixels (images only)"

        height:
          type: integer
          description: "Image height in pixels (images only)"

        altText:
          type: string
          maxLength: 255

        caption:
          type: text

        folder:
          type: string
          maxLength: 255
          default: "/"

        uploadedById:
          type: string
          immutable: true
          maxLength: 100

        $merge:
          - { $ref: '#/objects/Timestamp' }

      indexes:
        - fields: [mimeType]
          description: "Filter media by type"
        - fields: [folder]
          description: "Browse media by folder"
        - fields: [uploadedById]
          description: "List media by uploader"

      features:
        search: true

      permissions:
        list:   [authenticated]
        read:   [authenticated]
        create: [authenticated]
        update: [owner, editor, admin]
        delete: [admin]

    # ╔══════════════════════════════════════════════════════════════╗
    # ║  Category                                                    ║
    # ╚══════════════════════════════════════════════════════════════╝
    Category:
      description: >
        Article taxonomy. Demonstrates icon type and caching.
        Admin-managed vocabulary.

      object:
        id:
          type: uuid
          primaryKey: true

        name:
          type: string
          required: true
          unique: true
          maxLength: 100

        slug:
          $ref: '#/properties/slug'
          from: name

        description:
          type: text
          maxLength: 500

        icon:
          type: icon
          description: "Icon reference string, e.g. 'lucide:tag'"

        color:
          type: color

        $merge:
          - { $ref: '#/objects/Timestamp' }

      permissions:
        list:   ["*"]
        read:   ["*"]
        create: [admin]
        update: [admin]
        delete: [admin]

      features:
        caching: true
        cacheTtlSeconds: 600

    # ╔══════════════════════════════════════════════════════════════╗
    # ║  Article                                                     ║
    # ╚══════════════════════════════════════════════════════════════╝
    Article:
      description: >
        Primary content resource. Demonstrates blocks body (with custom
        Callout and PromoCard blocks), media fields (featuredImage, gallery,
        attachments), virtual computed readingTimeMinutes, versioning,
        scheduled publishing, SEO component, soft delete, and audit.

      object:
        id:
          type: uuid
          primaryKey: true

        title:
          type: string
          required: true
          maxLength: 200

        slug:
          $ref: '#/properties/slug'
          from: title

        excerpt:
          type: text
          maxLength: 400

        categoryId:
          type: uuid

        authorId:
          type: string
          required: true
          immutable: true
          maxLength: 100

        authorName:
          type: string
          maxLength: 200

        status:
          $ref: '#/enums/ArticleStatus'
          default: draft

        scheduledFor:
          type: datetime
          description: "Future date for auto-publish when status is 'scheduled'"

        featuredImage:
          type: media
          accept: ["image/jpeg", "image/png", "image/webp"]
          maxSize: "10mb"

        gallery:
          type: array
          items:
            type: media
            accept: ["image/*"]
            maxSize: "10mb"
          maxItems: 20

        attachments:
          type: array
          items:
            type: media
            accept: ["application/pdf", ".docx", ".xlsx", ".zip"]
            maxSize: "50mb"
          maxItems: 10

        body:
          type: blocks
          required: true
          maxBlocks: 300
          allowedBlocks:
            - paragraph
            - heading
            - image
            - code
            - quote
            - list
            - table
            - embed
            - divider
            - Callout
            - PromoCard

        seo:
          type: component
          object: { $ref: '#/objects/SeoMeta' }

        $merge:
          - { $ref: '#/objects/Timestamp' }
          - { $ref: '#/objects/SoftDelete' }

      computed:
        readingTimeMinutes:
          type: integer
          formula: "ceil(wordCount(body) / 250)"
          stored: false
          description: "Virtual — estimated reading time based on body word count"

      relationships:
        category:
          type: belongsTo
          target: Category
          foreignKey: categoryId

      indexes:
        - fields: [status]
          description: "Filter articles by status"
        - fields: [authorId]
          description: "List articles by author"
        - fields: [scheduledFor]
          description: "Query scheduled articles"
        - fields: [categoryId]
          description: "List articles by category"
        - fields: [status, scheduledFor]
          description: "Composite: scheduled articles pending publish"

      features:
        softDelete: true
        audit: true
        versioning: true
        search: true
        export: true

      audit:
        track: [title, status, body, featuredImage, scheduledFor]
        exclude: [updatedAt, deletedAt]

      validation:
        scheduledForFuture:
          rule: "scheduledFor == null || scheduledFor > now()"
          message: "Scheduled publish date must be in the future"

        scheduledRequiresPublishStatus:
          rule: "scheduledFor == null || status == 'scheduled'"
          message: "Set status to 'scheduled' when providing a scheduled date"

      permissions:
        list:   ["*"]
        read:   ["*"]
        create: [authenticated]
        update: [owner, editor, admin]
        delete: [admin]

        fields:
          status:
            read:  ["*"]
            write: [editor, admin]

          scheduledFor:
            read:  ["*"]
            write: [editor, admin]

          seo:
            read:  [authenticated]
            write: [editor, admin]

    # ╔══════════════════════════════════════════════════════════════╗
    # ║  SiteSettings (Singleton)                                    ║
    # ╚══════════════════════════════════════════════════════════════╝
    SiteSettings:
      description: >
        Global site configuration. Singleton resource — one record, no list
        or create. Read by anyone via GET /site-settings/current, written
        by admin only via PATCH /site-settings/current.

      object:
        id:
          type: uuid
          primaryKey: true

        siteName:
          type: string
          required: true
          maxLength: 100

        tagline:
          type: string
          maxLength: 200

        baseUrl:
          type: url

        logo:
          type: svg
          maxSize: "100kb"
          sanitize: true

        favicon:
          type: media
          accept: ["image/png", "image/x-icon", "image/svg+xml"]
          maxSize: "1mb"

        typography:
          type: component
          object: { $ref: '#/objects/Typography' }

        brandColors:
          type: component
          object: { $ref: '#/objects/BrandColors' }

        defaultSeo:
          type: component
          object: { $ref: '#/objects/SeoMeta' }

        analyticsId:
          type: string
          maxLength: 50

        maintenanceMode:
          type: boolean
          default: false

        $merge:
          - { $ref: '#/objects/Timestamp' }

      api:
        path: /site-settings
        disable_list: true
        disable_create: true
        disable_delete: true
        custom_operations:
          - name: current
            method: GET
            path: /current
            permission: ["*"]

      permissions:
        read:   ["*"]
        update: [admin]

Download bundle