- Install Hugo
- Create a Hugo site with the hugo-scroll theme
- Get the zjedi/hugo-scroll website
- Customize
- Deploy
- Set up a custom domain
- 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
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 intoC:\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 intoC:\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 thego.sum
andgo.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.
Clone it:
git clone https://github.com/juanMarinero/hugo-scroll-MWE
Build with
hugo server
(optional). Open http://localhost:1313 in the browser. The local website is the same as the Hugo Scroll demo.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.
- E.g. next has weight 99 to appear last exampleSite/content/en/homepage/credits.md
- Next weight 3 to appear the 3rd at top exampleSite/content/en/homepage/about-me.md
Lets check them all!
- Create a script like cat_by_weight.sh
- 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:
weight | file |
---|---|
1 | content/en/homepage/opener.md |
3 | content/en/homepage/about-me.de.md |
3 | content/en/homepage/about-me.md |
4 | content/en/homepage/contact.md |
98 | content/en/homepage/legal-brief.md |
99 | content/en/homepage/credits.md |
99 | content/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 withhttps://<username>.github.io
, for examplehttps://juanmarinero.github.io
defaultContentLanguage = "en"
edit if needed, but this requires to, if noten
norde
to- edit end of page
[languages]
- and add a directory like
contentDir = "content/es"
for español (Spanish)
- edit end of page
- The browser tab name
title = "..."
- set
title_guard
totrue
if title not legible because color contrast - set
language_menu
andshow_current_lang
tofalse
if just one language website invertSectionColors = false
set to true and check if it likes you more- set
showContactIcons
totrue
to share email/phone in footnote - edit
params.meta
andparams.contacts
Favicons, header-logo and cover-image
Replace
assets/images/cover-image.jpg
with for example https://www.freepik.com/search?format=search&last_filter=selection&last_value=1&query=Zen+Background&selection=1.assets/images/favicon.png
with for example https://www.flaticon.com/free-icons/zen. Convert it to andassets/images/apple-touch-icon.png
of 180x180px via:
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
andpublic/images/favicon-32x32.png
. Also forstatic/images/
andassets/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
assets/images/chef-hat.png
renamed toheader-logo.png
with for example https://www.flaticon.com/search?word=massage. Also rename that file incontent/en/_index.md
with your favorit editor:"$EDITOR" content/en/_index.md
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 asheader_menu
istrue
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 anstyle
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
- https://gohugo.io/host-and-deploy/host-on-github-pages/
- https://www.testingwithmarie.com/posts/20241126-create-a-static-blog-with-hugo/
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
changeDeploy from a branch
toGithub 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:
- Rebuild the website (
public/index.html
) withhugo server --disableFastRender
, - Check locally the changes on http://localhost:1313/,
- 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
- Check if available at https://lookup.icann.org/whois/
- Buy it at https://www.namecheap.com/domains/ for example. Note, you do not need Additional Hosting since GitHub Pages offers free static hosting.
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 mimicCNAME
behavior at the root but technically resolve toA
records. - These bypass DNS standards, making GitHub verification “work” even though it’s not a true
CNAME
.
- Use synthetic records (
- 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 useA
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:
- Visit the
github.io
Gihtub Page, for example https://juanmarinero.github.io - Visit your custom domain, like https://juan-marinero.info or with the
www
subdomain like https://www.juan-marinero.info
All these URLs will:
- serve the site from your repository, the
<username>.github.io
Github repo, namely https://github.com/juanMarinero/juanmarinero.github.io. - redirect and serve at the non-
www
domain, like https://juan-marinero.info. To serve at thewww
subdomain, like https://www.juan-marinero.info, read next section.
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
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
- DNS Standards (RFC 1034):
- The apex/root domain (e.g.,
example.com
) cannot have aCNAME
record if other records (likeMX
,TXT
,NS
) exist for the same domain. - This is because
CNAME
conflicts with other record types. GitHub Pages requires aCNAME
for verification, but apex domains needA
/AAAA
records instead.
- The apex/root domain (e.g.,
- 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 likewww.example.com
). - For apex domains, it checks for
A
records pointing to GitHub’s IPs.
- Looks for a
- If GitHub finds a
CNAME
at the apex (which violates DNS rules), verification fails.
- When you add
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).
- 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) - 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
- 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
, orREADME.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:
- https://juanmarinero.github.io/test-publishing-from-a-branch/
- https://juan-marinero.info/test-publishing-from-a-branch/
- https://www.juan-marinero.info/test-publishing-from-a-branch/
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.