1. Install Hugo
  2. Create a Hugo site with the hugo-scroll theme
  3. Get the zjedi/hugo-scroll website
  4. Customize
  5. Deploy
  6. Set up a custom domain
  7. Bonus. The More You Know

Install Hugo

https://gohugo.io/getting-started/quick-start/#prerequisites

Linux

https://gohugo.io/installation/linux/

Download the latest .deb package from Hugo’s GitHub releases, then install it and check version.

Edit vers below to the version you want to install.

vers=0.148.0
package=hugo_extended_"$vers"_linux-amd64.deb
URL="https://github.com/gohugoio/hugo/releases/download/v$vers/$package"
cd ~/Downloads && \
  wget "$URL" && \
  sudo dpkg -i "$package" && \
  rm -v "$package" && \
  hugo version

To remove run sudo apt-get purge hugo.

Windows

Install Git

https://git-scm.com/downloads/win

# Open Powershell as admin, run:
winget install --id Git.Git -e --source winget

# Show installed files
dir "C:\Program Files\Git\cmd"

# Check version
& "C:\Program Files\Git\cmd\git.exe" --version # outputs for example: git version 2.50.1.windows.1

# Check path
echo $env:Path
$env:Path -match "git"

# If False ("git" not in $PATH), add it
$env:Path += ";C:\Program Files\Git\cmd" # temporal
[Environment]::SetEnvironmentVariable("Path", ([Environment]::GetEnvironmentVariable("Path", "User") + ";C:\Program Files\Git\cmd"), "User") # persistent
git --version # outputs for example: git version 2.50.1.windows.1

Install Go

https://go.dev/dl/

Windows 10 or later, Intel 64-bit processor download for example go1.24.5.windows-amd64.msi.

Either

  • Double-click the downloaded .msi file and follow the on-screen instructions to complete the installation. The default settings are suitable for most users.
  • Or
cd $HOME/Downloads
Get-ChildItem | Where-Object { $_.Name -match "msi" } # shows go1.24.5.windows-amd64.msi

# Install
Start-Process 'msiexec.exe' -ArgumentList '/i', 'go1.24.5.windows-amd64.msi', '/qn', '/norestart', '/l*v', 'go_install.log' -Wait
# Check version
& "C:\Program Files\Go\bin\go.exe" version # outputs for example: go version go1.24.5 windows/amd64

# Check path
echo $env:Path
$env:Path -match "go"

# If False ("go" not in $PATH), add it
$env:Path += ";C:\Program Files\Go\bin" # temporal
[Environment]::SetEnvironmentVariable("Path", ([Environment]::GetEnvironmentVariable("Path", "User") + ";C:\Program Files\Go\bin"), "User") # persistent
go version # outputs for example: go version go1.24.5 windows/amd64

Install Dash

Download the dart-sass-1.89.2-windows-x64.zip or alike of https://github.com/sass/dart-sass/releases

Either

  • Double-click the downloaded .zip file and extract it into C:\dart-ass
  • Or
cd $HOME/Downloads
Get-ChildItem | Where-Object { $_.Name -match "zip" } # shows dart-sass-1.89.2-windows-x64.zip

# Unzip
Expand-Archive -LiteralPath .\dart-sass-1.89.2-windows-x64.zip -DestinationPath .\dart-sass

# Move the extracted folder
Move-Item .\dart-sass\* C:\dart-sass
# Check path
echo $env:Path
$env:Path -match "dart"

# If False ("dart-sass" not in $PATH), add it
$env:Path += ";C:\dart-sass" # temporal
[Environment]::SetEnvironmentVariable("Path", ([Environment]::GetEnvironmentVariable("Path", "User") + ";C:\dart-sass"), "User") # persistent
sass --version # 1.89.2

Install Hugo

https://gohugo.io/installation/windows/#prebuilt-binaries

Download the extended edition, for example hugo_extended_0.148.1_windows-amd64.zip from Hugo’s GitHub releases, then

Either

  • Double-click the downloaded .zip file and extract it into C:\hugo
  • Or
cd $HOME/Downloads
Get-ChildItem | Where-Object { $_.Name -match "zip" } # shows hugo_extended_0.148.1_windows-amd64.zip

# Unzip
Expand-Archive -LiteralPath .\hugo_extended_0.148.1_windows-amd64.zip -DestinationPath .\hugo

# Move the extracted folder
Move-Item .\hugo C:\hugo
# Check path
echo $env:Path
$env:Path -match "hugo"

