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:
- Horizontal rule toolbar button
- Clone (documents and blocks)
- Video embed (field type + TipTap node)
- Relation picker
- Media library
- Scheduled publishing
- 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
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.

