Building Zerberos Labs: Astro on Cloudflare Pages


After coming from WordPress previously, I figured I would document a quick write-up on what it took to stand this Astro-based blog up and why. If you’re thinking about doing the same, hopefully a few of the gotchas below save you the hour I lost to them (thank you Claude).

Why Astro over Hugo

I started by doing some research to compare the two top web framework options, Astro and Hugo, against one another. Hugo is hard to beat for pure-blog use cases: single binary, no Node, fast builds. There were some tradeoffs however:

  • Hugo’s templating is limited to Go’s html/template. No components, no JSX, no React.
  • There’s no clean path to embedding interactive UI (filterable tables, an EID lookup widget, etc.) without bolting on raw JS by hand (which I already barely understand to that extent).
  • Astro uses an islands architecture, which means it ships zero JS by default and only hydrates the components that need to be interactive.
  • Astro supports React natively, so I can drop components anywhere, including inside a blog post.

For a site I want to grow beyond pure blogging (DFIR tooling, embedded reference widgets, possibly a standalone EID lookup page, etc.), Astro is the better foundation. Hugo would have likely shipped the blog faster, but I’d have hit limitations on just post #2. There was also something satisfying about building something that could evolve with me over time and match my identity.

Prerequisites

  • Node.js (brew install node, verify with node -v) - write down your version, you’ll need it for Cloudflare
  • Visual Studio Code for editing
  • A GitHub account
  • A Cloudflare account with your domain already managed there (I leveraged Cloudflare Pages to host)

Creating the project

npm create astro@latest [blog-or-repo-name]

Pick the blog template when prompted, say yes to TypeScript, and let it install dependencies.

Then add React for interactive island components:

cd [blog-or-repo-name]
npx astro add react

Run locally:

npm run dev

Astro previews at http://localhost:4321 and hot-reloads on save.

Where things live

Below is a short list of the files and folders I found mattered most as I was getting started:

FilePurpose
src/consts.tsSite name and description, referenced site-wide
astro.config.mjsSet site: 'https://[DOMAIN]' here
src/content.config.tsZod schemas for blog post frontmatter validation
src/content/blog/Markdown and MDX post files
src/pages/index.astroHomepage
src/components/Header.astroSite header
src/styles/global.cssGlobal styles

A couple of quick notes:

  • The site field in astro.config.mjs is only used at build time for RSS, sitemap, and canonical URLs. Doesn’t affect local dev. I ended up setting this early, even though my blog wasn’t “live” yet.
  • Newer Astro versions moved the content-config file out of the content/ folder up to src/content.config.ts. Same file, slightly different path than older docs describe. I spent a stupid amount of time stuck on this.

GitHub + Cloudflare Pages

Push the repo to GitHub but keep it private - Cloudflare Pages works fine with private repos via OAuth, and the OAuth connection itself doesn’t expire or take the site down if anything wobbles (the CDN keeps serving the last successful deployment regardless). When authorizing Cloudflare on GitHub, choose Only select repositories and pick just this one. Security first, obviously.

In Cloudflare:

Build settings I used:

SettingValue
Framework presetAstro (auto-detected)
Build commandnpm run build
Build output directorydist
Environment variableNODE_VERSION = output of node -v on your machine

Custom domain

After the first successful deploy: Pages project → Custom Domains → Add Domain → enter your subdomain (labs.zerberos.io for me). Because my domain’s DNS is already on Cloudflare, the CNAME is auto-created and SSL is provisioned automatically. Usually live within a few minutes. Magic.

The deploy loop

As I grow this blog by adding posts or making tweaks to the underlying UI (like adding in a dark mode toggle), I have fallen into the following deployment loop:

  1. Edit files locally.
  2. Preview changes at localhost:4321.
  3. Push to GitHub.
  4. Cloudflare auto-rebuilds and deploys. No manual steps.

Based on the above setup, all I have to do to create a new blog post is whip up a Markdown file (.md) or an enhanced file that can run React components (.mdx), follow standard Markdown formatting, add in any React components as needed, and push to GitHub. Then it’s live.

Build status lives under the Deployments tab in the Pages project within Cloudflare’s portal. Old deployments stay in history, which can be useful for one-click rollback if something implodes in live time.

What came after

A few things weren’t done on day one but have been added since:

Support for dynamic mobile layouts

This was actually an extremely frustrating late realization when I first deployed the blog, opened it on my phone, and saw nothing but a garbled mess. I went back and used Chrome’s developer tools (FN+F12 on my MacBook) to preview the site in different mobile aspect ratios, fixing the header and blog post sizings. Small changes like this can actually involve multiple Astro files, so utilizing tools like Claude Code or OpenAI’s Codex can help simplify the lift.

Dark mode toggle

Dark mode doesn’t really need an explanation (it’s just better) so I wanted the blog to have that option. Astro did not have it baked in from the start, so I added in the button you see in the site header as a manual toggle, as well as system preference detection that persists via localStorage and a data-theme attribute on <html>. I also leveraged Claude Code to help me implement this.

Embedded React islands in posts

React allows you to add interactive elements to Astro, so not eveything us just plain Markdown format. My first real use of this was the EID preview widget in the EIDVault launch post, which also validated my decision to use Astro and MDX files.

Hopefully all of the above helps anyone looking to do something similar. The functionality of this platform allows me more flexability compared to a blog via WordPress, which also opens the door to some additional ideas I could explore down the road. For example, I could create a standalone EID lookup page here driven by the windows-eid-data JSON dataset, the same source of truth EIDVault uses, served as a free web tool.

More to come if I ever take that on.

ZB