Modernizing the site
tl;dr: this site is now built with Astro and served as static files from Cloudflare Workers. The Linode VPS, Caddy and the Cloudflare Tunnel are on their way out.
Problem
My first post promised a whole series on how I built this site. Then I wrote one more post and the site sat untouched for four years.
When I finally came back to it, here’s what I found:
- Hugo 0.92.2 with the LoveIt theme, both pinned to 2022.
- A Linode VPS running Caddy, reached through a Cloudflare Tunnel, that existed only to serve this site. That meant an OS, a web server and SSH to keep patched for a handful of static pages.
- A resume last updated in March 2022.
- No repeatable way to write a post. Every time I’d have to remember how the theme wanted things laid out.
Nothing was broken, but it was the kind of setup where the first time you try to update it, everything has rotted.
Environment
Before
- Hugo 0.92.2 + LoveIt theme
- Linode VPS running Caddy
- Cloudflare Tunnel (
cloudflared) from the VPS to Cloudflare - Cloudflare for DNS/CDN, Namecheap for the domain
After
- Astro with a small custom theme (light and dark mode)
- A private GitHub repo as the single source of truth
- Cloudflare Workers static assets, built and deployed on every push to
main - Cloudflare for DNS/CDN, Namecheap for the domain (unchanged)
Fix
1. Rebuild on Astro instead of upgrading Hugo
Upgrading Hugo across four years of releases plus an old theme looked like a project of its own. Astro let me start clean:
- Posts are a content collection with a typed schema: title, description (capped at 160 characters), date, category and tags. If I get the frontmatter wrong, the build fails instead of quietly publishing a broken page.
- The resume is data, not a page. Jobs and certifications live in YAML files, and the resume page is generated from them. Updating it means editing a list, not fighting HTML.
npm run new -- "Post title"scaffolds a draft with the sections I actually use for work write-ups: Problem, Environment, Fix, Verify, Gotchas. This post started from that template.- Both old posts and the full resume came across, updated where needed.
2. Don’t break the old links
Hugo put posts at the site root (/what-this-is-all-about/), while Astro puts them under /posts/. A _redirects file returns real 301s from every old URL to the new one, and /tags/* goes to the new categories page. The RSS feed stays at /index.xml, so existing subscribers didn’t have to change anything.
3. Add guardrails
- GitHub Actions runs
astro checkand a full build on every push and pull request, plus once a month so dependency rot shows up even if I don’t post. - Dependabot opens PRs for npm and Actions updates.
- A
_headersfile sets security headers (CSP,nosniff,Referrer-Policy,Permissions-Policy,X-Frame-Options) and caches Astro’s hashed assets for a year.
4. Move hosting to Cloudflare
Cloudflare already handled my DNS and CDN, so letting it serve the files too removed the server entirely. I weighed the trade-offs first:
- What you lose: shell access to the box, raw access logs, the ability to run other things next to the site, and some provider separation. Cloudflare is now DNS, CDN, build system and origin all at once.
- What you gain: no OS, web server or SSH to patch, no server bill, and a preview URL for every branch.
For a static resume and blog, that’s an easy call.
I planned it as a Cloudflare Pages project, but Cloudflare’s “connect a Git repo” flow now drops you into Workers by default. Workers can serve a static site the same way, supports the same _redirects and _headers files, and is where Cloudflare is putting its new features, so I went with it. The only addition to the repo was a small wrangler.jsonc:
{
"name": "sebastianscherrer",
"compatibility_date": "2026-10-08",
"assets": {
"directory": "./dist",
"not_found_handling": "404-page",
"html_handling": "auto-trailing-slash"
}
}
The name has to match the Worker’s name in the dashboard exactly, or the build fails.
5. Cut over DNS
Before touching anything, I exported the zone file and committed it to the repo so rolling back would take a minute. The apex and www records pointed at the tunnel. Adding them as custom domains on the Worker replaced those two records and left mail (MX, DKIM) alone. Because it’s all one Cloudflare zone, there’s no propagation to wait on.
Verify
Against the real domain, I checked:
- Every page returns
200, and a bad URL returns the custom404. - The old Hugo URLs return
301with the rightLocation:
curl -sI https://sebastianscherrer.xyz/what-this-is-all-about/ | grep -Ei '^(HTTP|location)'
/index.xmlhas links on the real domain, not a preview hostname.- The security headers come back on every response.
Gotchas
- Negative DNS caching. For a few seconds between deleting the tunnel record and adding the Worker domain,
wwwdidn’t exist. My Mac’s resolver cached that NXDOMAIN, sowwwfailed locally while it worked fine from outside my network. A DNS cache flush fixed it. If one name works and another doesn’t right after a cutover, check from outside your network before you start debugging the site. - Pages vs. Workers. If the setup screen shows a “Deploy command:
npx wrangler deploy” field, you’re in Workers, not Pages. That’s fine for a static site, but you need thewrangler.jsoncabove, or the first build fails. - Keep the rollback for a while. The old server stays up during a soak period before it’s deleted. Once it’s gone, the only way back to the old site is the git history.
What’s next
Actually writing. The whole point of this rebuild is that a new post is now one command, a Markdown file and a git push. The VPS hunting series is probably dead, but there’s a backlog of Juniper and Palo Alto write-ups that should have lived here years ago.