This blog started with Jekyll, the static site generator historically associated with GitHub Pages. After a few months of use, I decided to migrate to Hugo. Here’s why, and how it works.

Jekyll vs Hugo: why switch?

Jekyll is a solid tool, but it carries a few constraints that become painful over time.

JekyllHugo
LanguageRubyGo (single binary)
InstallationRuby + Bundler + gemsOne single binary
Build speedSlow (seconds to minutes)Very fast (milliseconds)
DependenciesMany (gems)None
ThemesVia gems or forkLocal directory or module
Native draftsPartialNative (draft: true)
Future datesNot handled nativelyNative (buildFuture)

The point that motivated me most: Hugo is a single binary compiled in Go. No Ruby to install, no gem version conflicts, no bundle install failing depending on the environment. Download it, run it, done.

How Hugo works

Hugo is a static site generator: it takes Markdown files and templates and produces an HTML/CSS/JS site ready to be hosted anywhere.

flowchart LR
    subgraph src["Sources"]
        md["content/posts/*.md"]
        theme["themes/my-theme/"]
        assets["static/assets/"]
        cfg["hugo.toml"]
    end

    hugo(["⚙️ hugo build"])

    subgraph out["public/ - static site"]
        html["index.html"]
        posts["posts/"]
        pub_assets["assets/"]
    end

    src --> hugo
    cfg --> hugo
    hugo --> out

Each article is a Markdown file with a header called frontmatter (between the ---) that defines the article’s metadata: title, date, tags, cover image, etc.

---
title: "My article"
date: 2026-04-28T10:00:00+02:00
draft: false
tags:
  - kubernetes
  - devops
cover:
  image: assets/images/my-image.png
  alt: "Cover image description"
---

Article content in Markdown...

Installation

On Linux

Hugo provides packages for common distributions. The simplest and most up-to-date method is to download the binary directly from the GitHub releases.

# Download the Extended version (required for advanced CSS themes)
HUGO_VERSION="0.147.0"
wget -O /tmp/hugo.deb \
  https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_linux-amd64.deb

sudo dpkg -i /tmp/hugo.deb

# Verify installation
hugo version

On Debian/Ubuntu, it’s also available via apt, but often in an older version:

sudo apt install hugo

On Windows

On Windows, the recommended method is via winget or Chocolatey:

# Via winget (built-in on Windows 11)
winget install Hugo.Hugo.Extended

# Via Chocolatey
choco install hugo-extended

Or download the .zip binary from Hugo’s GitHub releases and add it to your PATH.

The extended keyword matters: the Extended version includes Sass/SCSS support, required by most modern themes.

Creating a new site

hugo new site my-blog
cd my-blog

Generated structure:

my-blog/
├── archetypes/       ← Article templates
├── content/          ← Content (articles, pages)
├── layouts/          ← HTML templates (theme overrides)
├── static/           ← Served as-is (images, favicon...)
├── themes/           ← Themes
└── hugo.toml         ← Main configuration

Writing an article

hugo new posts/2026-04-28-my-article.md

Hugo creates the file with the frontmatter pre-filled from the default archetype. Just open the file and write the content in Markdown.

Publishing as a draft

While an article is being written, simply keep draft: true in the frontmatter:

---
title: "My draft article"
date: 2026-04-28T10:00:00+02:00
draft: true
---
flowchart TD
    A["draft: true"]

    A --> B{"Command"}
    B -->|"hugo server (no -D)"| C["❌ Hidden"]
    B -->|"hugo server -D"| D["✅ Visible"]
    B -->|"hugo build (prod)"| E["❌ Hidden"]

To publish the article, just switch draft: false or remove the draft line.

Publishing in the future

Hugo also allows scheduling article publication via the frontmatter date. If the date is in the future, the article is hidden by default during the build.

---
title: "Scheduled article"
date: 2026-12-01T09:00:00+02:00
draft: false
---
flowchart TD
    A["date: 2026-12-01 (future date)"]

    A --> B{"Command"}
    B -->|"hugo server"| C["❌ Hidden"]
    B -->|"hugo server --buildFuture"| D["✅ Visible"]
    B -->|"hugo build (prod)"| E["❌ Hidden"]

Combined with a GitHub Action that triggers daily, this enables automatic publication at the desired date without any manual intervention.

Running the development server

# Standard mode (published articles only)
hugo server

# Include drafts
hugo server -D

# Include drafts and future-dated articles
hugo server -D --buildFuture

The server starts at http://localhost:1313 and reloads automatically on every file change.

Build and deployment

To generate the final static site:

hugo --gc --minify

The result is in the public/ folder, ready to be deployed on any static host (GitHub Pages, Netlify, Vercel, a simple nginx server…).

Automated deployment with GitHub Actions

GitHub Actions is a continuous integration and deployment (CI/CD) system built into GitHub. It allows automating tasks on every git push: in our case, building the site with Hugo and publishing it to GitHub Pages, with no manual intervention.

flowchart LR
    A["📝 git push\narticle.md"] --> B["GitHub\nbranch main"]
    B --> C["⚡ GitHub Actions\nWorkflow"]

    C --> D["1. Install\nHugo Extended"]
    D --> E["2. hugo build\n--minify / no -D"]
    E --> F["3. Upload\nartifact"]
    F --> G["4. Deploy\nPages"]
    G --> H["🌐 Public site"]

The workflow file lives in .github/workflows/hugo.yml. Here is its commented structure:

name: Deploy Hugo site to GitHub Pages

# Trigger: on every push to main branch
on:
  push:
    branches:
      - main
  workflow_dispatch: # also allows manual trigger from GitHub

# Permissions required to write to GitHub Pages
permissions:
  contents: read
  pages: write
  id-token: write

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      # 1. Install Hugo Extended on the runner
      - name: Install Hugo CLI
        run: |
          wget -O /tmp/hugo.deb \
            https://github.com/gohugoio/hugo/releases/download/v0.147.0/hugo_extended_0.147.0_linux-amd64.deb \
          && sudo dpkg -i /tmp/hugo.deb

      # 2. Checkout source code
      - name: Checkout
        uses: actions/checkout@v4

      # 3. Build the site (no drafts, no future dates)
      - name: Build with Hugo
        env:
          HUGO_ENVIRONMENT: production
        run: hugo --gc --minify --baseURL "https://mysite.github.io/"

      # 4. Upload artifact for the deploy job
      - name: Upload artifact
        uses: actions/upload-pages-artifact@v3
        with:
          path: ./public

  deploy:
    needs: build # waits for build job to complete
    runs-on: ubuntu-latest
    steps:
      # 5. Publish to GitHub Pages
      - name: Deploy to GitHub Pages
        uses: actions/deploy-pages@v4

The key point is step 3: the hugo command is run without -D or --buildFuture. Hugo automatically excludes all articles with draft: true or a future date.

Articlehugo server -DGitHub Actions (prod)
draft: false, past datevisiblepublished
draft: true, past datevisiblehidden
draft: false, future datehiddenhidden
draft: true, future datehiddenhidden

hugo server -D --buildFuture shows all rows of the table.

Articles with draft: true or a future date are never included in the production build, guaranteeing that no unwanted content ends up published by mistake.

Conclusion

Migrating from Jekyll to Hugo was, for me, an obvious choice. The simplicity of installation, build speed, and native handling of drafts and future dates make it a much more pleasant tool to use day-to-day. If you host your blog on GitHub Pages and are still using Jekyll, the migration is definitely worth it.

Sources