TL;DR: In four hours, I moved my Hugo blog from GitHub Pages to Cloudflare Pages. Along the way I compromised a token, outwaited a timeout, patched a vulnerability, and scrubbed a dozen deprecated patterns from the theme. This is not a tutorial. It is a craftsman’s journal about what it means to move a digital workshop: honor what you leave behind, protect what you carry, build carefully where you arrive.

I. The Departure (Leaving the Public Square)

It began with a constraint.

GitHub Free requires public repositories. For two years, my blog’s source code — every Hugo configuration, every SCSS variable, every custom shortcode — was visible to anyone who cared to look. I had accepted this as the price of free hosting.

But the blog had grown. What started as a personal journal had become a platform for Rooted Nomadism, a resource for people relocating to Spain, a body of work I had begun to value as intellectual property.

The code was no longer just code. It was the scaffolding of a philosophy. And scaffolding should not be left exposed to the weather.

So I decided to move. Not to abandon GitHub, but to separate the public face — the published blog at salahnomad.com — from the private workshop that builds it.

The destination: Cloudflare Pages. Private repository. Global CDN. Zero cost. The journey: four hours, one compromised token, and more lessons than I expected.

A wooden workbench in a Málaga workshop, half-packed: tools neatly arranged on one side, empty space on the other. A laptop sits open in the middle, displaying a GitHub repository page. Warm afternoon light casts long shadows.
The Departure — when you realize the workshop must move, but the work remains.

II. The Compromise (A Token in the Open)

The first mistake happened in the first ten minutes.

I was copying commands from my terminal into a conversation with an AI assistant. The guide asked for a GitHub personal access token to authenticate the push.

I copied the token. I pasted it. And in that moment, I understood what I had done.

The token — forty characters beginning with ghp_ — now sat in plain text inside a persistent chat log. Not in a terminal that would scroll away. Not in a file I would delete. In a history that would remain.

My stomach dropped. I had spent fifteen years teaching others to avoid the “New Here Tax” — the €2,400 and months of stress my own carelessness cost me when I first reached Málaga. And here I was, taxing myself.

The fix was immediate: revoke the token, generate a new one with minimal permissions, store it in a password manager.

But the lesson ran deeper than the fix.

Speed is the enemy of security. I had been rushing, following a guide without reading ahead, treating the migration as a task to complete rather than a process to honor. The hammam teaches that you cannot rush the steam. I had tried to rush the departure, and the departure had caught me.

Close-up of a hand holding a fountain pen over a Moleskine notebook, the pen hovering above a line that reads 'ghp_...' but not yet writing it. A blurred laptop glows in the background, suggesting a moment of hesitation before a decision.
The Compromise — the moment before the mistake, when you still have a choice.

III. The Shortcut (Server-to-Server)

The second challenge was not a mistake. It was a discovery.

I tried to push the repository from my laptop to the new private repo. My upload speed was 383 KB/s. The repository was 117 MB. The push died after two minutes with an HTTP 408.

I tried again. Same result. I increased the Git buffer. Same result. I considered SSH keys — ten more minutes of setup for a problem that might not be solved.

Then the assistant suggested what I had not considered: GitHub Importer. A built-in tool that clones repositories server-to-server. No local bandwidth required.

I deleted the empty destination repo, opened github.com/new/import, and filled the form: source URL, owner, name, privacy set to private. I clicked Begin import.

Two minutes later it was done. All 3,899 commits. All 860 files. Every branch and tag, copied from GitHub’s servers to GitHub’s servers, my slow upload bypassed entirely.

The lesson was not about the tool. It was about the mindset.

Sometimes the shortest path is not the most direct one. The guide assumed a fast connection. My reality was different. When I stopped following the guide and started reading the landscape, the solution appeared.

A split view: on the left, a terminal window showing a failed git push with an 'HTTP 408' error; on the right, a browser window showing a green 'Import complete' message. Both screens share the same desk, suggesting a transition from frustration to solution.
The Shortcut — when the direct path fails, the indirect path appears.

IV. The Confusion (Workers vs Pages)

The third hurdle was a confusion of categories.

Cloudflare’s dashboard had changed. Workers and Pages now sat under a single menu. I clicked Create, selected my repository, and began the setup — until I noticed the build command: npx wrangler deploy.

That is not a Hugo command. That is a Workers command. I had been configuring a serverless function, not a static site.

At the bottom of the screen, barely visible, a link waited: “Looking to deploy Pages? Get started.”

I clicked it. The interface changed. Framework preset: Hugo. Build command: hugo --gc --minify. Output directory: public. I added the environment variables — HUGO_VERSION, HUGO_ENV, NODE_VERSION — and clicked Save and Deploy. The build succeeded. The site went live.

But the confusion had cost me twenty minutes, and it taught me this: interfaces change, documentation lags, and your judgment must be sharper than both. The dashboard was designed for the most common case, not my case. The clue was there. I simply had to be paying attention.


V. The Vulnerability (A Patch in Time)

The fourth discovery was the most important.

While we configured the build, the assistant mentioned that Hugo 0.159.2 — my version, installed in April — carried a known cross-site scripting flaw, patched in July by version 0.164.0.

I was three months behind on security.

The fix was straightforward: update Hugo locally, raise the environment variable, redeploy. But the discovery raised a heavier question: how many other patches was I missing? Node.js nearing end-of-life. Outdated dependencies. A certificate expiring in ninety days.