# If False ("hugo" not in $PATH), add it
$env:Path += ";C:\hugo" # temporal
[Environment]::SetEnvironmentVariable("Path", ([Environment]::GetEnvironmentVariable("Path", "User") + ";C:\hugo"), "User") # persistent
hugo version # hugo v0.148.1-...+extended windows/amd64 BuildDate=2025-07-11T12:56:21Z VendorInfo=gohugoio
# make sure its EXTENDED !

Test a Hugo repo

cd $HOME/Downloads
git clone https://github.com/juanMarinero/juanmarinero.github.io
cd juanmarinero.github.io
git submodule update --init --recursive # Important!
hugo server

…which outputs:

Watching for changes in C:\Users\UserName\Downloads\juanmarinero.github.io\{archetypes,assets,content,layouts,static,themes}
Watching for config changes in C:\Users\UserName\Downloads\juanmarinero.github.io\hugo.toml
Start building sites …
hugo v0.148.1-98ba786f2f5dca0866f47ab79f394370bcb77d2f+extended windows/amd64 BuildDate=2025-07-11T12:56:21Z VendorInfo=gohugoio

                  │ ES
──────────────────┼─────
 Pages            │   7
 Paginator pages  │   0
 Non-page files   │   0
 Static files     │ 110
 Processed images │   8
 Aliases          │   0
 Cleaned          │   0

Built in 23132 ms
Environment: "development"
Serving pages from disk
Running in Fast Render Mode. For full rebuilds on change: hugo server --disableFastRender
Web Server is available at http://localhost:1313/ (bind address 127.0.0.1)
Press Ctrl+C to stop
Current date: Monday, July 14, 2025, 11:50 AM CEST

Open in browser http://localhost:1313/

In future just run:

cd $HOME/Downloads/juanmarinero.github.io
hugo server

Create a Hugo site with the hugo-scroll theme

Follow quickstart - commands applaying hugo-scroll theme

cd ~/Downloads
hugo new site quickstart
cd quickstart
git init
git submodule add https://github.com/zjedi/hugo-scroll.git themes/hugo-scroll
echo "theme = 'hugo-scroll'" >> hugo.toml
hugo server

This should output like

> hugo server
Watching for changes in <path>/quickstart/{archetypes,assets,content,data,i18n,layouts,static,themes}
Watching for config changes in <path>/quickstart/hugo.toml
Start building sites … 
hugo v0.148.0-c0d9bebacc6bf42a91a74d8bb0de7bc775c8e573+extended linux/amd64 BuildDate=2025-07-08T13:34:49Z VendorInfo=gohugoio


                  │ EN  
──────────────────┼─────
 Pages            │   6 
 Paginator pages  │   0 
 Non-page files   │   0 
 Static files     │ 108 
 Processed images │   0 
 Aliases          │   0 
 Cleaned          │   0 

Built in 25 ms
Environment: "development"
Serving pages from disk
Running in Fast Render Mode. For full rebuilds on change: hugo server --disableFastRender
Web Server is available at http://localhost:1313/ (bind address 127.0.0.1) 
Press Ctrl+C to stop

Now, open in browser http://localhost:1313/

Notice the browser tab name My New Hugo Site but the site is empty.

Let’s stop the server with Ctrl+C and add some content.

We can commit the changes to the repository.

echo ".hugo_build.lock" >> .gitignore
git add .
git commit -m "add hugo-scroll theme as git submodule"

Get the zjedi/hugo-scroll website

https://github.com/zjedi/hugo-scroll#-installation says:

If you are starting fresh, simply copy over the contents of the exampleSite-directory included in this theme to your source directory. That should give you a good idea about how things work, and then you can go on from there to make the site your own.

