Introduction

I have been using WordPress for a long time to host slashdevops.com blog site. I had been looking for a way to migrate this blog from WordPress to a static site generator and during the research I found Hugo and I can say I am very happy with it. Hugo allows me to write this blog posts in Markdown and it is very fast and easy to use.

I have also been using GitHub Pages for other of my personal jobs and I decided to migrate this blog GitHub Pages as well.

To perform this migration I used the following software:

My Experience

It took me some time to migrate this blog from WordPress to Hugo and GitHub Pages. I had to learn how to use Hugo, GitHub Pages and how to configure my slashdevops.com domain in Google Domains but it was worth it.

Now, the blog implement Disqus for comments and likes and it is very fast and responsive. Also, the blog implement fuzzy search using Fuse.js. I decided to lost the comments and likes from the WordPress posts, I’m very sorry about that.

πŸ‘‰ The most hard part was understand how should work the GitHub Pages and Hugo together.

There are several tutorials on the internet that can help you with this migration, but not even the official documentation of Hugo and GitHub Pages helped me to understand the fact that one of the best way is implementing two repositories to have this done - one repository to host the source code of the blog (the hugo repository, where you create the Markdown files) and another repository to host the compiled code of the blog (static files generated from hugo and in the public folder of the first repo).

Just to clarify, the source code repository is the one you use to write your blog posts and the compiled code repository is the one you use to host the blog.

Yes, truth, there are several ways to have this done, there are many blog posts and tutorials on the internet that can help you with this configuration of Github Pages and Hugo, the majority of these use two repositories approach with git submodules or override the docs folder of the same repository. Also, you can use a different branch in the same repository to store the static files generated from hugo, the last one is use the same repository to store the source code and the static files and deploy the static files to the Github Pages using a Workflow.

I decided to use the two repositories approach without git submodules implementation because it is easier to maintain, understand and has me the ability to keep the hugo repository (source code) private.

The Good πŸ‘

  • Hugo is very fast and “easy to use” (ones you understand its philosophy).
  • Hugo allows me to write this blog posts in Markdown.
  • Hugo has a lot of themes to choose from.
  • GitHub Pages could be free and “easy to use”.
  • The blog is very fast and responsive.
  • The blog implement fuzzy search using Fuse.js thanks to the Hugo Theme PaperMod implementation/integration.
  • The blog is very easy to maintain and update.

The Bad πŸ‘Ž

  • Migrating from WordPress to Hugo was not easy. I made it manually and it took me some time, fortunately I didn’t have many posts.
  • At the beginning, I had some issues with the Hugo theme I chose (PaperMod), but I was able to fix them after understanding how Hugo works.
  • I lost the comments and likes from the WordPress posts, I’m very sorry about that.
  • I take some time to have the right configuration of the GitHub Pages to use a public repository to host the blog (static files) and a private repository to host the source code of the blog (hugo files) and the magic to do that was the Github Actions Workflow I made.

Not so Bad / Not so Good 🫀

  • I had to learn how to use Hugo and GitHub Pages, it takes some time, but it was worth it.
  • To have GitHub Pages for free, I had to use a public repository -> slashdevops.github.io, but I am happy with it. This means you are seeing the code of this blog and how it is built (static files). Fortunately, I can have a private repository to host the source code of the blog (hugo implementation).

Relevant steps

This is not a detailed guide step by step, because I made this migration in a series of back and forth steps until this worked, but I want to share with you the most relevant steps I took to have this blog working properly.

The steps I’m sharing here are not necessary in the order I did them, and some of them are interdependent, but I will try to explain them in the order I think is more relevant.

NOTES:

  • You should have installed and configured GitHub CLI to perform some of these steps.
  • You should have installed and configured Hugo to perform some of these steps.

1. Repository Configuration

As I explained before, to have this blog working properly I needed two two repositories.

  1. slashdevops.github.io: The repository to host the compiled code of the blog (static files coming from Hugo folder public).
  2. hugo-slashdevops.github.io: The repository to host the source code of the blog.

πŸ‘‰ I create these using the GitHub CLI, but you can create them using the GitHub web interface as well.

1
2
3
4
5
# Create the repository to host the compiled code of the blog
gh repo create slashdevops/slashdevops.github.io --public --description "slashdevops.com blog site" --clone --gitignore "html"

# Create the repository to host the source code of the blog
gh repo create slashdevops/hugo-slashdevops.github.io --private --description "slashdevops.com blog site source code" --clone

So, for my migration I used the following repositories:

1.1 slashdevops.github.io repository

Repository slashdevops/slashdevops.github.io is public and it is used to host the compiled code of the blog (static files coming from Hugo folder public).