I had been so focused on building that I had neglected maintenance.

This is the paradox of the digital artisan. We create beautiful things, but we must also keep them sound. The hammam teaches that the scrubbing is not optional; you cannot skip the gommage and expect clean skin. You cannot skip maintenance and expect secure software.

I updated everything, and wrote one line in my notebook: security is not a feature. It is a practice.


VI. The Warnings (Listening to the Code)

The fifth lesson came from the build logs.

The new build succeeded — but not silently. The logs carried warnings: languageCode deprecated since Hugo 0.158.0. .Language.LanguageDirection deprecated. .Site.Data superseded.

Warnings are not errors. The site worked. But warnings are the code’s way of speaking to you: this will break in a future version; fix it now, while you have time.

I grepped the config and the theme and found the patterns in eleven places. I replaced them: locale, .Language.Direction, hugo.Data, .Site.Language.Locale. I tested locally. The warnings vanished. I pushed, and the Cloudflare build logs came back clean.

The lesson was not about the specific deprecations. It was about the habit.

Most developers ignore warnings because the build succeeded. But warnings are cracks in a foundation — small now, catastrophic later. The zellige artisan does not leave a tile that does not fit. He recuts it. That is the standard.

A terminal window showing Hugo build output with deprecation warnings highlighted in yellow. Beside it, a Moleskine notebook with a checklist of corrections, each item checked off, a fountain pen resting on the page.
The Warnings — the code’s way of asking for help.

VII. The Archive (The Lamp Behind)

The sixth decision was philosophical.

What to do with the old repository, now that the code had moved to a private one? Delete it? Hide it? Or keep it public as an archive?

I chose the third.

3,899 commits. Two years of mistakes and corrections. That history deserved to exist, even if it was no longer the current version. So I wrote a README — not a generic “this repo is archived” notice, but an explanation of why the workshop remains open, and where the work now lives. It begins with a line I love: “One does not leave a house. One leaves a lamp burning inside.”

I disabled GitHub Pages. The old address now returns a 404, and the README points travelers to salahnomad.com.

You can move without erasing. You can grow without denying your past. This is the essence of Rooted Nomadism: carry your heritage not as baggage, but as a compass.

A diptych: on the left, a public GitHub repository page whose README opens with a handwritten-style quote; on the right, a private repository marked with a small lock icon. Both screens share the same desk, suggesting continuity between old and new.
The Archive — the lamp you leave burning when you move to a new house.

VIII. The Lesson (The Caravan Moves On)

Four hours, from first command to final deployment. But the lessons will outlast the infrastructure.

Security is a practice, not a feature. The shortest path is not always the most direct one. Interfaces lie; judgment must be sharper. Warnings are requests, not noise. And archives are not failures — they are lamps.

The assistant executed the migration. But I had to recognize the token as a mistake, the timeout as a signal, the warning as a request. This is the difference between a servant and a sage. The machine is fast, tireless, efficient. The human is the one who pays attention.

The migration succeeded not because the AI was smart, but because I was present.

The caravan has moved on. The new workshop is private, protected, faster than the old one. And somewhere behind us, in a public repository that no longer deploys, a README begins with a line from a poem.

The lamp still burns.

May you find your soil sooner than I did.

Salah Nomad Rooted in Pedregalejo since 2021

Salah, a man in his mid-40s wearing a classic zebra-striped shirt, sits on a marble bench in a sun-drenched Andalusian courtyard, holding a glass of mint tea. A closed Moleskine notebook rests beside him; a closed laptop sits in the background. Golden Mediterranean light envelops the scene.
The Lesson — when the work is done, and the caravan moves on.

🗺️ Continue Your Rooted Nomad Journey

This article is one thread in a larger tapestry:


🛠️ Technical Appendix (For the Curious)

If you’re considering a similar migration, here are the key decisions and their rationale:

DecisionRationale
GitHub Importer over local pushBypassed a 383 KB/s upload for a 117 MB repository
Cloudflare Pages over alternativesExisting Cloudflare DNS; unlimited bandwidth on the free tier
Hugo 0.164.0 over 0.159.2Patched a known cross-site scripting vulnerability
Private repo + public archiveProtected the work in progress; honored the journey
Branch protection on mainNo direct pushes to production; every change via pull request
_redirects fileRedirected .pages.dev to the custom domain; no duplicate content

These are not universal recommendations. They are the decisions that made sense for my context, my values, and my infrastructure. Your mileage may vary.


❓ Frequently Asked Questions

Is this a technical tutorial?

No. It is a craftsman’s journal about a real migration — the mistakes, the corrections, and the philosophy behind the decisions. For step-by-step commands, read the official Cloudflare Pages documentation.

Why did you migrate from GitHub Pages to Cloudflare Pages?

Performance, security, and scalability. But the deeper reason was philosophical: I wanted infrastructure that could grow with the project without compromise — a private workshop behind a public face.

What was the biggest mistake you made?

I pasted a GitHub personal access token into a chat log, in plain text. It was revoked within minutes, but the lesson remains: speed is the enemy of security.

Did you lose any history?

No. GitHub Importer copied all 3,899 commits server-to-server. The old repository remains public as an archive.

How long did the migration take?

Four hours of active work in a single evening. The execution was fast; the thinking was slow.