cd ~/Downloads/quickstart
hugo_scroll_exampleSite=themes/hugo-scroll/exampleSite
cp -rv "$hugo_scroll_exampleSite"/content/* content/
cp -rv "$hugo_scroll_exampleSite"/assets/* assets/
cp -v "$hugo_scroll_exampleSite"/hugo.toml .
rm -v archetypes/* # to use the hugo-scroll ones

Run hugo server --disableFastRender.

Watching for changes in <path>/quickstart/{archetypes,assets,content,data,i18n,layouts,static,themes}
Watching for config changes in <path>/quickstart/hugo.toml
Start building sites … 
hugo v0.148.0-c0d9bebacc6bf42a91a74d8bb0de7bc775c8e573+extended linux/amd64 BuildDate=2025-07-08T13:34:49Z VendorInfo=gohugoio


                  │ EN  │ DE  
──────────────────┼─────┼─────
 Pages            │  15 │  14 
 Paginator pages  │   0 │   0 
 Non-page files   │   1 │   0 
 Static files     │ 114 │ 114 
 Processed images │   8 │   0 
 Aliases          │   1 │   0 
 Cleaned          │   0 │   0 

Built in 4793 ms
Environment: "development"
Serving pages from disk
Web Server is available at http://localhost:1313/ (bind address 127.0.0.1) 

Now we succeeded! Our local website http://localhost:1313/ it’s like the Hugo Scroll demo site!

Commit.

git add .
git commit -m "build hugo-scroll demo site"

From Git submodule to Hugo module

Previously we included the theme as a Git submodule cloning the Hugo Scroll theme into the themes/hugo-scroll directory.

git submodule add https://github.com/zjedi/hugo-scroll.git themes/hugo-scroll
echo "theme = 'hugo-scroll'" >> hugo.toml

And in previous section we copied from that folder content/, assets/ and the Hugo config file into our project’s root.

Well, this proccedure is not recommended any longer. Unit 8.2 Themes as Hugo Modules of the ebook Hugo in Action says next:

In section 2.2, where we introduced the concept of themes, we discussed three ways to integrate themes into a website:

  • download and copy (which we have used so far),
  • Git submodules (not recommended anymore),
  • and Hugo Modules (which we did not use because download and copy is easier for beginners).

Next we follow the instructions provided in the documentation of a very well up-to-date documented Hugo theme called Docsy. In particular the section titled Migrating to Hugo Modules.

Turn your existing site into a Hugo Module

Run hugo mod init github.com/me/my-existing-site. In my case it’s hugo mod init github.com/juanMarinero/juanmarinero.github.io.

This will create the go.mod file in the root of the project. While hugo mod get will generate go.sum.

  • go.mod for the module definitions.
  • go.sum contains checksums for module verification.
Get the Hugo Scroll theme as a Hugo Module
hugo mod get github.com/zjedi/hugo-scroll@latest

If needed read Getting a specific version of a theme.

Edit the hugo.toml file to substitute the theme = "hugo-scroll" (or alike) line with a comment
sed -i 's/^theme =.*/# Read [[module.imports]]/' hugo.toml

Check that replacement and surrounding with grep -C 3 '# Read \[\[module.imports\]\]' hugo.toml. It will echo for example:

defaultContentLanguage = "en"

# The name of this wonderful theme ;-).
# Read [[module.imports]]

# The browser tab name
title = "Jane Doe - Nutrition Coach & Chef Consultant"

Or just remove the config line with: sed -i '/^theme =.*/d' hugo.toml.

Now append next configuration, where:

  • The Hugo version is copied from Hugo Scroll’s theme.toml
  • The Hugo extended version is required for the SCSS scripts to work. It’s stated in the theme’s README.md
cat >> hugo.toml <<EOL

[module]
  proxy = "direct"
  [module.hugoVersion]
    extended = true
    min = "0.132.0"
  [[module.imports]]
    path = "github.com/zjedi/hugo-scroll"
    disable = false
EOL
tail hugo.toml # inspect

Alternative to this whole step (remove/comment the theme = line and append the later configs) we could simply replace the theme = line with:

sed -i 's/^theme =.*/theme = "github.com/zjedi/hugo-scroll"/' hugo.toml

Though this alternative is not so explicit about getting the theme as a module.

Build and commit
hugo server --disableFastRender

Stop with Ctrl+C and commit the changes

git add .
git commit -m "add hugo-scroll as a Hugo Module"
Remove the Git submodule
git rm -rf themes/hugo-scroll
Build and commit
hugo server --disableFastRender

Stop with Ctrl+C and commit the changes

git add .
git commit -m "remove themes/hugo-scroll/"
(Optional) Vendor dependencies

Viewing the dependencies source code:

Run hugo mod vendor. Hugo creates a folder called _vendor with the source code of all the dependencies. Not only that, if this folder is present and contains the dependencies, Hugo does not go to the internet to build our website.

To create a local copy of dependencies run hugo mod vendor.

Check the _vendor folder with tree -L 4 _vendor:

_vendor
├── github.com
│   └── zjedi
│       └── hugo-scroll
│           ├── archetypes
│           ├── assets
│           ├── i18n
│           ├── layouts
│           ├── package.json
│           ├── static
│           └── theme.toml
└── modules.txt

