CMS Chronicle·January 2026·5 min read

CMS Chronicle #01: Building the Blog the Right Way

We built the Articles section of webhouse.dk this week. Twice. The first version hardcoded everything in TSX. The second version does it properly — content in CMS, rendering in code.

There is a pattern we keep catching ourselves in. We build something, it works, it looks right — and then we notice that all the content is sitting directly in the component file. This week we caught it early. Twice.

What we built

The Articles section on webhouse.dk needed a blog. Four series, each with its own voice and focus:

  • Field Notes — operational insights from running production systems
  • AI Dispatch — what we're learning building real AI tools
  • CMS Chronicle — this series, following the build in public
  • 30 Years — pattern recognition across three decades of shipping software

A category index for each. Individual article pages. A shared renderer. The first AI Dispatch post — a piece about what happens to designers when AI starts doing the making. All of it live. But getting there took a detour.

The detour

The first version of the article page had the content hardcoded directly in the TSX component. Every paragraph, every pull quote, every bullet point — JSX. It looked correct in the browser. The problem is that a site that hardcodes its content into its components isn't really using a CMS. It's using a CMS to manage some things and pretending the rest doesn't matter.

We have a rule about this. It's written down. We ignored it anyway.

The rule is simple: content lives in CMS. .tsx files contain rendering logic, not words.

So we rewrote it. The article body is now Markdown stored in a content field in the posts collection. Structured data — title, excerpt, date, category, the comparison block — lives as separate fields in the JSON document. The TSX file receives data and renders it. That's all it does.

How it actually works

We use react-markdown with a custom components map. Each Markdown element maps to a styled React component:

  • Paragraphs and headings render at reading size with proper spacing
  • Pull quotes — blockquotes that start with \u201c — get the large bold treatment with a teal left border
  • Statement blockquotes (everything else) render as compact green-dot items, like these
  • The [comparison] marker in the Markdown string injects a structured Less/More panel from a separate JSON field

The comparison block can't be expressed in Markdown, so it lives alongside the content as structured data. It's a small convention that solves a real problem without overcomplicating the model.

What we learned

A rule that isn't in memory isn't a rule — it's a suggestion. The "content lives in CMS" principle existed in the project plan but not in the persistent memory file Claude reads at session start. So it got ignored. It's in memory now.

The other thing: Playwright is useful for catching rendering issues you'd otherwise miss. We screenshot both versions side by side and could see immediately that the blockquote font sizes, the paragraph spacing, and the title split were all wrong. Fixing CSS by looking at screenshots is much faster than fixing it by reading code.

What's next

The CMS Chronicle series is the right place to document this build in public. Every session that moves something meaningful forward gets a post. The webhouse.dk site and the @webhouse/cms platform are being built in parallel — one tests the other, and both improve because of it.

Next up: Phase 3. Supabase adapter, PostgreSQL backend, Docker packaging, and a Fly.io deploy pipeline. When that lands, @webhouse/cms stops being a local filesystem tool and becomes something you can actually run in production.