Blogging With Hugo, GitHub and Netlify
This is a follow-up to my earlier post, Blogging With Hugo, Bitbucket and Firebase. Since writing that post, I’ve migrated my blog’s repository from Bitbucket to GitHub, and switched my hosting from Firebase to Netlify. The core of the setup - Hugo generating a static site from Markdown - remains unchanged. What’s different is everything around it: where the code lives, how it gets built, and where it ends up.
This post covers why I made the switch, and how to set up the new pipeline from scratch.
Why Move?
Three reasons, in order of importance:
-
Consolidation. Most of my other projects already live on GitHub. Having the blog repo on Bitbucket meant context-switching between two platforms for no compelling reason. One fewer set of credentials to manage, one fewer UI to remember.
-
Netlify is a better fit for static sites than Firebase Hosting. Firebase Hosting is perfectly capable, but it’s a small feature in a large platform designed primarily for app backends. Netlify is purpose-built for static sites and JAMstack deployments: the defaults are saner, the deploy previews are useful, and the free tier is generous (100 GB bandwidth/month, 300 build minutes/month). It also gives me automatic HTTPS on custom domains, rollbacks per deploy, and branch deploy previews - all without configuration.
-
Simpler CI/CD. Bitbucket Pipelines required me to write a YAML build script that downloaded Hugo, installed it, ran the build, and then pushed to Firebase via an Atlassian pipe with base64-encoded service account keys. Netlify has native Hugo support: point it at a branch, tell it to run
hugo, and it handles the rest. No Docker images, no credential plumbing, no build scripts to maintain.
None of these reasons are damning criticisms of the old setup. It worked fine for years. But the new setup works better with less friction, and the migration was straightforward enough that there was no reason not to do it.
What Changed
| Before | After | |
|---|---|---|
| Repository | Bitbucket (private) | GitHub (private) |
| CI/CD | Bitbucket Pipelines + custom YAML | Netlify (built-in Hugo support) |
| Hosting | Firebase Hosting | Netlify |
| Static Site Generator | Hugo | Hugo (unchanged) |
| Deploy trigger | Cut a release/* branch |
Push to deploy branch |
The overall shape of the workflow is the same: I work on main, and when I’m ready to publish, I deliberately push to a production branch. In the old setup that was a release/* branch; now it’s a single deploy branch. The intent is identical: publishing is a conscious, manual act, not something that happens every time I fix a typo in a draft.
Migrating the Repository from Bitbucket to GitHub
If you’re starting fresh, skip this section - just create a new GitHub repo and push your Hugo site to it. If you’re migrating an existing Bitbucket repo, here’s what I did:
-
Create a new, empty repository on GitHub. Do not initialize it with a README,
.gitignore, or license - you want it completely empty. -
In your local clone of the Bitbucket repo, add GitHub as a new remote and push:
git remote add github https://github.com/<username>/<reponame>.git
git push github main --all
git push github --tags
-
Verify that everything - branches, tags, commit history - is intact on GitHub.
-
Once you’re satisfied, update the default remote:
git remote set-url origin https://github.com/<username>/<reponame>.git
git remote remove github
- Verify that submodules still work:
git submodule sync
git submodule update --init --recursive
That’s it. Your full history is preserved, submodules point to the same upstream repos, and git push now goes to GitHub.
Setting Up the deploy Branch
Before connecting Netlify, create the branch that will serve as your production trigger:
git checkout -b deploy
git push -u origin deploy
The idea is simple: main is your working branch where drafts live and edits happen freely. When you’re ready to publish, you merge main into deploy and push. Only pushes to deploy trigger a Netlify build and deployment. This keeps publishing deliberate - you decide exactly when your site goes live, and a stray commit to main never accidentally publishes a half-finished post.
The publishing step, when you’re ready, is:
git checkout deploy
git merge main
git push
git checkout main
That’s it. Four commands, and your site is building.
Setting Up Netlify
Creating a Netlify Site
-
Sign up at netlify.com using your GitHub account. This makes the repository linking step seamless.
-
Click Add new site > Import an existing project > GitHub.
-
Select your blog repository. Netlify will ask for build settings:
- Branch to deploy:
deploy - Build command:
hugo - Publish directory:
public
- Branch to deploy:
-
Under Environment variables, add one:
HUGO_VERSION=0.150.0(or whatever version you’ve tested your site with locally). Netlify’s default Hugo version lags behind the latest release, and version mismatches can cause subtle rendering differences. Pinning the version avoids surprises.
-
Click Deploy site. Netlify will clone the repo, check out the
deploybranch, runhugo, and deploy the contents ofpublic/to its CDN. The first deploy takes a minute or two.
Netlify assigns you a random subdomain like https://wonderful-torte-a1b2c3.netlify.app. You can change this to something more readable in Site configuration > Domain management > Change site name.
A few things Netlify handles automatically that required manual effort in the old setup:
- Submodules. Netlify detects and clones git submodules during the build, so your Hugo theme (if stored as a submodule) is fetched without any explicit
git submodule init/git submodule updatesteps. - Caching. Netlify caches build dependencies between deploys, so subsequent builds are faster than the first.
- HTTPS. Netlify provisions and renews TLS certificates via Let’s Encrypt automatically. No configuration needed.
Custom Domain
If you have a custom domain:
- Go to Domain management > Add custom domain.
- Enter your domain and verify ownership.
- Update your DNS provider’s records to point to Netlify. Netlify will provide the exact records you need (typically a CNAME or ALIAS record pointing to your Netlify subdomain).
- HTTPS is provisioned automatically once DNS propagation completes.
Updating config.toml
Update the baseURL in your Hugo config to reflect your new domain:
baseURL = "https://yourdomain.com/"
The New Workflow
With all of this in place, my publishing workflow is now:
- Write or edit a post in VS Code. Preview locally with
hugo server. - Commit and push to
mainas often as I like. Nothing gets published. - When I’m ready to publish, merge
mainintodeployand push. - Netlify builds the site with Hugo and deploys it. The site is live in under a minute.
The shape of this is almost identical to the old workflow - swap “cut a release/* branch” for “merge into deploy” - but the plumbing underneath is drastically simpler. No bitbucket-pipelines.yml to maintain, no Docker images, no base64-encoded service account keys, no Firebase CLI. Netlify knows how to build a Hugo site out of the box.
Cleaning Up
After confirming that the new pipeline was working reliably, I cleaned up the old infrastructure:
- Deleted the
firebase.jsonand.firebasercfiles from the repo. - Removed the
bitbucket-pipelines.ymlfile. - Archived the Bitbucket repository (I didn’t delete it - keeping it around as a backup costs nothing).
- Removed the Firebase Hosting site and associated service account.
Final Thoughts
The old setup served me well for years, and I have no complaints about it. The new setup is simply better along every axis I care about: fewer moving parts, a more generous free tier, simpler credential management, and all of my projects under one roof. If you’re currently running a similar Bitbucket + Firebase pipeline and have been thinking about moving, I’d encourage you to do it. The migration is a single afternoon’s work, and the result is a cleaner, more maintainable setup.