If you don’t want to commit vendored files run echo "_vendor/" >> .gitignore.

Build and commit
hugo server --disableFastRender

Stop with Ctrl+C and commit the changes

git add .
git commit -m "add _vendor/hugo-scroll/"
Commonly used Hugo Modules APIs

Hugo In Action ebook explains next commands. The Hugo documentation is the right place to study them deeper.

  • hugo mod tidy removes unused entries from the go.sum and go.mod
  • hug mod clean clears the module cache to pick up newer changes (if done locally). Sometimes, the local store may get corrupt, giving us incorrect data.
  • hugo mod graph list the dependencies of the current website.
  • hugo mod get -u updates all modules.
  • {hugo mod get -u ./…} updates all modules and their dependencies.
  • hugo mod get @ gets a specific version of a module.
Work smart, not hard

Finally, it’s awesome that you followed these non trivial instructions all the way.

But no need to repeat this tedious procedure next time. Instead you can use the repo where I pushed all the previous steps.

  1. Clone it: git clone https://github.com/juanMarinero/hugo-scroll-MWE

  2. Build with hugo server (optional). Open http://localhost:1313 in the browser. The local website is the same as the Hugo Scroll demo.

  3. Customize it.

But wait! This is not the smart way to do it. A migration is no need if you never existed! Keep reading!

Create the Hugo Scroll demo from scratch

Create a new Hugo site, initialize it as a Hugo Module, and add the Hugo Scroll theme as a submodule.

hugo new site quickstart
cd quickstart

hugo mod init github.com/me/my-existing-site # edit!

hugo mod get github.com/zjedi/hugo-scroll@latest # ⏳ wait few seconds

Now clone the Hugo Scroll repo in other directory.

cd .. # out of quickstart

git clone https://github.com/zjedi/hugo-scroll.git
cd hugo-scroll

And copy what we need from it:

my_repo="../quickstart"
cp -rv exampleSite/content/* "$my_repo"/content/
cp -rv exampleSite/assets/* "$my_repo"/assets/
cp -v exampleSite/hugo.toml "$my_repo"/
rm -v "$my_repo"/archetypes/* # to use the hugo-scroll ones

And do not forget to edit the hugo.toml:

cd "$my_repo"
sed -i 's/^theme =.*/# Read [[module.imports]]/' hugo.toml
cat >> hugo.toml <<EOL

[module]
  proxy = "direct"
  [module.hugoVersion]
    extended = true
    min = "0.132.0"
  [[module.imports]]
    path = "github.com/zjedi/hugo-scroll"
    disable = false
EOL

Get the _vendor populated:

hugo mod vendor

Build and commit

hugo server

Stop with Ctrl+C and commit the changes

git init
echo ".hugo_build.lock" > .gitignore
echo "_vendor/" >> .gitignore # optional
git add .
git commit -m "Initial commit

- Hugo Scroll theme as Hugo Module
- From Hugo Scroll exampleSite copied:
  - content/
  - assets/
  - hugo.toml (adapted for module)"

…or just clone a repo that already ran all the previous steps:

git clone https://github.com/juanMarinero/hugo-scroll-MWE-revised
cd hugo-scroll-MWE-revised

Customize

Order of markdowns

The website sections are sorted by the weigtht metadata. Details in my post concerning Hugo bundle pages.

Lets check them all!

  1. Create a script like cat_by_weight.sh
  2. Run it on the content directory
chmod +x cat_by_weight.sh
./cat_by_weight.sh content/en/homepage head -n 5

Note. You can pass another catter like cat or tail. The default is bat --line-range=1:10

to get:

weightfile
1content/en/homepage/opener.md
3content/en/homepage/about-me.de.md
3content/en/homepage/about-me.md
4content/en/homepage/contact.md
98content/en/homepage/legal-brief.md
99content/en/homepage/credits.md
99content/en/homepage/external.md
--- content/en/homepage/opener.md (weight: 1) ---
---
title: "Welcome"
weight: 1
---


--- content/en/homepage/about-me.de.md (weight: 3) ---
---
title: "Über mich (shared folder)"
weight: 3
header_menu: true
---

--- content/en/homepage/about-me.md (weight: 3) ---
---
title: "About Me"
weight: 3
header_menu: true
---

--- content/en/homepage/contact.md (weight: 4) ---
---
title: "Contact"
weight: 4
header_menu: true
---

