From WordPress to EmDash: Building a Modern Personal Site on Cloudflare for $5 a Month

I ran the same WordPress site for 15 years. Here is how I rebuilt it on EmDash and Cloudflare Workers, built custom plugins, and cut my monthly hosting bill to $5.

A WordPress Site That Served Its Time

About 15 years ago I won a StudioPress license at WordCamp Raleigh. At the time it felt like a real score. The Genesis Framework paired with the Authority Pro theme gave me a clean, well-built personal site without having to design everything from scratch. It had a blog, a projects section, a contact form, and it held up well for years.

But WordPress ages in place. What started as a lean setup gradually accumulated plugin updates, PHP version warnings, security patches, and the low-grade anxiety that comes with running a PHP and MySQL stack for a site that publishes a few posts a year. Hosting costs were never outrageous, but for a personal site with light traffic they were always more than felt reasonable.

I wanted something faster, cheaper, and built on technology I actually enjoy working with.

Finding EmDash

I found EmDash while exploring the Cloudflare ecosystem. The description that resonated was "spiritual WordPress successor." EmDash is a CMS built on top of Astro with a full admin interface, using Cloudflare Workers for compute, D1 (Cloudflare's edge SQLite database) for content storage, and R2 for media. No servers to manage, no PHP, no MySQL.

What sold me was the architecture. Every page is server-rendered at the edge, close to whoever requests it. There is no caching layer to configure or CDN to bolt on. Performance is a default, not a feature you configure. The admin panel has the familiar structure of a WordPress dashboard, but under the hood it is a completely different system.

The Tech Stack

The site runs on Astro as the framework, handling routing, layouts, and server-side rendering. EmDash integrates as an Astro plugin and provides the CMS layer: content collections, the admin UI, full-text search, sitemaps, and media management.

The infrastructure is entirely on Cloudflare. Workers serve every request. D1, a SQLite database that runs at the edge, stores all content. R2 handles image uploads and media storage. GitHub Actions deploys to Workers automatically when I push to either branch. There is no VPS to patch, no managed database subscription, and no CDN configuration to worry about.

Getting It Running

The base site came together over a weekend. EmDash's documentation is solid for a project that young, and the Astro integration is clean. I started from the EmDash starter, defined my content types in the seed file, and had the admin UI working locally within a few hours.

The interesting problems came later. Site search for logged-out visitors was the first real puzzle. EmDash's built-in search endpoint requires authentication, and the auth middleware runs before any user code, so it cannot be intercepted the usual way. My solution was a public Astro API endpoint that calls the EmDash search function server-side, combined with a small client-side fetch interceptor that reroutes the search component's requests to that endpoint. It works cleanly.

The second issue came from how EmDash creates database tables. The table for a content type is created on the first save through the admin. If you insert collection metadata directly into the system tables, which I did when setting up custom collections, the content table does not get created automatically. I discovered this when my portfolio and press item saves started throwing errors. The fix was creating the tables manually with the correct column set.

Schema drift between environments was the third issue. After upgrading EmDash versions, production and staging can end up with different columns. I wrote a sync script that queries the column list on both sides and only inserts columns that exist in the target database, which handles the mismatch gracefully.

Custom Plugins

EmDash has a plugin system, and I used it to replace four WordPress integrations that were previously handled by shortcodes and third-party plugins.

The YouTube Feed plugin pulls my latest videos from the YouTube Data API and renders them on the site. The data is cached in Cloudflare's Worker cache for 24 hours, so the API is not hit on every request.

The GitHub Projects plugin shows my public repositories and recent activity. Same caching approach. This replaced a GitHub widget I had been embedding as an iframe for years.

The Docker Hub plugin lists my published Docker images. The data is fetched via an authenticated API call and cached in the Worker for 24 hours.

The QRZ Logbook plugin integrates with QRZ.com to show my amateur radio contact log. I am licensed as K1DUG, and displaying the logbook has been a fixture of my personal sites for years. The plugin version is cleaner than any iframe solution I had used before.

Each plugin lives in a local plugins/ directory in the repository. They are not published npm packages, just local integrations wired into the Astro config. Building them gave me a real feel for how EmDash's plugin hooks work and how to structure data fetching for the Cloudflare edge runtime.

Custom Content Types

I added two content types beyond what EmDash provides out of the box: portfolio items and press items.

Portfolio items document projects and websites I have built, with fields for the URL, project type, description, and a screenshot. Each item has a detail page that redirects to the external URL when one is set, so the Live View link in the admin goes straight to the actual project.

Press items are for media mentions and coverage. Each one has an outlet name, publication date, a link to the original piece, and an optional clipping image. The detail page renders a summary with a link back to the source.

Both are new to this version of the site. The old WordPress setup had no equivalent. Adding them was straightforward once the custom content type workflow clicked, and they are now a meaningful part of the site that the old version simply could not support without a significant plugin investment.

The Workflow

The deployment setup is two branches and two Workers. Pushing to the develop branch deploys to the staging Worker automatically via GitHub Actions. Pushing to main deploys to production. The staging site is IP-restricted so only I can reach it, and it sends noindex headers so search engines ignore it entirely.

I used Claude Code as an AI pair programmer throughout the build. Not to generate boilerplate, but to work through specific problems: understanding EmDash internals, debugging D1 edge cases, writing plugin logic, and designing the public search workaround. The ability to have a conversation about a specific technical constraint, with the AI reading the actual codebase, is a genuinely different experience from searching documentation or forums.

I also built a script to sync production content down to the staging database. It is a common need during development, and having a reliable way to handle it saves time. The script queries both databases, handles schema differences between environments, syncs revisions before content tables to satisfy foreign key constraints, and runs in about 30 seconds.

What It Costs

The Cloudflare Workers Paid plan is $5 a month. That covers both the production Worker and the staging Worker. D1 and R2 storage are effectively free at personal site scale. The free tier allowances are generous, and a personal site with a few hundred posts and a few gigabytes of media is nowhere near the limits.

A typical WordPress hosting plan for a personal site with decent performance runs $10 to $20 a month, and more if you want a separate staging environment. With Cloudflare I get better performance, two full environments, and more control for $5 a month. There is no database subscription, no CDN add-on, and no backup service to pay for separately.

Should You Try EmDash?

If you are comfortable with JavaScript and TypeScript, want a modern architecture, and are interested in the Cloudflare ecosystem, EmDash is worth serious consideration. The admin interface is approachable, the content model is flexible, and the Astro foundation gives you a lot of frontend capability from the start.

If you need something you can hand to a non-technical user, or if you depend on the breadth of WordPress's plugin ecosystem, EmDash is not there yet. It is a young project and some rough edges still show. But it is moving fast: it went from version 0.1.1 to 0.3.0 while I was building this site, adding native sitemaps, bylines, composite database indexes, and a comments system.

For me it was the right call. The site is faster, cheaper, and more interesting to work on than the WordPress version. I understand every layer of it, and when something breaks I know exactly where to look. If you have a personal site on aging WordPress infrastructure and have been looking for a reason to rebuild it on something modern, EmDash is worth a look. The Cloudflare free tier covers local development, the $5 a month paid tier handles production, and the documentation has enough to get you started. It is not a drop-in replacement for WordPress, but for a developer who wants to own their stack, it is a solid foundation.