---
title: "The blog merge that turned into a platform decision"
canonical: https://dxdev.com/blog/2026-04-19_blog-merge-platform-decision/
datePublished: 2026-04-19
---
On April 19, two Cloudflare Pages deployments fired every time I published anything to dxdev.com. One for `dxdev-landing`, the marketing site. One for `dxdev-blog`, a completely separate Astro project sitting at the same domain. I had been running this arrangement for months and calling it architecture. It was an accident I had stopped noticing.

It meant double config, double deploy, and a standing question every time I wanted to add a page. Which repo owned this? Marketing? Blog? Something in between? The answer kept shifting. Neither repo accumulated the shared context it needed to answer cleanly. The blog's Astro content schema had grown to track series, episode, and reader tier. `dxdev-landing` knew none of it. The landing site's Tailscale dev-host config, which let me preview the marketing site on my local network, had never been threaded through to the blog. Two surfaces serving a single product had stopped talking to each other.

Folding `dxdev-blog` into `dxdev-landing` was the obvious fix. I expected to spend an afternoon moving files. I spent the day doing product architecture.

## the merge that exposed the schema gap

The blog's content model had grown in its own repo, loosely, responding to needs as they surfaced. Carrying it into `dxdev-landing` meant making it explicit.

The frontmatter for a post in the merged repo now carries eight fields that weren't previously shared: `series`, `episode`, `tier`, `tags`, `component`, `concern`, `phase`, and `status`. Each exists because a specific rendering question needed an answer prose alone couldn't give. `tier` distinguishes free posts from paywalled content. `component` tracks which part of the product stack a post addresses. `concern` is narrower than `tags`, pointing at the operational problem rather than the general topic. `phase` marks where in a project arc the work happened.

None of these are decorative. A renderer that doesn't understand `tier` can't decide whether to show a paywall. A renderer that doesn't understand `series` can't build episode navigation. This is where content schema crosses from editorial metadata into rendering contract. The merge was what made the crossing visible.

The question the merge forced me to sit with: how many fields can accumulate in frontmatter before the post renderer becomes a CMS by stealth? I don't have the threshold yet. What I have is the recognition that since `dxdev-landing` now owns both surfaces, this schema is shared infrastructure. Adding a field is no longer an editorial call. It ships with the next deploy.

## what the renderer had forgotten

Moving content is easier than moving capabilities.

`dxdev-blog` had a renderer feature I'd built for a post called "The 32-Day Revolution," written from multiple narrator viewpoints on the same arc of events. I'd added perspective-tab rendering for it. A mechanism for toggling between two framings of the same moment in the text. When I folded the blog into `dxdev-landing`, that capability dropped silently. The Astro component that handled it didn't transfer cleanly, and the merged site's renderer gave no indication anything was missing.

A content migration validates that text moved. It does not validate that rendering behavior moved with it. The perspective tabs on "The 32-Day Revolution" would have silently collapsed into a broken layout for any reader who hit that post on the new site. Nothing in the migration output flagged it. Catching it required knowing to look.

## scrubbing the surface

While I had both surfaces open simultaneously, my last name was still on the public-facing landing pages, and a handful of links pointed at GitHub repos that had been renamed or frozen since I'd written the posts that referenced them. Neither was urgent. But with both surfaces mapped to the same mental model at the same time, cleaning them then was cheaper than re-opening the context later.

Removing my last name from the public site is a product identity choice. dxdev.com should carry the product, not a signal that it's a personal project. Dead GitHub links are just wrong. Cleaning both at the same merge point cost a few minutes and would have cost an afternoon to re-derive context for on a separate day.

## when a shared deploy couples previously separate concerns

The practical change from this day is that `dxdev-blog` is frozen. Every future post, every renderer update, every schema change goes into `dxdev-landing`. Three posts moved from review to published on this day. The Tailscale dev-host config got threaded through properly so I could preview the combined site on my local network. The GitHub Actions workflow dropped to a single run pushing to a single Cloudflare Pages project.

That sounds like a simplification. It is. It also comes with coupling worth being direct about. When the blog and the marketing site share a deploy, a content edit and a homepage redesign go out in the same release window. A broken schema field in a draft post can hold back a marketing change. The two concerns are coupled now, and coupling is real.

A few months ago I wrote about splitting a monorepo into several smaller repos because the coordination tax of keeping everything together was climbing faster than the simplicity gains. This is the inverse move. Collapsing two repos into one because they'd already converged on the same deploy target, the same Astro config, the same audience. The difference is that the earlier split was separating genuinely independent concerns. This merge was recognizing that `dxdev-landing` and `dxdev-blog` were never really independent, and the coupling they'd been hiding behind two repo names was always there.

## the schema was the real work

Getting the text out of `dxdev-blog` and into `dxdev-landing` was the smallest part of the day. The perspective-tab renderer for "The 32-Day Revolution" needed reconstruction, since the Astro component hadn't transferred cleanly. The frontmatter schema had to be negotiated deliberately. Which of the eight fields belonged in the shared contract, and which were `dxdev-blog` artifacts that could be dropped.

When a blog folds into a product site and both have to ship together, the frontmatter fields stop being editorial notes. The `tier` field in `dxdev-landing` determines whether a paywall component renders, and getting it wrong ships a content access bug to every visitor. Deciding what `tier` means when both surfaces share a single Cloudflare Pages deploy, and whether `concern` lives in the shared rendering contract or stays a local annotation, was the architecture. The file migration was mechanical. That part wasn't.

A content migration becomes architecture the moment two surfaces share a deploy. The files were always the easy part. The contract between them was the work.