--- content/en/homepage/legal-brief.md (weight: 98) ---
---
title: "Brief Legal Information"
weight: 98
header_menu_title: "Legal"
navigation_menu_title: "Legal stuff"

--- content/en/homepage/credits.md (weight: 99) ---
---
title: "Credits"
weight: 99
header_menu: true
---

--- content/en/homepage/external.md (weight: 99) ---
---
title: "GitHub"
weight: 99
header_menu: true
external: https://github.com/zjedi/hugo-scroll

--- content/en/homepage/index.md (no weight) ---
---
headless: true
---

--- content/en/homepage/license.md (no weight) ---
---
footer_menu_title: License
footer_menu: true
detailed_page_path: /license/
detailed_page_homepage_content: false

--- content/en/homepage/services.md (no weight) ---
---
title: "The Services I Offer"
header_menu_title: "Services"
navigation_menu_title: "My Services"
weight: 2

hugo.toml

Open with your favorit editor "$EDITOR" hugo.toml and edit, among other:

  • baseURL replace with your domain, or with https://<username>.github.io, for example https://juanmarinero.github.io
  • defaultContentLanguage = "en" edit if needed, but this requires to, if not en nor de to
    • edit end of page [languages]
    • and add a directory like contentDir = "content/es" for español (Spanish)
  • The browser tab name title = "..."
  • set title_guard to true if title not legible because color contrast
  • set language_menu and show_current_lang to false if just one language website
  • invertSectionColors = false set to true and check if it likes you more
  • set showContactIcons to true to share email/phone in footnote
  • edit params.meta and params.contacts

Favicons, header-logo and cover-image

Replace

cd assets/images/
sudo apt update && sudo apt install imagemagick optipng -y
convert favicon.png -resize 180x180 apple-touch-icon.png
  • Same for public/images/favicon-16x16.png and public/images/favicon-32x32.png. Also for static/images/ and assets/favicon.ico
cd <repo-path>
convert assets/images/favicon.png -resize 16x16 public/images/favicon-16x16.png
convert assets/images/favicon.png -resize 32x32 public/images/favicon-32x32.png
convert assets/images/favicon.png -resize 16x16 static/images/favicon-16x16.png
convert assets/images/favicon.png -resize 32x32 static/images/favicon-32x32.png
convert assets/images/favicon.png -define icon:auto-resize=64,48,32,16 assets/favicon.ico

To change the background image to a video hugo.toml says

    # These "images" are used for the structured data templates. This will show up, when
    # ...
    # NOT the actual header background image, go to _index.md instead
    images = ["images/cover-image.jpg"]

Thus, check if content/en/_index.md sets a fixed image:

# When set true, uses video from custom_header_video.html partial, instead of header_image
header_use_video: false

Toggle it to true, add the video (for example from pexels) to assets/cover/, and edit the video name in layouts/partials/custom_header_video.html.

Tip, set video speed there directly in that html file with:

<video id="portfolioVideo" ...
    {{ $videoResource := resources.Get "cover/video_name.mp4" }}
    <source src="{{ $videoResource.RelPermalink }}" type="video/mp4">
</video>

<script>
  // Slow down video to 50% speed
  document.getElementById('portfolioVideo').playbackRate = 0.5;
</script>

Edit a mainpage section

For example edit content/en/homepage/services.md

  • title: "The Services I Offer" is the title of that section of the website.
  • header_menu_title: "Services" is text of a link to this website section, this link is displayed in the cover section or hero section., i.e. content/en/_index.md section, i.e. in at start of the mainpage.
  • navigation_menu_title: "My Services" is the text of a link to this website section, this link is displayed at the cover menu (the navigation bar at the top showing links to sections of the main page, it keeps there even while one scrolls)… as long as header_menu is true in the webseite section.

In contrast content/en/homepage/opener.md has no header_menu_title nor navigation_menu_title. And therefore this website section does not appear in the cover section nor in the cover menu. As its content stated here:

By the way this welcome section won’t show in the cover menu.

Edit a not mainpage

For example https://zjedi.github.io/hugo-scroll/services/ has the content of https://github.com/zjedi/hugo-scroll/blob/master/exampleSite/content/en/services.md (not to confuse with https://github.com/zjedi/hugo-scroll/blob/master/exampleSite/content/en/homepage/services.md)

https://github.com/zjedi/hugo-scroll/blob/master/exampleSite/content/en/homepage/opener.md?plain=1, i.e. the mainpage, links to this not-mainpage via [dedicated pages](services) of:

