CMS Chronicle·March 2026·

CMS Chronicle #12: The AI-Native Editor

748 commits in 11 days. We shipped a conversational CMS interface, upgraded the richtext editor to TipTap v3, built a media processing pipeline with Sharp and EXIF, added AI image analysis, and discovered a save bug that had been hiding since day one. [ICD test]

748 Commits in 11 Days

This chronicle covers March 16–27, 2026 — arguably the most productive stretch in the project's history. We shipped 8 features to Done, created 4 new feature plans, fixed a critical save bug, and built something no other CMS has: a full-screen conversational interface that can read, search, create, and edit your entire site through natural language.

Here's everything.

F107: Chat with Your Site

The headline feature. A master toggle in the admin header switches between "Traditional" (the sidebar/tabs/editor UI you know) and "Chat" — a full-screen conversational interface.

Phase 1 shipped with read-only tools: site summary, list documents, search content, get schema, list drafts, get site config, list media. You can ask "show me all draft posts" or "what fields does the pages collection have?" and the AI answers using your actual site data.

Phase 2 followed immediately: create, update, publish, unpublish, trash documents. Confirmation flow for destructive actions. Preview cards showing what changed. The AI calls internal CMS API routes directly — not MCP — for lower latency and automatic feature parity as the API evolves.

The chat renders rich markdown, page preview cards with iframe snapshots, and tool execution summaries. Conversations are persisted per user. Smart welcome screen with context-aware suggestions based on site state (draft count, scheduled posts, deploy status).

No headless CMS offers this. Contentful, Sanity, Strapi — they all have traditional admin panels. A CMS you can talk to is genuinely new.

F106: TipTap v3 Upgrade

We upgraded the richtext editor from TipTap v2.27 to v3.21. This was supposed to be straightforward. It was not.

The package migration itself was clean: consolidated table imports (4 packages → 1), moved BubbleMenu to Floating UI, updated the StarterKit config. TypeScript compiled first try.

Then we discovered that toolbar buttons stopped reflecting the current formatting state. In v3, shouldRerenderOnTransaction is disabled by default — the component doesn't re-render on every cursor move. We moved all mark states (bold, italic, strike, underline, lists, headings) into the useEditorState selector, which subscribes to transactions independently.

Then we discovered that saving content sometimes lost the latest edits. Playwright E2E tests confirmed: content survived save, but disappeared after tab navigation.

The root cause was a stale closure bug that had existed since before the v3 upgrade — v2's aggressive re-rendering just masked it. The Cmd+S keyboard handler was inside a useEffect([saving]) that captured a stale save() function. When the user typed text and pressed Cmd+S, the handler called save() with an old doc.data that didn't include the latest edits. The file was "saved" with stale content.

Fix: a ref (saveRef.current = save) that always points to the latest function. Three Playwright tests now guard this: content survives save, content survives tab navigation, content survives full page reload.

The flushSync warning from React 19 was resolved by removing immediatelyRender: false (unnecessary for client components) and keeping shouldRerenderOnTransaction: false.

F108: Rich Text Editor Enhancements

With TipTap v3 stable, we added the formatting features the editor was missing:

  • Underline (Cmd+U) — was in StarterKit but disabled
  • Superscript (Cmd+.) and Subscript (Cmd+,)
  • Text alignment — left, center, right for paragraphs and headings
  • Highlight — 6 color picker (green, blue, yellow, red, purple, orange) with clear button
  • Editor zoom — [−] 100% [+] range 50-200%, CSS font-size scaling
  • AI Proofreading — auto-detects language, checks spelling/grammar/style
  • Table controls — add/delete rows and columns, context buttons that appear when cursor is in a table

All formatting uses HTML-in-markdown (<u>, <sup>, <sub>, <mark>) since standard markdown doesn't support these. The tiptap-markdown config was changed to html: true to preserve these tags through the roundtrip.

F44: Media Processing Pipeline

The media system got a complete upgrade:

  • Sharp WebP generation — on upload, automatically creates 400w, 800w, 1200w, 1600w variants
  • EXIF extraction — camera model, GPS coordinates, focal length, ISO, all persisted
  • Build-time <picture> upgrade<img> tags in static HTML get WebP srcset automatically
  • Batch optimization — "Optimize All" button for existing media libraries
  • Configurable — variant widths and WebP quality adjustable in Site Settings

Originals are always preserved. Variants are stored alongside (hero.jpghero-400w.webp, hero-800w.webp). The build pipeline auto-discovers variants on disk.

F103: AI Image Analysis

Every uploaded image is now automatically analyzed by Claude:

  • Caption — descriptive sentence about the image content
  • Alt-text — accessible description (max 125 chars)
  • Tags — 3-8 relevant keywords
  • Language-aware — prompts adapt to the site's language

Batch analyze existing libraries. Results visible in the lightbox AI panel. Alt-text auto-fills when inserting images in the editor. The analysis runs non-blocking after upload — the file is available immediately while AI works in the background.

F102: Schema Drift Detection

A data integrity guardrail born from a real incident. On March 19, a Claude session accidentally stripped all Posts fields from cms.config.ts down to just title. The damage went unnoticed for 6 days.

Now, when you open a collection in the admin, the system samples documents and compares their data keys against the schema fields. If content has fields that don't exist in the schema — a yellow warning banner appears:

⚠ Schema drift detected in "Posts": 8 fields found in content but missing from schema (excerpt, content, date, author, category, readTime, tags, attribution)

Purely informational, dismissible per session. 8 unit tests guard the detection logic.

Media Library Improvements

  • Sort by newest, oldest, name, size (default: newest first)
  • Search matches AI metadata — caption, alt-text, tags, not just filename
  • User tags — add custom tags to any file, filter by tag in sidebar
  • WebP variants hidden — the media list only shows originals
  • Cmd+K finds media by EXIF data and AI descriptions
  • Image picker in the editor also searches AI metadata

The Save Bug

This deserves its own section because it's the kind of bug that erodes trust.

The symptom: you type text, save, navigate to another tab, come back — your text is gone. The file on disk has the old content. The editor shows the old content. Your work is lost.

We spent hours chasing this through layers of React state management, useEffect guards, module-level caches, globalThis persistence, and webpack file watcher configuration. None of it was the real problem.

The real problem was a JavaScript closure. The Cmd+S handler captured a save() function from an old render. That function read doc.data from its closure scope — which didn't include the latest keystrokes. The fix was one line: saveRef.current = save.

This bug existed before the TipTap v3 upgrade. TipTap v2's shouldRerenderOnTransaction: true (default) caused so many re-renders that the effect re-attached with a fresh closure on almost every keystroke, masking the issue. v3's performance optimization (disabled by default) exposed it.

The lesson: performance optimizations that reduce re-renders can expose stale closure bugs that were always there but never triggered. Write E2E tests for critical paths.

What's Next

  • F109: Inline Proofreading — ProseMirror decorations for inline corrections with accept/reject
  • F97: SEO Module — per-document SEO panel, SERP preview, scores
  • F96: Embeddable Maps — every business site needs a map
  • F107 Phase 3 — inline forms in chat (hybrid mode)

We're building the CMS we needed thirty years ago. Every week it gets closer.