When I started this blog about a year ago, I did the laziest possible thing: pushed the source to a public GitHub repo, flipped the GitHub Pages switch, pointed blog.diego.dev at d6o.github.io, and forgot about it. It worked. I wrote five posts. I moved on.
Last week I came back to write something new, looked at the browser tab, and realized two things at once. First, I no longer wanted my blog on infrastructure I don’t run. Second, PaperMod is the most-cloned Hugo theme on the planet, and my blog was visually indistinguishable from countless others. Both had been quietly nagging me for a while. I decided to fix them in one sitting.
This is the story of that afternoon: moving the deploy off GitHub onto my own Gitea instance plus Cloudflare Pages, and then rewriting the theme from scratch.
Why move off GitHub Pages
GitHub Pages works fine. The free tier is generous, the workflow is documented, and TLS renews itself. There is nothing wrong with it.
What was wrong, for me, was the same thing that’s wrong with anything I don’t operate myself: it’s a black box that lives on someone else’s policy decisions. I run a self-hosted Gitea on my own k3s cluster (git.ri.gd) and a Cloudflare account where every other static site I’ve shipped in the last six months follows the same pattern—Gitea repo, Gitea Actions workflow, wrangler pages deploy, proxied CNAME. The blog was the last holdout. Static-site hosting isn’t where I want my taste to break.
The shape
The pattern I use for every static site I run looks like this:
- Source repo on Gitea, named after the bare domain (
blog.diego.dev). - A Cloudflare Pages project, named with dashes because Pages doesn’t allow dots in project names (
blog-diego-dev). - A
.gitea/workflows/deploy.ymlthat builds the site and runswrangler pages deploy publicagainst that Pages project. - A proxied CNAME in the Cloudflare DNS zone pointing the public domain at
<project>.pages.dev. - Two repo secrets,
CLOUDFLARE_API_TOKENandCLOUDFLARE_ACCOUNT_ID, so the workflow can authenticate.
Migrating the blog meant recreating the source repo in this shape and flipping the existing CNAME. The DNS for diego.dev was already on Cloudflare, so I didn’t need an NS change—just a PATCH on the existing record’s content field, swapping d6o.github.io for blog-diego-dev.pages.dev.
The migration itself
I created the Gitea repo via the API:
curl -X POST -H "Authorization: token $GITEA_API_TOKEN" \
-H "Content-Type: application/json" \
https://git.ri.gd/api/v1/user/repos \
-d '{"name":"blog.diego.dev","auto_init":false,"default_branch":"main","private":false}'
…added the two Cloudflare secrets to the new repo, created the Cloudflare Pages project, and attached blog.diego.dev as a custom domain. Cloudflare lets you attach the domain before any code has been pushed; it sits at status initializing until DNS resolves.
In the local clone I:
- Renamed
origintogithub(kept the GitHub repo as a passive mirror). - Added the Gitea remote as the new
origin. - Replaced
.github/workflows/hugo.yamlwith.gitea/workflows/deploy.yml. - Untracked the committed
public/directory—a legacy from before GitHub Actions started doing the build.
Push to Gitea, watch the workflow run, flip the CNAME content. Within minutes, the Pages custom domain validated and blog.diego.dev was live on the new pipeline. As far as readers were concerned, nothing changed.
Two things did try to stop me along the way.
Gotcha 1: peaceiris/actions-hugo on Gitea
My first version of .gitea/workflows/deploy.yml used peaceiris/actions-hugo@v3 to install Hugo, mirroring the GitHub Actions pattern I’d seen elsewhere. On the Gitea Actions runner the step never finished. The job sat at running for over five minutes with no log output beyond the initial checkout. I cancelled it.
The fix is something I should have known: third-party Actions like peaceiris/* are not reliable on my Gitea Actions setup. The runner’s DNS resolution to fetch from GitHub’s action registry is flaky enough that the job hangs rather than failing fast. The first-party actions/checkout@v4 works because Gitea proxies it specially.
Every other Hugo build in my fleet installs Hugo by wget-ing the .deb directly from the GitHub releases page:
- name: Install Hugo
run: |
wget -q "https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_linux-amd64.deb"
sudo dpkg -i "hugo_extended_${HUGO_VERSION}_linux-amd64.deb"
hugo version
Not glamorous. Works in seconds. Never hangs.
Gotcha 2: Hugo 0.145 broke PaperMod
After fixing the runner step, the build ran. And immediately failed:
ERROR deprecated: site config key paginate was deprecated in Hugo v0.128.0 and subsequently removed.
ERROR deprecated: .Site.Social was deprecated in Hugo v0.124.0 and subsequently removed.
I had bumped Hugo from 0.128.0 (what the old GitHub Pages workflow used) to 0.145.0 to match the rest of my fleet. That broke three things in the older PaperMod fork I had checked into the repo:
- The top-level
paginateconfig key was removed in favour ofpagination.pagerSize. .Site.Socialwas removed in favour of.Site.Params.Social. PaperMod’s templates had a backward-compat fallback that referenced both, but in 0.145 even referencing the removed key is a hard error.- The
partials/prefix inpartial "partials/templates/_funcs/get-page-images"calls became a hard error.
The fastest path to a working build was to pin Hugo back to 0.128.0. The deeper fix was to upgrade or replace PaperMod entirely. I made a note and shipped the migration with the pin in place. By that evening the blog was deploying through Gitea Actions to Cloudflare Pages, end to end on my own pipeline.
And then, with the migration done, I sat with the result.
The other problem
Looking at the blog with fresh eyes for the first time in months, the visual situation became impossible to ignore. PaperMod is fine. It is also the theme I see on every personal dev blog I land on, and the chrome it adds—top nav with Search/Tags/Archive, share buttons under every post, breadcrumbs, reading-time progress bar, light/dark toggle, social icons—stops earning its space the moment you start writing.
I have a personal site at diego.dev that’s the opposite of that. It’s a single HTML file: a name, a few links separated by pipes, a list of jobs, a small <style> block at the top of <head>. Zero dependencies. It auto-switches between light and dark based on prefers-color-scheme. It’s exactly what I want a personal site to be.
I wanted the blog to feel like the same person made it.
Designing it as a document
The constraint I gave myself was tight: extend diego.dev’s visual language to long-form blog content. Same colors. Same card-on-neutral-background layout. Same auto dark mode. Same instinct about what to leave out.
Concretely:
- Color palette, lifted unchanged. Light is
#f4f4f4page bg,#ffffffcard surface,#333333text,#007BFFlinks. Dark is#121212/#1e1e1e/#e0e0e0/#82aaff. Switching is automatic viaprefers-color-scheme. There is no toggle button—anyone who wants dark mode already has their OS set to dark mode. - One accent color. Blue links. No second accent. No semantic colour (success, warning, error). No category-coded tags. The only colourful thing on the page is a link.
- Typography is the system font stack.
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-seriffor body.ui-monospace, Menlo, "Cascadia Code", Consolas, "Liberation Mono", monospacefor code. No web fonts. No Google Fonts. Zero CLS from font swap. - The blog reads as a document. A single white card centered on the neutral background, 800px wide on the home and 720px on post pages (the readable optimum for long-form prose). All the chrome of the previous theme is gone. The site header is one line: site title plus a strip of links separated by middle dots. That’s it.
I wrote a DESIGN.md to capture the system before touching any templates—color tokens, type scale, spacing, layout, motion (none, deliberately; the only state change on the entire site is the link underline on hover). Then I wrote the theme.
The theme: 11 templates, one CSS file, zero JS
themes/diego/ is a small custom Hugo theme that implements DESIGN.md directly. The whole thing is:
- 11 template files.
baseof.html,single.html,list.html,index.html,archives.html,terms.html,taxonomy.html,404.html, plus partials forhead,header,footer, and apost-card. - One CSS file.
assets/css/main.css, around 290 lines, fingerprinted via Hugo’s resource pipeline so cache headers can be aggressive. - No JavaScript anywhere.
Most of the CSS is the variable definitions and the dark-mode media query:
:root {
--bg: #f4f4f4;
--surface: #ffffff;
--text: #333333;
--muted: #666666;
--link: #007BFF;
--hairline: #e0e0e0;
--shadow: rgba(0, 0, 0, 0.1);
--code-bg: #f6f6f6;
--code-border: #ececec;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #121212;
--surface: #1e1e1e;
--text: #e0e0e0;
--muted: #9a9a9a;
--link: #82aaff;
--hairline: #2a2a2a;
--shadow: rgba(0, 0, 0, 0.5);
--code-bg: #171717;
--code-border: #262626;
}
}
…and then about 250 lines of selectors that consume those variables. Syntax highlighting for code blocks comes from Hugo’s built-in chroma generator (pygmentsUseClasses: true in hugo.yaml). I generated the github and github-dark token sets with hugo gen chromastyles and dropped them inline, scoped by the same prefers-color-scheme query so they switch with the rest of the page.
With the new theme in place, I bumped Hugo from 0.128.0 back up to 0.145.0—the version every other site in my fleet uses. PaperMod is gone from the repo; the directory and its 100+ files were replaced by my 12.
Final thoughts
The afternoon ended with two things that had been quietly bothering me both fixed.
The blog now runs on the same pipeline as everything else I ship: a Gitea repo, a Gitea Actions workflow, a wrangler pages deploy step. Pushing a commit to main rebuilds the site and pushes it through Cloudflare Pages within a minute. There is no part of this stack I don’t operate.
It also no longer looks like every other PaperMod blog. It looks like diego.dev writing long-form posts—because it is.
If you’re reading this on blog.diego.dev after this post went up, you’re seeing it. The blog you’re looking at right now is the one I just described.