You can also delegate lengthier, less important or more sizeable content to [dedicated pages](services).

In the language directory, for example public/en/ edit the sitemap.xml file.

CSS

Previously we copied the content of content/, assets/ and hugo.toml from the Hugo Scroll example site. Let’s now copy next HTML file to customize it.

hugo_scroll_exampleSite="themes/hugo-scroll/exampleSite"
custom_head_dir="layouts/partials"
mkdir -p "$custom_head_dir"
cp -v "$hugo_scroll_exampleSite"/"$custom_head_dir"/custom_head.html "$custom_head_dir"/

layouts/partials/custom_head.html shows how to add CSS code:

  • Directly in that file, just below comment line Custom CSS via inline styles add an style tag not commented (not wrapped inside <!-- - -->) for example <style>fontsize=1.2</style> to increase every text size a 20%
  • And/or via a css file, read commentes in the file

Check original CSS style in themes/hugo-scroll/assets/css/. For example to change background colors of odd sections edit themes/hugo-scroll/assets/css/variables.scss, its --section-dark-bg-color. Note, I should not directly edit themes/hugo-scroll since its a submodule not of my property, thus I cannot push it. Instead in layouts/partials/custom_head.html write:

<style>
:root {
    --section-dark-bg-color: #c1cdaa;
}
</style>

Raw HTML

Check out my post concerning Hugo shortcodes, specially its dedicated paragraph about raw HTML.

Deploy

References

Create a GitHub repository

Create a new public repository named username.github.io, where username is your GitHub username. But use lowercase letters even if your username has capitals. For example, for mdcruz it is mdcruz.github.io.

And push your local repository to GitHub.

Note, content from submodules of third people must not be edited. I.e. do not edit themes/hugo-scroll/ submodule.

Host on GitHub

Follow steps of https://gohugo.io/host-and-deploy/host-on-github-pages/

  • Under Settings/Pages/Source change Deploy from a branch to Github Actions.

  • $EDITOR hugo.toml and add

[caches]
  [caches.images]
    dir = ':cacheDir/images'
  • Create a file named hugo.yaml in a directory named .github/workflows
mkdir -p .github/workflows
touch .github/workflows/hugo.yaml
  • $EDITOR .github/workflows/hugo.yaml and copy and paste the YAML below into the file you created. Change the branch name and Hugo version as needed.
# 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
  pages: write
  id-token: write

# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
concurrency:
  group: "pages"
  cancel-in-progress: false

# Default to bash
defaults:
  run:
    # GitHub-hosted runners automatically enable `set -eo pipefail` for Bash shells.
    shell: bash

jobs:
  # Build job
  build:
    runs-on: ubuntu-latest
    env:
      DART_SASS_VERSION: 1.89.2
      HUGO_VERSION: 0.148.0
      HUGO_ENVIRONMENT: production
      TZ: America/Los_Angeles
    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: |
          wget -O ${{ runner.temp }}/dart-sass.tar.gz https://github.com/sass/dart-sass/releases/download/${DART_SASS_VERSION}/dart-sass-${DART_SASS_VERSION}-linux-x64.tar.gz
          tar -xf ${{ runner.temp }}/dart-sass.tar.gz --directory ${{ runner.temp }}
          mv ${{ runner.temp }}/dart-sass/ /usr/local/bin
          echo "/usr/local/bin/dart-sass" >> $GITHUB_PATH
      - name: Checkout
        uses: actions/checkout@v4
        with:
          submodules: recursive
          fetch-depth: 0
      - name: Setup Pages
        id: pages
        uses: actions/configure-pages@v5
      - name: Install Node.js dependencies
        run: "[[ -f package-lock.json || -f npm-shrinkwrap.json ]] && npm ci || true"
      - name: Cache Restore
        id: cache-restore
        uses: actions/cache/restore@v4
        with:
          path: |
            ${{ runner.temp }}/hugo_cache
          key: hugo-${{ github.run_id }}
          restore-keys:
            hugo-
      - name: Configure Git
        run: git config core.quotepath false
      - name: Build with Hugo
        run: |
          hugo \
            --gc \
            --minify \
            --baseURL "${{ steps.pages.outputs.base_url }}/" \
            --cacheDir "${{ runner.temp }}/hugo_cache"
      - name: Cache Save
        id: cache-save
        uses: actions/cache/save@v4
        with:
          path: |
            ${{ runner.temp }}/hugo_cache
          key: ${{ steps.cache-restore.outputs.cache-primary-key }}
      - name: Upload artifact
        uses: actions/upload-pages-artifact@v3
        with:
          path: ./public

  # Deployment job
  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@v4

