← /blog

Migrating from WordPress to Cloudflare Workers in 2026

Migrating from WordPress to Cloudflare Workers in 2026

After years on a WordPress site that I rarely touched, I finally pulled the trigger on a full rebuild. The new yigittanriverdi.com runs as a single Cloudflare Worker, with Astro 6 for SSR, D1 for content, and an admin panel I can edit from anywhere. Here is the why and the how.

Why now

WordPress was fine. It rendered. It was slow on mobile, the admin felt heavy, and every time I opened it I lost ten minutes deciding whether to publish anything. The friction was real.

Three things changed my mind:

  • The Cloudflare stack genuinely is a fullstack platform now. Workers, D1, R2, KV, Cloudflare Access, plus the Vite-based developer tooling. It is not just a CDN anymore.
  • I wanted to author posts from my terminal. My day job is in TypeScript. Logging into a CMS feels like pulling on a costume.
  • I want to be able to ship the same kind of stack I would build for a client. There is no honest way to recommend a stack you do not use yourself.

The stack

  • Single Cloudflare Worker, deployed with Workers Static Assets (the new unified pattern, not Pages).
  • Astro 6 in SSR mode for the public site.
  • D1 (SQLite at the edge) as the source of truth for posts, projects, and site settings.
  • R2 for binary assets (images, the CV PDF).
  • Cloudflare Access for the admin route in production.
  • Tailwind CSS v4 for styling.
  • TypeScript end to end.

The data model

Three tables do the real work:

  • posts for the blog (title, slug, body, status, timestamps).
  • projects for the home-page cards (title, tagline, description, status).
  • settings is a single key/value row holding the JSON blob the admin edits.

Drizzle ORM gives me typed queries over D1. The D1 migrations live in src/db/migrations as plain SQL files and are applied with wrangler d1 migrations apply.

Two write paths into the same source of truth

This was the load-bearing decision. I wanted to author from two places: an admin UI and the terminal.

  • The admin UI is a small Astro page behind a session cookie. Forms post to /api/admin/*.
  • The CLI is a tiny script that POSTs to /api/posts with a bearer token. From anywhere I have a shell, pnpm new-post --title "..." --body-file draft.md ships a post.

Both paths funnel through the same service layer in src/lib/posts.ts. One source of truth, two ergonomic surfaces.

What surprised me

A few things were less polished than I expected:

  • The Astro Cloudflare adapter (v13) emits dist/server/entry.mjs and tries to validate the wrangler.toml main path at config-load time, before the build has produced it. I had to leave main out of wrangler.toml and pass it on the deploy CLI.
  • The @cloudflare/vitest-pool-workers v0.14 rewrite dropped the ./config export. I inlined readD1Migrations to apply migrations to the test runtime.
  • Astro 6 auto-provisions a SESSION KV binding for its session feature. If you do not use sessions, set the session driver explicitly to lruCache so the adapter stops asking for KV.

These are the papercuts you only learn about by shipping.

SEO without ceremony

A server-rendered site lets you cheat at SEO, but I wanted the real signals:

  • Server-rendered HTML on every page (no JS required to read the content).
  • A <SEO> component that emits canonical, OpenGraph, Twitter, and JSON-LD BlogPosting, Person, and BreadcrumbList.
  • Sitemap and RSS generated on request from D1.
  • A robots.txt that points at the sitemap and disallows /admin and /api.
  • Human-readable URL slugs validated against ^[a-z0-9]+(?:-[a-z0-9]+)*$ server-side.

That is the entire SEO surface. Google does the rest.

What is next

I am writing this on the same site. The blog is live, the admin panel works, the CV is one click away. Next up is a nightly cron that exports D1 to git as markdown for free version history.

The RSS feed is the lowest-friction way to catch the next post.

Built on Cloudflare, in Berlin, in 2026.