This should be filled by a GitHub Actions -> Workflow that will be in the repository hugo-slashdevops.github.io and used to build the blog and push the static files generated in the public folder to this repository.

This repository should not be empty to enable the GitHub Pages feature. So, I created a index.html file with a minimal content to avoid the repository being empty and to test the Github Pages configuration.

πŸ‘‰ The file I added to the repository slashdevops/slashdevops.github.io in the beginning was:

1
2
3
4
5
6
7
8
9
<!DOCTYPE html>
<html>
<head>
  <title>slashdevops.com blog page</title>
</html>
<body>
  <h1>Welcome to the home page</h1>
</body>
</html>

πŸ‘‰ And the configuration of this repository in the GitHub is:

Repository Configuration

πŸ‘‰ And after push the minimal html file to the repository, and configure your domain DNS as it is explained in the step 2 below, you can see the blog site working properly.

Repository test

1.2 hugo-slashdevops.github.io repository

Repository slashdevops/hugo-slashdevops.github.io is private and it is used to host the source code of the blog. Basically, this repository contains the Hugo configuration, the Markdown files of the blog posts and the Github Action -> Workflow to build and deploy the static files into the repository slashdevops.github.io.

I used the .github/workflows/hugo.yaml configuration file recommended in Hugo - Host on GitHub Pagesand then I modified it to fit my needs, which is to have two repositories, one for the source code and another for the static files. –. This modification allows me to build the blog and deploy the static files to the repository slashdevops.github.io using git commands.

IMPORTANT: I needed to configure the Github Actions -> Workflow to use the GITHUB_TOKEN with the right permissions to allow the deployment of the static files to the repository slashdevops.github.io. I named it ACTIONS_GITHUB_TOKEN (see the line 81 of the code below).

πŸ‘‰ The content of the secret ACTIONS_GITHUB_TOKEN was created in the repository slashdevops/hugo-slashdevops.github.io in the Settings -> Secrets section of the repository and I used the GitHub CLI to create it.

1
2
3
4
5
# Create the token
gh auth token

# Set the token in the repository slashdevops/hugo-slashdevops.github.io
gh secret set ACTIONS_GITHUB_TOKEN -r slashdevops/hugo-slashdevops.github.io -b <token generated before>

πŸ‘‰ The code of the Github Action -> Workflow I used to build and deploy the blog is:

Source: slashdevops/hugo-slashdevops.github.io -> .github/workflows/hugo.yml private repository.

NOTES:

  • This is a private repository, so you can’t see the code, but I will show you the code here.
  • Highlighted lines are the most important lines in the code.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
# Sample workflow for building and deploying a Hugo site to GitHub Pages
name: Deploy Hugo site to Pages

on:
  # Runs on pushes targeting the default branch
  push:
    branches: ["main"]

  # Allows you to run this workflow manually from the Actions tab
  workflow_dispatch:

# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
  contents: read
  id-token: write

# Default to bash
defaults:
  run:
    shell: bash

jobs:
  # Build job
  build:
    runs-on: ubuntu-latest
    env:
      SITE_URL: "https://slashdevops.com"
      HUGO_VERSION: 0.124.1
    steps:
      - name: Install Hugo CLI
        run: |
          wget -O ${{ runner.temp }}/hugo.deb https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_linux-amd64.deb \
          && sudo dpkg -i ${{ runner.temp }}/hugo.deb          

      - name: Install Dart Sass
        run: sudo snap install dart-sass

      - name: Checkout
        uses: actions/checkout@v4
        with:
          submodules: recursive
          fetch-depth: 1


      - name: Build with Hugo
        env:
          # For maximum backward compatibility with Hugo modules
          HUGO_ENVIRONMENT: production
          HUGO_ENV: production
        run: |
          hugo \
            --gc \
            --minify \
            --baseURL "${{ env.SITE_URL }}/"          

      - name: Show files
        run: tree -I '.git' -a

      - name: Upload artifact
        uses: actions/upload-artifact@v4
        with:
          name: static-files
          path: ./public
          retention-days: 1
          overwrite: true

  # Deployment job
  deploy:
    env:
      STATIC_SITE_REPO: "slashdevops/slashdevops.github.io"
      STATIC_SITE_REPO_BRANCH: main

    runs-on: ubuntu-latest
    needs: build
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          repository: ${{ env.STATIC_SITE_REPO }}
          ref: ${{ env.STATIC_SITE_REPO_BRANCH }}
          token:  ${{ secrets.ACTIONS_GITHUB_TOKEN }}

      - name: Download artifact
        uses: actions/download-artifact@v4
        with:
          name: static-files
          path: .

      - name: Show files
        run: tree -I '.git' -a

      - name: Commit changes
        run: |
          git config --local user.name "GitHub Actions from hugo-slashdevops.github.io repo"
          git config --local user.email "66886265+christian-slashdevops@users.noreply.github.com"
          git add .
          git commit -m "Deploy site"
          git push --force origin ${{ env.STATIC_SITE_REPO_BRANCH }}          