For example check and edit if needed:

  • DART_SASS_VERSION: 1.89.2

  • HUGO_VERSION: 0.148.0

  • TZ: America/Los_Angeles

  • Commit and push the change to your GitHub repository.

git add -A
git commit -m "Create hugo.yaml"
git push

Wait for Github pages to be ready

Go to Actions tab, for example, and check the status of the deployment, wait until Build and deploy job is green.

Now click on the link with same content as last commit message, Create hugo.yaml for example, this will show the link to your live site, like https://mdcruz.github.io (for this repo).

Rebuild before pushing

After every change remember to rebuild the site by running the following command: hugo server --disableFastRender

For example, if I modify projects.md then git status --short just shows:

M content/en/homepage/projects.md

instead of the neccesary:

M content/en/homepage/projects.md
M public/index.html

Therefore, if I just commit and push it then I will see no changes in the live site, instead one must:

  1. Rebuild the website (public/index.html) with hugo server --disableFastRender,
  2. Check locally the changes on http://localhost:1313/,
  3. And if the result is satisfactory proceed to stage, commit and push.

Removing public related files is also a good idea. This way Hugo will re-build just the neccesary files, and the no longer needed files can be commited. For example, for my blogs (content/en/blogs) I run:

rm -rf public/blogs
hugo server --disableFastRender

Then I check it in the browser.

Finally I make sure that all needed is rebuilt previous to push:

tree -L 2 public/blogs
hugo list all

Set up a custom domain

Buy a domain

Manage the DNS records for your domain

Read

Remove both the ALIAS and CNAME records you currently have set.

The A records

For the A record, set the following values:

185.199.108.153
185.199.109.153
185.199.110.153
185.199.111.153

For example in Porkbun’s DNS management add these four A records (one for each GitHub IP). Like this:

Type: A
Host: @ (or leave empty)
Answer/Value: 185.199.108.153
TTL: 600

The CNAME records

  • The DNS-CNAME-record tells the internet where to find your site.
  • The GitHub-CNAME-file tells GitHub which domain(s) are allowed to show your site.

Next steps varies in each domain provider.

  • Flexible Providers (e.g., Porkbun, Cloudflare):
    • Use synthetic records (ALIAS, ANAME) that mimic CNAME behavior at the root but technically resolve to A records.
    • These bypass DNS standards, making GitHub verification “work” even though it’s not a true CNAME.
  • Strict Providers (e.g., EuroDNS, AWS Route 53):
    • Enforce pure RFC compliance.
    • Block CNAME at the apex entirely → GitHub can’t verify the domain unless you use A records.

For example, for my.euroDNS.com just remove every CNAME and that’s it. You can jump to the next step.

For Porkbun’s DNS management and alike, add a CNAME with the value <username>.github.io.

Type: CNAME
Host: www
Answer/Value: juanmarinero.github.io
TTL: 600

This is because Porkbun-alike domain providers let you set an ALIAS (not true CNAME) for root, this works like A records. This is confirmed in the testingwithmarie.com post, where the screenshot shows CNAME (Aliases) instead of plain CNAME.

Set your domain on Github

Read https://www.testingwithmarie.com/posts/20241126-create-a-static-blog-with-hugo/#set-up-custom-domain

Add your domain to GitHub Pages settings

In the Github repo, go to Settings > Pages and add your domain under the Custom domain field. For example add juan-marinero.info, or www.juan-marinero.info.

Wait for up to 24 hours to see the site to your custom domain. So, from now on, you can either:

All these URLs will:

Add your domain to a CNAME file

