Rebuilding Paleo Coder: From a WordPress Backup to a Static Cave Wall

How this site went from a dead WordPress install to a static Astro build — mockup-first design, a numbered story backlog, and a legacy-content migration that kept the old voice intact.

activeClaudeAstroTypeScriptTailwind CSS v4MDXGitHub ActionsBluehost
↗ Visit Site

The old Paleo Coder had been offline in spirit for a decade — a WordPress install with nineteen posts from 2011 to 2013, then silence. When I decided to bring it back, I didn’t want another WordPress site. I wanted something I could run for free on the hosting I already had, that couldn’t be taken down by a plugin update, and that I could build the same way I’d build anything else I actually cared about getting right: with a plan, in the open, one piece at a time.

That turned into an Astro rebuild, a mockup I generated before writing any code, and a backlog of more than fifty numbered stories tracking every feature from the hero section to the RSS feed. Here’s how the pieces actually went together.

Starting from a picture, not a page

Most of my client work starts from a wireframe or a component library. This one started from a single generated mockup image — one raster file showing the whole page: dark textured header, a cave-wall hero with a manifesto headline, featured article cards, four category bands, an about section, a palette strip, a footer.

A generated mockup isn’t a layered design file, so there was no way to pull out clean vector assets after the fact. Instead of redrawing everything by hand and losing fidelity, I treated the mockup as the source of truth and cropped directly out of it — exact pixel crops for every section, every card, every icon, saved into docs/design/paleo-coder-ui-kit-exact/ with a JSON manifest recording the crop coordinates. Not elegant, but honest: what shipped looks like what was approved, because it’s the same pixels.

Out of that mockup came the actual brand system — not vibes, but values I could put in code:

--pc-color-bone-white: #F5F2EA;
--pc-color-sandstone: #D9C6AA;
--pc-color-ochre-gold: #D4A017;
--pc-color-clay-amber: #B35A1E;
--pc-color-charcoal-black: #1E1E1E;

Ten colors, four font stacks (a serif for headlines, a serif for body copy, IBM Plex Mono for labels, a handwritten face for manifesto-style accents), all landing as CSS custom properties in one pass rather than getting bolted on feature by feature.

A backlog for a blog

The part that would look strange to most solo bloggers: every feature on this site is a numbered story in .claude/backlog/, written and closed like a real product backlog — hero section, navigation, footer, about page, blog listing, post template, syntax highlighting, SEO tags, RSS, a 404 page, a contact page, deploy pipeline, then a second wave for the palette foundation, category taxonomy, and the myth/signal callout components that let a post argue both sides of a claim in the same layout.

That structure earned its keep almost immediately. When the color palette had two competing sources — one in my notes, one in the design kit — the story documented the conflict, the decision (the notes version had all ten required colors; the design-kit version was missing two), and the reasoning, right in the file next to the CSS change. Six weeks later I don’t have to remember why; I can read it.

Migrating nineteen old posts without losing the voice

The hardest part wasn’t new content — it was the old content. Nineteen posts sat in a WordPress SQL backup under archive/, written between 2011 and 2013 by a much earlier version of me who’d just lost 100 pounds and was mad at the food pyramid about it. I pulled them out of the SQL dump, staged them as Markdown with the original WordPress metadata intact. Claude wrote 19 migration stories and one reference story (031) that an implementation sub agent could reference. The orchestration agent tracked every per-post migration story and delegation to implementation sub agents.

That reference story did the unglamorous work: mapping old WordPress categories onto the new taxonomy (primal-nutrition, metabolic-health, food-myths, field-notes, big-food, experiments), flagging dead Flash embeds and hotlinked images from sites that don’t exist anymore, and drawing a hard line on the text itself — fix a genuine typo, don’t touch the voice. Some of the funnier original typos survived that filter on purpose (excercise, dispell, trippled) because sanding them off would have made 2011-me sound like he was writing for 2026-me, which isn’t the point of a field note.

A ~25-link spot-check across the batch turned up the expected graveyard — dead Hulu and Flash embeds, domains that now resolve to gambling sites, hosts that just stopped existing — replaced with plain links where a working redirect existed, or dropped entirely where it didn’t.

Shipping without a build step, deploying without a password

Adding a post is just dropping a Markdown file into src/content/blog/ — Astro’s content-collection loader picks it up on the next npm run dev with no build required, and a draft: true flag keeps anything half-finished out of the production build and the RSS feed until it’s ready.

Getting that build to Bluehost went through one real revision: it started as FTP, then moved to SSH-key-based rsync once I didn’t want a password sitting in GitHub secrets. The current pipeline is a single GitHub Actions job — checkout, npm ci, npm run build, load the deploy key into an SSH agent, pin the host key fresh each run, then rsync -avz --delete the dist/ output straight to the addon domain’s document root. --delete means the server always mirrors exactly what’s in git, which is the property I wanted — but it’s also the one part of this pipeline I ran a --dry-run pass against before trusting it near production, because a mirror that deletes is only as safe as the exclude list backing it up.

What’s still open

The backlog isn’t empty — prev/next post navigation and clickable category-chip filtering on the listing page are both still queued. That’s the part of building in the open that actually matters: the stories that are done are done because they were written down and closed one at a time, and the ones that aren’t yet are sitting right there in the same file, waiting their turn instead of living only in my head.