How to Set Up a Blog for Free Powered by Hugo, Firebase, and GitHub

How to set up a fast, free, and low-maintenance blog powered by Hugo, Firebase Hosting, and GitHub Actions—based on the exact steps I took to build this site.

What is Hugo?

Hugo is one of the fastest open-source static site generators available. Instead of querying a database and rendering pages on the fly on every request like traditional CMS platforms, Hugo takes your Markdown files, applies HTML templates, and compiles your entire site into static HTML, CSS, and JavaScript in milliseconds.

Even better: Hugo is written in Go (Golang), which is currently my favourite programming language! Because it’s built in Go, Hugo ships as a single, dependency-free binary with blazing-fast build times and rock-solid tooling. If you’re a fellow Go fan—or curious to see what a large, high-performance real-world Go codebase looks like—I highly recommend checking out the Hugo source code on GitHub.


1. Initializing the Site & Adding a Theme

Once you have Hugo installed, scaffolding a new site and initializing Git takes just a few commands:

hugo new site blog
cd blog
git init

Rather than copying theme files directly into the project, adding your theme as a Git submodule makes it easy to pull upstream updates later without cluttering your repository history. For this blog, I used the Rusty Typewriter theme:

git submodule add https://github.com/math-queiroz/rusty-typewriter themes/rusty-typewriter

Your site’s global settings live in hugo.toml. Beyond setting your baseURL, title, and author details, this is where you configure menus, taxonomies, and theme parameters.

Rusty Typewriter also includes built-in client-side search powered by FlexSearch. To enable it, you configure Hugo to output a searchindex.json file at build time alongside the standard HTML and RSS outputs.

Here is the complete hugo.toml configuration:

baseURL = 'https://www.tangent-labs.co.za/'
locale = 'en-US'
title = 'Off on a tangent'
theme = 'rusty-typewriter'

[rusty_typewriter]
 [params.rtwt]
  sidePane = true
  countPageItems = 7
  searchPath = 'search'
 [params.rtwt.home]
  showBio = true
  showAuthorImg = false
  sidePaneTags = true
  showFeatured = true
  showRecent = true
  hideRecentWhenFeatured = true
  countRecent = 5
 [params.rtwt.side.home]
  sidePaneSticky = false
  taxonomies = ['tags']
  countTaxonomy = 6
 [params.rtwt.side.list]
  sidePaneSticky = false
 [params.rtwt.side.single]
  sidePaneSticky = true
  showDetails = true
  showTableOfContents = true
  showAttachments = true
  showRelated = true
  countRelated = 5

[related]
 includeNewer = true
 [[related.indices]]
  name = 'tags'
  weight = 100

[taxonomies]
 tag = 'tags'

[params]
 summaryLength = 1
 dateFormat = '2 Jan 2006'
 searchLimit = 10

[params.author]
 name = 'Keanu Dirkse'

[params.links]
 hugo = 'https://gohugo.io/'
 theme = 'https://github.com/math-queiroz/rusty-typewriter'

[[menus.main]]
 name = 'Posts'
 pageRef = '/posts'
 weight = 1

[[menus.main]]
 name = 'About'
 pageRef = '/about'
 weight = 2

[outputFormats]
 [outputFormats.SearchIndex]
  mediaType = 'application/json'
  baseName = 'searchindex'
  isPlainText = true
  notAlternative = true

[mediaTypes]
 [mediaTypes.'application/json']
  suffixes = ['json']

[outputs]
 home = ['HTML','RSS','SearchIndex']

Tip on Go Date Formatting: Notice dateFormat = '2 Jan 2006'. Because Hugo uses Go’s time package under the hood, layouts are formatted using Go’s reference time (Mon Jan 2 15:04:05 MST 2006) rather than YYYY-MM-DD tokens.

Home Page Data & Static Pages

For the Rusty Typewriter home page bio, create data/rtwt/content.yaml:

home:
  greetings: 'Off on a tangent'
  text: 'Notes and experiments from Tangent Labs.'

And create an About page at content/about.md (make sure to include a date in the front matter so the search index doesn’t fall back to 1 Jan 0001):

+++
title = 'About'
date = 2026-11-11T18:00:00+02:00
type = 'blank'
+++

# About

Hi, I'm Keanu Dirkse. This is where I write about things I'm building and learning.

3. Setting Up .gitignore

Before committing or setting up hosting, make sure build artifacts, caches, and any local credentials or service account keys are ignored in .gitignore:

public/
resources/
.hugo_build.lock

# Firebase
.firebase/
*-debug.log
firebase-debug.log*
.runtimeconfig.json

# Secrets & credentials
.env
.env.*
!.env.example
*.pem
*.key
*.p12
*.pfx
*service-account*.json
*serviceAccount*.json
*-firebase-adminsdk-*.json
google-services.json
GoogleService-Info.plist
.netrc

# Dependencies / logs
node_modules/
npm-debug.log*

# OS / editor
.DS_Store
.vscode/
.idea/
*.swp

4. Firebase Hosting Configuration

To host the static site on Firebase Hosting, run firebase init hosting:

  • Public directory: public (where Hugo outputs compiled files)
  • Configure as a single-page app (rewrite all urls to /index.html)? No. Hugo generates a separate HTML file for every route. Enabling SPA rewrites would route missing URLs to the home page instead of your 404.html.
  • Overwrite public/404.html or index.html? No.

This generates two configuration files in your repository:

firebase.json:

{
  "hosting": {
    "public": "public",
    "ignore": [
      "firebase.json",
      "**/.*",
      "**/node_modules/**"
    ]
  }
}

.firebaserc:

{
  "projects": {
    "default": "<YOUR_FIREBASE_PROJECT_ID>"
  }
}

5. Automating Deploys with GitHub Actions

Instead of building and deploying manually from your machine, you can use GitHub Actions to build and deploy automatically whenever changes are pushed to main.

Setting Up the Secret

  1. In the Firebase Console, go to Project settings → Service accounts and generate a new private key JSON (or run firebase init hosting:github). Never commit this JSON file to Git.
  2. In your GitHub repository, go to Settings → Environments, create an environment (e.g. main-blog), and add an environment secret named FIREBASE_SERVICE_ACCOUNT containing the service account JSON.

The Workflow File

Create .github/workflows/deploy.yml:

name: Deploy to Firebase Hosting

on:
  push:
    branches: [main]
  workflow_dispatch:

concurrency:
  group: deploy
  cancel-in-progress: true

jobs:
  deploy:
    # Only ever deploy from main (also guards manual runs)
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    environment: main-blog
    permissions:
      contents: read
      checks: write
    steps:
      - uses: actions/checkout@v4
        with:
          submodules: recursive

      - uses: peaceiris/actions-hugo@v3
        with:
          hugo-version: '0.169.0'
          extended: true

      - name: Build
        run: hugo --gc --minify

      - uses: FirebaseExtended/action-hosting-deploy@v0
        with:
          repoToken: ${{ secrets.GITHUB_TOKEN }}
          firebaseServiceAccount: ${{ secrets.FIREBASE_SERVICE_ACCOUNT }}
          projectId: <YOUR_FIREBASE_PROJECT_ID>
          channelId: live

Notice submodules: recursive on the checkout step—without it, GitHub Actions won’t pull the theme submodule and the build will render an empty site.


6. Managing the Blog Going Forward

With the CI/CD pipeline in place, day-to-day blogging is just writing Markdown and pushing to Git.

Writing a New Post

Create a new post using the default archetype:

hugo new posts/my-post-title.md

This creates content/posts/my-post-title.md with TOML front matter:

+++
title = 'My Post Title'
date = 2026-11-13T11:00:00+02:00
draft = true
tags = ['golang', 'notes']
+++

Previewing Locally

Run Hugo’s built-in development server with live reload:

hugo server -D
  • Open http://localhost:1313 in your browser.
  • -D (--buildDrafts) includes posts with draft = true.
  • -F (--buildFuture) includes posts whose date timestamp is still in the future.

Publishing

When your post is ready to go live:

  1. Change draft = true to draft = false in the post’s front matter (and make sure the date is not set in the future).
  2. Commit and push to main:
    git add content/posts/my-post-title.md
    git commit -m "Publish post: My Post Title"
    git push
    
    GitHub Actions will automatically build the site with hugo --gc --minify and deploy it to Firebase Hosting.

Cloning on a New Machine & Updating the Theme

  • Cloning onto a new machine: Because the theme is a submodule, clone with --recurse-submodules, or run:
    git submodule update --init --recursive
    
  • Updating the theme: To pull the latest changes from the theme author:
    git submodule update --remote themes/rusty-typewriter
    git add themes/rusty-typewriter
    git commit -m "Update rusty-typewriter theme"
    

References & Further Reading