To serve your site at the wwww subdomain (e.g. http(s)://www.juan-marinero.info) and redirect the root domain (juan-marinero.info) to this www address:

  • Make sure your DNS is configured properly, as described earlier.
  • Add a CNAME file to your <username>.github.io repo, as Marie’s page did here. You can do this with the following commands:
cd <repo-path>
echo "www.juan-marinero.info" > CNAME # edit !!
git add CNAME
git commit -m "add CNAME with www.juan-marinero.info"
git push

Enforce HTTPS

Previous links follow the HTTPS protocol because I enforced HTTPS for my GitHub Pages site .

This is not mandatory, but recommended.

Bonus. The More You Know

Apex Domains and the www subdomain

Github Pages docs states

  • An apex domain is a custom domain that does not contain a subdomain, such as example.com.
  • Apex domains are also known as base, bare, naked, root apex, or zone apex domains.
  • If you are using an apex domain as your custom domain, we recommend also setting up a www subdomain.

Notice we followed this advice in previous sections, where the CNAME record has www as host.

Understanding CNAME Restrictions on Apex Domains

  1. DNS Standards (RFC 1034):
    • The apex/root domain (e.g., example.com) cannot have a CNAME record if other records (like MX, TXT, NS) exist for the same domain.
    • This is because CNAME conflicts with other record types. GitHub Pages requires a CNAME for verification, but apex domains need A/AAAA records instead.
  2. How GitHub Pages Checks Domains:
    • When you add example.com in GitHub Pages settings, GitHub:
      • Looks for a CNAME file in your repo (for subdomains like www.example.com).
      • For apex domains, it checks for A records pointing to GitHub’s IPs.
    • If GitHub finds a CNAME at the apex (which violates DNS rules), verification fails.

GitHub Pages sites can be stored in any repository

Read Types of GitHub Pages sites.

In this guide we worked with the <username>.github.io repository, which has the apex domain <username>.github.io (accesible via http(s)://<username>.github.io).

But GitHub Pages also support storing sites in other repositories, like <project-name>.github.io/<repo-name>, which have the apex domain <project-name>.github.io/<repo-name> (displayed via http(s)://<project-name>.github.io/<repo-name>). For these you must have a project account. Read next section.

Hugo is not the only solution

To deploy our local website on GitHub Pages is very recommended to use a static site generator like Hugo.

With Hugo we followed its Host on GitHub Pages docs, which, in summary, apply Github actions adding the config file .github/workflows/hugo.yaml. This workflow install Hugo’s dependencies, Hugo itself, node.js, configure git,… and then builds the site using Hugo. For example, the build step depends on these few lines

      - name: Build with Hugo
        run: |
          hugo \
            --gc \
            --minify \
            --baseURL "${{ steps.pages.outputs.base_url }}/" \
            --cacheDir "${{ runner.temp }}/hugo_cache"

Jekyll is other static site generator solution with built-in support for GitHub Pages, i.e. an alternative to Hugo.

Next I exemplify how to create a Github Pages site without any of those static site generators. It’s exaplained for a user or organization site, but it’s analogous for a project site case.

Publishing from a branch

Create a site for your <username>.github.io repository, screenshots in https://pages.github.com/ (click on User or organization site).

  1. Create a new Github repository called <username>.github.io (unless you already have one, if you do, you can skip these steps or create a new repo with other name)
  2. Push an index.html file to it
cd <repo-path>
echo '<h1>Hello!</h1>' > index.html
git init
git add .
git commit -m "Initial commit"
git branch -M main
git remote add origin https://github.com/<username>.github.io.git
git push -u origin main
  1. In a browser go to http://<user-name>.github.io.

Note. The Github Pages docs is not limited to index.html:

Create the entry file for your site. GitHub Pages will look for an index.html, index.md, or README.md file as the entry file for your site.

Now let’s create another repo, for example test-publishing-from-a-branch, with that same index.html. Then follow steps of Publishing from a branch (TL;DR Settings/Pages, under Source select Deploy from a branch, select the main branch, and click Save)

Also check the step-by-step screenshots in https://pages.github.com/ (click on Project site and Start from scratch). Though initial steps are meant for a project site.

For the repo juanMarinero/test-publishing-from-a-branch that configuration triggers next workflow: pages build and deployment #1

Thus, I can see that index.html is the entry file for my site. Any of next links would work:

Once the index.html file is edited the site is auto-updated.

cd <repo-path>
echo '<h2>Welcome!</h2>' >> index.html
git add index.html
git commit -m "Edit index.html"
git push

Check the previous URLs again and the new workflow execution pages build and deployment #2

Publishing with a custom GitHub Actions workflow

Alternative to Publishing from a branch is Publishing with a custom GitHub Actions workflow. This way one sets GitHub Actions, and then, quoting in cursive the docs:

  • GitHub will suggest several workflow templates. If you already have a workflow to publish your site, you can skip this step. Actually this is how we already did it before (here) in our Hugo example, via the workflow template .github/workflows/hugo.yaml.
  • Otherwise, choose one of the options to create a GitHub Actions workflow. This is the challenge! To create a custom workflow for your site’s needs.