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 withnode -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:
| File | Purpose |
|---|---|
src/consts.ts | Site name and description, referenced site-wide |
astro.config.mjs | Set site: 'https://[DOMAIN]' here |
src/content.config.ts | Zod schemas for blog post frontmatter validation |
src/content/blog/ | Markdown and MDX post files |
src/pages/index.astro | Homepage |
src/components/Header.astro | Site header |
src/styles/global.css | Global styles |
A couple of quick notes:
- The
sitefield inastro.config.mjsis 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 tosrc/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:
| Setting | Value |
|---|---|
| Framework preset | Astro (auto-detected) |
| Build command | npm run build |
| Build output directory | dist |
| Environment variable | NODE_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:
- Edit files locally.
- Preview changes at
localhost:4321. - Push to GitHub.
- 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