Modern Blogging: Hugo, GitHub Pages, and a Custom Domain

Do you have a project idea and are looking for a simple way to present it on your own website? With Hugo and GitHub Pages, you can get it done in no time. Using this site as an example, I’ll show you how straightforward it is to build your own presence.
Requirements
The barriers to your “own website” project are minimal. All you need is:
- GitHub account: This is where your code will be managed and published.
- Custom domain (optional): If you want your site accessible via a personal address.
- Operating system with a terminal: A solid command line is essential (this guide uses OpenBSD as an example).
Why Hugo?
For someone who works a lot in the terminal (and appreciates minimalism as an OpenBSD maintainer), Hugo is a blessing. It generates static HTML, which means:
- Security: No SQL injections or PHP vulnerabilities.
- Speed: The site loads almost instantly.
- Portability: All content is stored in Markdown files.
Workflow on OpenBSD
Installing Hugo, Git, and some additional useful tools is very simple thanks to available packages:
doas pkg_add hugo git nvi ImageMagickChoosing a Theme
Before getting started, you have plenty of options: Hugo offers a huge selection of themes. In the official overview you can explore designs and test them in live demos.
In this guide, I use the LoveIt theme as an example.
Preparations
DNS Configuration (optional)
Detailed information can be found in the GitHub documentation.
For a personal portfolio page, creating a user page is the easiest way:
- Create repository: Name it
[your-username].github.io - DNS config: Add a CNAME record pointing to your GitHub page
Git(-Hub) Settings
Configure the following settings in GitHub to publish your site:
-
Create Repository: If you haven’t already, create a new repository using the naming scheme
[Your-GitHub-Username].github.io. -
Configure GitHub Pages: Navigate to the Pages menu item in your repository settings and enter the following details:
-
Build and deployment: Under Source, select the GitHub Actions option.
-
Custom Domain: If you want to use your own domain, enter your hostname (e.g.,
buzzdeee.reitenba.ch) and confirm with Save. The DNS check should be successfully completed after a few moments. -
Enforce HTTPS: Enable this checkbox to ensure that your site is encrypted and accessible exclusively via HTTPS.
-
Your settings should then look like the following example:

Example configuration of GitHub Pages settings
##Minimal Setup and Initialization
Once you have decided on a theme, we will lay the foundation for your project.
First, create a local directory, switch into it, initialize a new Git repository, and add the LoveIt theme as a submodule. This has the advantage that you can easily keep the theme up to date later without mixing it with your own code.
Create and enter directory
mkdir buzzdeee.github.io
cd buzzdeee.github.ioInitialize Git repository (with ‘main’ as the default branch)
git init -b mainAdd and initialize theme as a submodule
git submodule add https://github.com/dillonzq/LoveIt.git themes/LoveIt
git submodule update --init --recursiveAdjusting the Configuration
To avoid starting from scratch, we copy the theme’s example configuration into our root directory and edit it using an editor (e.g., nvi or vi):
cp themes/LoveIt/exampleSite/hugo.toml .
nvi hugo.tomlIn the hugo.toml file, you should primarily check and individualize the following points:
- BaseURL: The final address where your site will be accessible (e.g., https://buzzdeee.reitenba.ch/).
- Author Information: Name, social links, and a short bio.
- Language Settings: Default language and menu entries for navigation.
- Comment Function: For the start, it is recommended to deactivate this (a detailed setup would be beyond the scope of this guide).
Below you will find the most important configuration examples to serve as a guide for your own hugo.toml:
baseURL = "https://buzzdeee.reitenba.ch"
# theme
theme = "LoveIt"
# themes directory
themesDir = "themes"
defaultContentLanguage = "de"
[languages]
[languages.de]
weight = 1
languageCode = "de"
languageName = "Deutsch"
title = "Mein Blog"
[languages.de.menu]
[[languages.de.menu.main]]
name = "Posts"
url = "posts"
weight = 1
[[languages.de.menu.main]]
weight = 2
identifier = "tags"
pre = ""
post = ""
name = "Tags"
url = "/tags/"
title = ""
[[languages.de.menu.main]]
weight = 3
identifier = "categories"
pre = ""
post = ""
name = "Kategorien"
url = "/categories/"
title = ""
[[languages.de.menu.main]]
weight = 4
identifier = "documentation"
pre = ""
post = ""
name = "Docs"
url = "/categories/documentation/"
title = ""
[[languages.de.menu.main]]
weight = 5
identifier = "about"
pre = ""
post = ""
name = "About"
url = "/about/"
title = ""
[[languages.de.menu.main]]
weight = 6
identifier = "github"
pre = "<i class='fab fa-github' aria-hidden='true'></i>"
post = ""
name = ""
url = "https://github.com/myaccount"
title = "GitHub"
[languages.en]
weight = 2
languageCode = "en"
languageName = "English"
title = "My Blog"
[languages.en.menu]
[[languages.en.menu.main]]
name = "Posts"
url = "posts"
weight = 1
[[languages.en.menu.main]]
weight = 2
identifier = "tags"
pre = ""
post = ""
name = "Tags"
url = "/tags/"
title = ""
[[languages.en.menu.main]]
weight = 3
identifier = "categories"
pre = ""
post = ""
name = "Categories"
url = "/categories/"
title = ""
[[languages.en.menu.main]]
weight = 4
identifier = "documentation"
pre = ""
post = ""
name = "Docs"
url = "/categories/documentation/"
title = ""
[[languages.en.menu.main]]
weight = 5
identifier = "about"
pre = ""
post = ""
name = "About"
url = "/about/"
title = ""
[[languages.en.menu.main]]
weight = 6
identifier = "github"
pre = "<i class='fab fa-github' aria-hidden='true'></i>"
post = ""
name = ""
url = "https://github.com/buzzdeee"
title = "GitHub"
...
# Author config
[params.author]
name = "buzzdeee"
email = ""
link = ""
...
[params.page.comment]
enable = false
...Store your articles as Markdown files in the ./content/posts/ directory. For Hugo to recognize that two files represent the same content in different languages, they must have the identical filename and only differ by the language code.
Example:
my-article.de.md(German version)my-article.en.md(English version)
Only then can visitors switch directly between the languages of the respective article.
The First Local Test Run
Once the configuration is set, it’s time for the first trial run. Start the Hugo server with the command hugo server -D (the -D flag ensures that drafts are also displayed):
ERROR error building site: failed to acquire a build lock:
open /home/user/GIT/buzzdeee.github.io/.hugo_build.lock: too many open filesOuch! On OpenBSD, you often run into a classic limit problem (too many open files). This happens because Hugo monitors many files simultaneously by default.
There are two ways to solve this:
- The Clean Solution (Increase system limits): Increase the values for
openfiles-maxandopenfiles-curin the/etc/login.conffile to4096. After logging in again,hugo server -Dshould start without issues, including automatic file monitoring (watch mode). - The “Quick Fix” (Without file monitoring): If you don’t want to change the system configuration, you can start Hugo with reduced features. This disables automatic reloading upon changes:
hugo server --disableFastRender --ignoreCache --noHTTPCache --watch=falseOnce the server is running successfully, you can view and test your intermediate result in your browser at http://localhost:1313/ in real-time.
The Avatar Image
There are two ways to provide the profile picture for your home page:
- Gravatar: A global service that provides your image linked to your email address.
- Local Image: An image stored directly in your repository.
Pros and Cons of Gravatar
-
Pro: You only need to update your image in one central place (gravatar.com), and it will automatically be updated across all linked platforms (GitHub, StackOverflow, your blog, etc.).
-
Con (Privacy): To load the image, the visitor’s browser sends a request to Gravatar’s servers. This transmits a hash of your email address and the visitor’s IP address, which can be sensitive regarding privacy (GDPR).
Avatar Configuration in hugo.toml
Depending on which path you choose, you either enter your email address at gravatarEmail or adjust the path at avatarURL:
[params.home.profile]
enable = trueGravatar email for the profile picture (optional)
gravatarEmail = ""Local path to the profile picture
avatarURL = "/images/avatar.png"./static/ directory. The avatar image should therefore be stored at ./static/images/avatar.png. Hugo then makes it available at /images/avatar.png during the build process.Favicon
To make your site look good in the browser tab, you need a favicon. The favicon.ico file belongs directly in the ./static/ directory.
You can use the convert tool from the ImageMagick package to easily transform your avatar image into a suitable favicon:
convert static/images/avatar.png -define icon:auto-resize=64,48,32,16 static/favicon.icoCreating the “About” Page
To ensure the “About” menu item doesn’t lead to a 404 error, we need to create the corresponding content file. Since Hugo distinguishes between regular blog posts and static pages, we place this file directly in the content folder.
Creating the Files
Create a separate Markdown file for each language under ./content/about/:
-
German:
./content/about/index.de.md -
English:
./content/about/index.en.md
Content Example (about/index.en.md)
title: "About me"
layout: "single"
nodateline: true
noauthor: true
Here you can briefly introduce yourself, describe your projects, or explain what this blog is about. Since this file is named index.en.md, it will automatically be displayed under the URL /about/.about/index.de.md.title: "About me"
layout: "single"
nodateline: true
noauthor: true
Here you can briefly introduce yourself, describe your projects, or explain what this blog is about. Since this file is named index.en.md, it wi
ll automatically be displayed under the URL /about/.about/index.de.md.Creating Your First Post
Now that the framework is ready, it’s time for some real content. To create a new blog post, it’s best to use the hugo new command. This creates the file with the appropriate basic structure (the so-called Frontmatter).
Step 1: Create Files
In your terminal, create the German and English versions of your post one after the other:
hugo new posts/my-first-post.de.md
hugo new posts/my-first-post.en.mdStep 2: Set Status to “Published”
By default, Hugo marks new posts as drafts. To make them appear on your live site, you must either delete the line draft = true in the file header (Frontmatter) or set it to false:
title: "My First Post"
date: 2026-03-25T10:00:00+01:00
draft: falsedraft = true is set, you will only see the post locally if you start the server with the -D flag (hugo server -D). On the final website on GitHub, drafts are ignored.Automation with GitHub Actions
To ensure your site is automatically built and published with every git push, we use GitHub Actions. To do this, create the file .github/workflows/hugo.yaml in your repository and insert the following content:
name: Deploy Hugo site to Pages
on:
push:
branches: ["main"]
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: "pages"
cancel-in-progress: false
defaults:
run:
shell: bash
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: recursive # to include the theme
fetch-depth: 0
- name: Setup Hugo
uses: peaceiris/actions-hugo@v3
with:
hugo-version: 'latest'
extended: true # LoveIt theme requires extended version (SCSS)
- name: Build with Hugo
run: hugo --minify
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: ./public
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
needs: build
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4Activation in the Repository Settings
To ensure that GitHub actually uses this workflow (if you haven’t done so already), you need to make a small change in the settings: Navigate to Settings > Pages. Under Build and deployment, select the GitHub Actions option from the Source dropdown menu.
Keeping It Clean: The .gitignore File
When testing and building your site locally, temporary files and directories (such as the public/ folder) are created that do not belong in your Git repository. To exclude these, create a file named .gitignore in your root directory:
Local build directory (will be regenerated on GitHub)
public/Resource cache and temporary files
resources/
.hugo_build.lockThe Final Git Push
Everything is now prepared to take your site live. Use the following commands to transfer your local configuration to your GitHub repository:
git add .
git commit -m "Initial commit of the Hugo site"
git remote add origin git@github.com:buzzdeee/buzzdeee.github.io.git
git push -u origin mainPotential Hurdle: GitHub Push Protection
It may happen that GitHub rejects the push with an error message. This is usually because the LoveIt theme’s example configuration contains a default key for Mapbox, which GitHub blocks for security reasons:
remote: error: GH013: Repository rule violations found for refs/heads/main.
remote: - GITHUB PUSH PROTECTION
remote: Push cannot contain secrets
remote: Mapbox Secret Access Token found in hugo.toml:472The Solution: In this specific case, it is a known demo key from the theme. You have two options:
-
Delete the key in
hugo.toml(around line 472) if you are not using Mapbox. -
Follow the link in the error message (
https://github.com/.../unblock-secret/...) to inform GitHub that this specific key should be ignored.
Once the key has been cleared or removed, the git push will complete successfully, and the build process will start automatically.
hugo.toml. Instead, use environment variables or GitHub Actions Secrets. This keeps your credentials private and prevents them from ending up in your repository’s public code.Conclusion & Checklist
Congratulations! If everything went well, your personal website is now accessible worldwide under your own domain. Thanks to Hugo and GitHub Actions, you now have an extremely fast, secure, and low-maintenance system.
Before you start writing your next article, here is a quick Success Checklist:
- Domain reachable? Check if your URL (e.g.,
https://buzzdeee.reitenba.ch) resolves correctly. - HTTPS active? Does the padlock icon appear in your browser’s address bar?
- Language switch okay? Does switching between German and English work on the “About” page and in your posts?
- Favicon visible? Is your custom icon displayed in the browser tab?
- GitHub Action green? Check the Actions tab in your GitHub repo to see if the build completed without errors.
What’s Next?
You can now create new content at any time by simply creating a new Markdown file and uploading it via git push. Automation will handle the rest for you.
Please note that this guide only covers the basic framework. The LoveIt theme offers many more powerful features that I haven’t covered in detail here to keep things concise. These include:
-
Mapbox Integration: For interactive maps.
-
Comment Systems: Integration of Disqus, Utterances, or Giscus.
-
Social Media: Linking your profiles in the navigation or footer.
-
Shortcodes: Special Hugo commands for galleries, music players, or YouTube videos.
The further journey of discovery through the LoveIt Documentation and trying out new features is now entirely up to you.
Good luck and have fun with your new blog!