Migrating This Site From GitHub Pages to Vercel

Migrating This Site From GitHub Pages to Vercel
August 14, 2026 · Woojae

This site spent years on GitHub Pages, and honestly, it was fine. A GitHub Actions workflow built the Hugo site on every push to main, a CNAME file pointed woojae.com at it, and I never thought about hosting. That’s the highest compliment you can pay a static host.

But “fine” had a ceiling. GitHub Pages serves static files and that’s it. No custom HTTP headers. No serverless functions. No preview deployments for pull requests. Every time I wanted the site to do something instead of just be something, I hit the same wall.

So last week I moved everything to Vercel. The whole migration took an afternoon, and it unlocked a couple of things within a day that Pages could never have done.

The Actual Migration

The move itself was four small steps:

  1. Import the repo into Vercel. Connect the GitHub repo through Vercel’s dashboard, and it deploys on every push. This replaced my entire GitHub Actions workflow — 76 lines of YAML that installed a pinned Hugo version, built the site, and shuffled artifacts between a build job and a deploy job. All deleted.

  2. Add a vercel.json. More on why below.

  3. Point the domain at Vercel. Domains are managed in Vercel’s dashboard instead of a CNAME file in the repo, so the CNAME file got deleted too.

  4. Fix baseURL in hugo.toml. It was still pointing at woojae.github.io, which meant canonical URLs, the sitemap, and RSS links were all advertising the old address. One-line fix.

The One Gotcha

The first deploy served a completely empty site.

When I imported the project, Vercel didn’t detect Hugo as the framework, so git-triggered deploys ran no build at all and happily served… nothing. The fix was pinning the build configuration in the repo instead of relying on dashboard auto-detection:

{
  "framework": "hugo",
  "buildCommand": "hugo --gc --minify",
  "outputDirectory": "public"
}

I like this failure mode as a lesson: put build config in the repo, not in a dashboard. The dashboard forgets. Git doesn’t.

What the Move Unlocked

Here’s the part that made the migration worth it. Within a day of moving, I shipped two things that were flat-out impossible on GitHub Pages.

Real security headers. On Pages, the best you can do for security headers is <meta http-equiv> tags, which are a second-class citizen — some headers (like X-Frame-Options and frame-ancestors) are simply ignored in meta form. On Vercel, vercel.json sets real HTTP headers on every response. The site now sends a proper Content-Security-Policy, and instead of copping out with 'unsafe-inline', the CSP allowlists the two inline scripts (Google Analytics and Mermaid init) by their SHA-256 hashes. The trade-off: if I so much as change whitespace in either script, the hashes have to be recomputed. Worth it.

A backend. This site has a Tetris game, and Tetris deserves a high score board. That needs an API, and a static host has nowhere to put one. On Vercel, a single file in api/ becomes a serverless function: GET /api/scoreboard returns the top ten, POST submits a score, and the data lives in a Redis sorted set on Upstash. A static blog quietly grew a backend, and the hosting bill is still zero.

About Lock-In

I wrote a whole post about AI-native infrastructure and the risk that platforms like Vercel become Heroku 2.0 — a great on-ramp you eventually pay to escape. So it would be a little embarrassing to migrate onto Vercel and immediately lock myself in.

The escape hatches are deliberate. All the scoreboard logic lives in a platform-agnostic, dependency-free module that talks to Upstash over plain fetch; the Vercel function is a thin adapter around it, and the repo documents how to redeploy the same core on Cloudflare Workers or a plain Express server. The database is reachable from anywhere, so it moves with me automatically. And Hugo doesn’t know or care who serves its output — leaving Vercel would mean rewriting one adapter file and repointing DNS.

Verdict

GitHub Pages is still a great answer for a site that’s purely static and wants to stay that way. But the migration cost me an afternoon, most of which was spent deleting things: the Actions workflow, the CNAME file, a pile of meta tags. In exchange I got faster deploys, preview URLs on every pull request, real HTTP headers, and serverless functions.

The site looks exactly the same as it did last week. It just stopped being only a pile of files.