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.