← All writing

Moving this site off WordPress

This site was WordPress. I wanted a folder of markdown that turns into HTML, with nothing to log into and nothing loaded from someone else’s server.

Claude designed the first version of that. One stylesheet. A light theme and a dark theme. An orange accent. Three fonts that ship with the site: Inter for the text, Newsreader for the headlines, JetBrains Mono for the code. The rules were strict on purpose. No client-side framework, no database, no CMS, and no inline styles or scripts. The Content-Security-Policy only allows files from this domain, so a style="" attribute or a pasted <script> does nothing in production. Fonts come from the build, not a CDN.

That first version was a small script, build.mjs. It was enough to publish a post. It was not enough once I wanted the quiet parts of a blog: resized images, a page for each tag, a full RSS feed, links on headings, highlighted code, search, and a social card per page. Those are build-time jobs. Eleventy already did them, and the posts could stay markdown. Eleventy does not run in the browser. npm run build writes the finished site to dist/. npm run dev serves it locally and rebuilds when a file changes.

A few of those pieces are worth remembering:

  • Search is Pagefind. Its JavaScript loads on /search/ only.
  • Social cards are PNGs made during the build. They are the og:image tag, not a picture on the page. The variable font used in the CSS would not render in that step, so the cards use a static Inter file.
  • Images in a post are normal markdown. The build makes smaller copies.
  • A link checker runs at the end of the build. It should fail the build for a broken link inside the site. It should not fail the build because a 2009 post links to a dead page on the public web.

Getting the old posts out was the slow part. The WordPress REST API answered 401 and 406, so that door was closed. The public sitemap and the public HTML were enough. Each post kept its original date and slug. The URL is still /YYYY/MM/DD/slug/, which is what the old permalinks were. Existing links can survive the move.

The conversion had a few sharp edges. WordPress wrapped code in highlighter markup, and a careless cleanup turned a C# sample into one long line. It also ate /// <summary> comments, because those look like HTML tags. Protect the code block first, then decode entities.

Deploy is Cloudflare Pages, connected to the GitHub repo. Framework preset None. Build command npm run build. Output directory dist. Node 20 or newer. public/_headers is the CSP and the cache rules. public/_redirects sends the old /feed, /category/*, /page/*, and /author/* URLs somewhere that still exists. A push to main publishes. Any other branch gets a preview URL.

The last step was moving the DNS so samuelhaddad.com pointed at that project. That is what replaced WordPress.