dotmeps

Hi, I'm Andy — a Solutions Architect based in Kent, England. By day I design systems and help teams work out how all the pieces fit together. Away from the keyboard you'll find me out walking my dogs, listening to music, or hunched over a desk painting Dark Angels for Warhammer 40k.

Building this blog on Cloudflare Workers

2026-10-29 · #cloudflare #meta #workers

Every so often I get the urge to have a small corner of the internet that's actually mine. Not a profile on someone else's platform, just a page with my name on it, a few posts, and nothing I don't understand. So I built one, and this is the first post on it: a write-up of how it works.

The brief I gave myself was simple: monospace, black on white, fast, and cheap enough to forget about. Day to day I spend a lot of time working with complex distributed .NET systems, so it was nice to build something outside of my usual remit for once.

The stack

There's no front-end framework and no build step for the pages. Each page is a template string in TypeScript that renders HTML on the server. The CSS is about twenty lines and sits inline in the page.

Writing posts

Posts are written in Markdown and rendered on the server with marked. The output goes through sanitize-html before it reaches the page. It's my own blog and I'm the only one writing, but sanitizing costs nothing and means a stray <script> pasted from somewhere can never run.

To write posts there's a private editor page with the Toast UI WYSIWYG editor, so I don't have to think in Markdown when I don't want to. The editor loads from Toast's CDN, and if that ever fails the page falls back to a plain textarea. Writing a post should never depend on a third-party script loading.

Each post gets a URL slug from its title when it's created, and the slug never changes after that. If I fix a typo in a title later, existing links don't break.

Locking down the editor

There's exactly one user (me), so there's no user table and no sign-up. A password lives in a Cloudflare secret, and logging in sets a signed, HttpOnly, Secure, SameSite=Strict cookie that's only sent to the editor's pages. A few small details matter:

On top of that, Cloudflare's rate limiting and bot protection sit in front of the site, so most nonsense never reaches the Worker at all.

Tags

Tags turned out to need only one extra table:

CREATE TABLE post_tags (
  post_id INTEGER NOT NULL REFERENCES posts(id) ON DELETE CASCADE,
  tag TEXT NOT NULL,
  PRIMARY KEY (post_id, tag)
);

There's no separate tags table, because at this scale it would just be ceremony. Tags are cleaned up when a post is saved: lowercased, spaces become hyphens, duplicates are removed, and there's a sensible cap on how many a post can have. If I type "cloudflare, meta, Cloudflare", the post ends up tagged #meta #cloudflare. Every tag links to its own page, like #cloudflare.

Saving a post and its tags happens in one D1 batch, so a post can never end up half-saved with its tags missing. Deleting a post also removes its tags, through the foreign key.

Images

I wanted to paste or drag images into the editor without hosting them anywhere else. Toast UI has an upload hook for this. When an image is added, the browser sends the raw bytes to the Worker, the Worker stores them in R2, and the editor inserts the returned URL into the post.

Accepting files from the internet, even from yourself, deserves some care:

One bug got through. The editor's image dialog has a "description" field, which is exactly what should become the image's alt text, but my upload hook always passed the file name, and that quietly overrode whatever I'd typed. Now the description wins, and the file name is only used when there isn't one, such as when pasting a screenshot. The original file name isn't lost either: it's stored with the image, so "Save image as" suggests a sensible name instead of a long random ID.

Testing it

There are two layers of tests:

Writing the Playwright tests found the kind of problems unit tests never do. For example, the page is narrow enough that the editor tucks its image button into a "more" menu, so the test has to open that menu first.

Keeping it cheap

The whole point was to forget about hosting costs, so it's worth knowing where they come from. Static files like the logo are served for free. Each page view is one Worker request and a few small database reads, which fits comfortably inside Cloudflare's free allowances for a blog this size. Cloudflare's rate limiting and bot protection stop abusive traffic before it reaches the Worker, and billing alerts mean I'll hear about it if usage ever spikes.

What's next

Probably an RSS feed, and maybe serving images straight from R2 through Cloudflare's cache instead of through the Worker. Mostly, though, the plan is to actually write things: about architecture, about the dogs, and almost certainly about painting more Dark Angels than any reasonable person needs.

The site's code is about a dozen small files, and I built it with Claude Code as a pair programmer, which made getting from idea to deployed much quicker. If you're after something similar, I'd recommend the same shape: a Worker, a database, a bucket, and as little else as you can get away with.

all posts →