CMS Chronicle·March 2026·4 min read

CMS Chronicle #07: Feature Roundup Seven features in one session

Horizontal rules, clone, video embeds, relation pickers, a proper media library with folders, scheduled publishing, and revision history — all shipped in a single session. Plus what each one actually is under the hood.

The list

We had a backlog of seven features that had been sitting in notes for a while. Instead of doing them one at a time across multiple sessions, we did all of them at once:

  1. Horizontal rule toolbar button
  2. Clone (documents and blocks)
  3. Video embed (field type + TipTap node)
  4. Relation picker
  5. Media library
  6. Scheduled publishing
  7. Revision history

Here is what each one actually does.


Horizontal rule

The simplest one. A button in the richtext toolbar calls editor.chain().focus().setHorizontalRule().run(). StarterKit already includes the extension. Serialises to --- in Markdown.

Clone

A Clone button appears in both the collection list (row level) and the document editor header. It hits POST /api/cms/{collection}/{slug} with { action: "clone" }. The API finds the first unused slug — {slug}-copy, then -copy-2, -copy-3 — creates a new draft with the same data, and redirects the editor to it.

Useful for: creating variants of a page, duplicating a template post, testing changes without touching the live document.

Video embed

Two surfaces:

As a field type — add { name: 'intro', type: 'video' } to any collection. The field editor shows a URL input + a live 16:9 iframe preview. YouTube, Vimeo, and youtu.be links are all parsed automatically.

Inside richtext — a 🎬 button in the toolbar opens a URL prompt and inserts a videoEmbed TipTap node. The node renders the iframe in the editor. Below it a footer bar shows the URL with Edit and ✕ buttons. Serialises to [video:URL] in Markdown, parsed back on load.

Less

  • Plain text field for everything
  • Paste a YouTube URL, hope it works
  • Type a slug manually for relations
  • Uploads dumped in one flat folder
  • No save history — ctrl+Z or nothing

More

  • video, relation, image-gallery, tags, select — typed fields
  • video field shows live embed preview
  • relation field searches and picks from collection
  • Media library with folders, grid/list, drag-drop
  • Revision history panel — restore any previous save

Relation picker

The relation field type previously fell through to a plain text input. Now it fetches GET /api/cms/{collection} and renders a searchable dropdown. Each option shows the document's title, name, or label field (whichever exists) with the slug shown smaller below.

Defining a relation: { name: 'author', type: 'relation', collection: 'team' }.

Media library

A dedicated /admin/media page linked from the sidebar.

Folders — one level deep. Files uploaded to a folder land in {UPLOAD_DIR}/{folder}/filename. The sidebar shows all folders with file counts. Set a folder in the "Upload to folder" input and all new uploads go there — including drag-and-drop.

Drag-and-drop — the entire page is a drop zone. Drag 200 images from Finder, they all upload in parallel. A drag overlay shows which folder they will land in.

Grid / List toggle — grid shows thumbnails, list shows a table with name, folder, size, date, and action buttons.

Pagination — 48 files per page in grid, 100 in list. Page resets when folder or search filter changes.

Tip

The entire Media page is a drop zone. Drag 200 images from Finder directly onto the page — they all upload in parallel. Set a folder name in the sidebar first and all dropped files land there automatically.

Scheduled publishing

Draft documents get a Schedule button in the editor header. It opens a datetime-local picker. The selected time is stored as data.publishAt (ISO string) and saved with the document.

A POST /api/publish-scheduled endpoint scans all collections for drafts with publishAt <= now and publishes them. Call it from an external cron, a Fly.io process, or manually.

Warning

The scheduler runs on demand via POST /api/publish-scheduled. There is no background cron yet — call the endpoint from an external cron job, a Fly.io machine process, or manually from the dashboard. Automation is coming in a later phase.

Revision history

Every time a document is saved via PATCH, the current state is snapshotted to {projectDir}/_revisions/{collection}/{slug}.json before the update is written. The last 20 revisions are kept.

A History button in the document editor header opens a slide-out panel listing all revisions with timestamps and status. Click Restore to reapply any snapshot — the current state is saved as a new revision first, so the restore is itself reversible.

Nina Munkholm
Christian Broberg
1 / 2

Nina Munkholm


What is next

The seven features above were the obvious gaps. What comes next is less obvious — probably ⌘K global search across all content, multi-language routing, and block components with actual design on webhouse-site. The infrastructure is solid enough to start building on top of it.