2. Google Domains Configuration

I made the validation of the domain slashdevops.com on my GitHub Settings account and then I configured the domain in Google Domains to point to the GitHub Pages servers (list of IPs in Configuring an apex domain).

NOTES:

  • This step is not necessary if you are using the GitHub Pages domain, but it is recommended for security reasons.

πŸ‘‰ This image shows how I configure the verified domains in GitHub Settings:

domain validation

πŸ‘‰ After the Validation of the domain, I added the domain to the GitHub Pages configuration in the repository slashdevops.github.io settings.

domain validation

Then, I configured the domain in Google Domains to point to the GitHub Pages servers. This configuration is done in the DNS section of the domain configuration in Google Domains.

πŸ‘‰ This image shows how I configured the domain in Google Domains:

domain validation

References:

3. Hugo Configuration

Here there are some relevant configurations I made, let me enumerate them:

  1. The hugo.toml configuration file.
  2. The hugo static files content. You will need to put at least the CNAME file in the static folder to configure the domain in GitHub Pages.
  3. Enabling messages and likes with Disqus.

πŸ‘‰ 1. My hugo configuration is very simple, I used the Hugo Theme -> PaperMod Wiki file as template and I made some changes to fit my needs.

My hugo.toml configuration files is:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
baseURL      = "https://slashdevops.com/"
languageCode = "en-us"
title        = "SlashDevOps"
theme        = "PaperMod"

# Read: https://github.com/adityatelange/hugo-PaperMod/wiki/FAQs#using-hugos-syntax-highlighter-chroma
# https://bwaycer.github.io/hugo_tutorial.hugo/extras/highlighting/
pygmentsUseClasses = true

###############################################################################
# metadata
enableRobotsTXT = true
buildDrafts     = false
buildFuture     = false
buildExpired    = false

[minify]
disableXML   = true
minifyOutput = true

###############################################################################
# menu
[[menu.main]]
identifier = "categories"
name       = "Categories"
url        = "/categories/"
weight     = 10

[[menu.main]]
identifier = "GitHub"
name       = "GitHub"
url        = "https://github.com/slashdevops"
weight     = 10

[[menu.main]]
identifier = "tags"
name       = "Tags"
url        = "/tags/"
weight     = 20

[[menu.main]]
identifier = "blog"
name       = "Blog"
url        = "/post/"
weight     = 20

[[menu.main]]
identifier = "search"
name       = "Search"
url        = "/search"
weight     = 20

[[menu.main]]
identifier = "archives"
name       = "Archives"
url        = "/archives"
weight     = 20

###############################################################################
# params
[params]
env = "production"
title = "SlashDevOps"
description = "SlashDevOps is a blog about DevOps, Cloud, Containers, Kubernetes, CI/CD, Automation, Infrastructure as Code, and more."
keywords = [
  "DevOps",
  "Cloud",
  "Containers",
  "Kubernetes",
  "CI/CD",
  "Automation",
  "Infrastructure as Code",
]
author = "Christian GonzΓ‘lez Di Antonio"
defaultTheme = "auto" # auto | light | dark
disableThemeToggle = false
ShowReadingTime = true
ShowShareButtons = true
ShowPostNavLinks = true
ShowBreadCrumbs = true
ShowCodeCopyButtons = true
ShowWordCount = true
ShowRssButtonInSectionTermList = true
UseHugoToc = true
disableSpecial1stPost = false
disableScrollToTop = false
comments = true
hidemeta = false
hideSummary = false
showtoc = true
tocopen = true
searchHidden = false

  # logo
  [params.label]
  text       = "slashdevops.com"
  icon       = "/safari-pinned-tab.svg"
  iconHeight = 45

  # profile mode
  [params.profileMode]
  enabled = false

  # home page
  [params.homeInfoParams]
  Title   = "SlashDevops's Blog"
  Content = "πŸ‘‰ Welcome to the SlashDevOps blog. Here you will find articles about DevOps, Cloud, Containers, Kubernetes, CI/CD, Automation, Infrastructure as Code, and more."

  # social icons
  [[params.socialIcons]]
  name = "x"
  url  = "https://x.com/slashdevops"

  [[params.socialIcons]]
  name = "github"
  url  = "https://github.com/slashdevops"

  [params.assets]
  disableHLJS           = true
  disableFingerprinting = false

  # for search
  # https://fusejs.io/api/options.html
  [params.fuseOpts]
  isCaseSensitive = false
  shouldSort = true
  location = 0
  distance = 100
  threshold = 0.4
  minMatchCharLength = 0
  limit = 10 # refer: https://www.fusejs.io/api/methods.html#search
  keys = [
    "title",
    "permalink",
    "summary",
    "content",
  ]

  [params.editPost]
  URL            = "https://github.com/slashdevops/slashdevops.github.io/tree/main/content/"
  Text           = "Edit this post on GitHub"
  appendFilePath = true

###############################################################################
# services
[services]
  [services.disqus]
  shortname = "https-slashdevops-com"

###############################################################################
# markup
[markup.highlight]
anchorLineNos      = true
codeFences         = true
guessSyntax        = false
lineAnchors        = ''
lineNoStart        = 1
lineNos            = true
lineNumbersInTable = true
noClasses          = false
noHl               = false
style              = "monokai"
tabWidth           = 4

###############################################################################
# outputs
[outputs]
home = [
  "HTML",
  "RSS",
  "JSON",
]

πŸ‘‰ 2. My hugo static folder content:

1
2
3
4
5
6
7
8
9
β”œβ”€β”€ static
β”‚   β”œβ”€β”€ Ads.txt
β”‚   β”œβ”€β”€ CNAME     <- this is very important to configure the domain in GitHub Pages
β”‚   β”œβ”€β”€ README.md
β”‚   β”œβ”€β”€ apple-touch-icon.png
β”‚   β”œβ”€β”€ favicon-16x16.png
β”‚   β”œβ”€β”€ favicon-32x32.png
β”‚   β”œβ”€β”€ favicon.ico
β”‚   └── safari-pinned-tab.svg

The content of the CNAME file is:

source: slashdevops/hugo-slashdevops.github.io -> static/CNAME private repository.

1
slashdevops.com

NOTES:

  • Ensure the CNAME file contains the domain you want to use and must be match with the hugo configuration baseURL in the hugo.toml file and with the domain configuration in Google Domains and with the domain configuration in GitHub Pages.
  • This file is located at the private repository hugo-slashdevops.github.io in the static folder and will be deployed to the root of the public repository slashdevops.github.io when the GitHub Actions -> Workflow runs.
  • To understand why this is necessary read CNAME errors

πŸ‘‰ 3. Enabling messages and likes with Disqus

To have messages and likes in the blog posts I used Disqus. To configure it, you need to create an account in Disqus and then create a new site in the Disqus configuration. After that, you will have a shortname that you will use in the hugo.toml configuration file.

To see the configuration in the hugo.toml file, see the hugo.toml configuration file above.

Then I override the themes/PaperMods//layouts/partials/comments.html creating a comments.html inside the layouts/partials folder of my hugo-slashdevops.github.io repository.

This is necessary because you need to have a comments.html with the configuration of the hugo -> Disqus I found in the Hugo -> Disqus -> source code the reference is here Hugo - Disqus.

source: slashdevops/hugo-slashdevops.github.io -> layouts/partials/comments.html private repository.

1
2
3
β”œβ”€β”€ layouts
β”‚   └── partials
β”‚       └── comments.html  <- this is very important to configure the comments in the blog posts

The content of the comments.html file is:

source: Hugo -> Disqus -> source code

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{{- $pc := .Site.Config.Privacy.Disqus -}}
{{- if not $pc.Disable -}}
{{ if .Site.Config.Services.Disqus.Shortname }}<div id="disqus_thread"></div>
<script>
    window.disqus_config = function () {
    {{with .Params.disqus_identifier }}this.page.identifier = '{{ . }}';{{end}}
    {{with .Params.disqus_title }}this.page.title = '{{ . }}';{{end}}
    {{with .Params.disqus_url }}this.page.url = '{{ . | html  }}';{{end}}
    };
    (function() {
        if (["localhost", "127.0.0.1"].indexOf(window.location.hostname) != -1) {
            document.getElementById('disqus_thread').innerHTML = 'Disqus comments not available by default when the website is previewed locally.';
            return;
        }
        var d = document, s = d.createElement('script'); s.async = true;
        s.src = '//' + {{ .Site.Config.Services.Disqus.Shortname }} + '.disqus.com/embed.js';
        s.setAttribute('data-timestamp', +new Date());
        (d.head || d.body).appendChild(s);
    })();
</script>
<noscript>Please enable JavaScript to view the <a href="https://disqus.com/?ref_noscript">comments powered by Disqus.</a></noscript>
<a href="https://disqus.com" class="dsq-brlink">comments powered by <span class="logo-disqus">Disqus</span></a>{{end}}
{{- end -}}

Conclusion

I am very happy with the migration from WordPress to Hugo and GitHub Pages. I can say that I am very satisfied with the result and I recommend this migration to anyone who wants to have a fast, responsive, and easy to maintain blog.