Installing Hugo + the Blowfish theme from scratch - a detailed guide#
This note is a separate, standalone guide: how to deploy a Hugo site with the Blowfish theme from a blank slate. It doesn’t depend on the publishing automation guide (Obsidian → Forgejo → CI/CD1) - if your site is already running, you don’t need this note. It’s useful if you’re deploying a new site, a test bench, or want to understand what’s actually going on inside that LXC machine where your site already lives.
The guide is aimed at beginners - every command comes with an explanation.
On my setup, the site with all its files runs in an LXC container inside Proxmox, so throughout this article I’ll be working from that premise. But Hugo can be installed on any system that supports git, even Windows. I chose LXC so I could write articles from anywhere in the world, by remotely connecting to my server.
How it all fits together#
Before going through the steps, it’s worth seeing the big picture - otherwise it’s not entirely clear why this seemingly complex structure exists: a separate folder for the theme, a separate one for settings split across several files, and all of it managed through git and a submodule instead of just “dumping everything into one folder.”
The logic is simple: each part is responsible for its own thing, and this isn’t complexity for complexity’s sake, but a separation of concerns.
content/- this is what you write: the actual articles. Changes most often.config/_default/- these are the settings: what the site looks like, what the menu is, what the theme’s parameters are. Changes rarely, but when it does, it’s convenient that each aspect lives in its own file rather than a thousand-line wall of text.archetypes/- a template used to create a new article, so you don’t have to copy-paste the front matter by hand every time.themes/blowfish- and this is fundamentally different: this is not your code, it’s a separate third-party repository (the Blowfish theme), connected as a git submodule. It’s deliberately kept separate from your files - this way you can update the theme to a new version with a single command, without worrying that some of your edits got accidentally buried somewhere in its files, and vice versa - your articles and settings don’t depend at all on the theme’s internal structure.
Next, Hugo Extended takes all of this together - your content, your settings, and the external theme - and outputs a finished static site in the public/ folder. That’s the pipeline we’ll be building step by step from here.
In other ready-made Hugo themes, the project structure logic can differ.
Terminology used in this article#
- Hugo - a static site generator: it takes markdown content files and layout templates and outputs a finished HTML site (no database, no PHP - just files).
- theme - a set of layout templates and styles. Blowfish is one of the popular Hugo themes, tailored for blogs/documentation, and definitely one of the most customizable.
- Hugo Extended - an extended Hugo build with SCSS/SASS support (style compilation). Most modern themes, including Blowfish, specifically need this build - the regular (non-Extended) build won’t work.
- submodule - a repository inside a repository. A theme is usually attached as a submodule: your site references a specific version of the theme’s repository, without directly copying its files.
- frontmatter - the metadata block at the top of an article’s markdown file, usually between
---markers, where you specify the title, date, tags, etc. - page bundle - a way of structuring an article in Hugo where the article has its own folder (rather than just a single
.mdfile), with its images stored alongside the text (for examplefeatured.png). - config/_default/ - a folder with site settings split across several files by topic (
hugo.toml,params.toml,menus.toml, etc.) - this is how Blowfish organizes its configuration. - TOML - a configuration file format (similar to INI, but with its own syntax quirks).
Stage 1. Preparing the server#
If you’re deploying on a new machine (for example, another LXC container in Proxmox), start with basic setup first.
Step 1.1. Create/connect to the server#
If this is a new LXC container in Proxmox - create it with a Debian or Ubuntu template, allocate at least 1 vCPU / 1 GB RAM / 8 GB disk - that’s plenty for a static site.
Connect via SSH:
ssh root@CONTAINER_IPStep 1.2. Update the system#
apt update && apt upgrade -yStep 1.3. Install git#
apt install -y git
git --versionStage 2. Installing Hugo Extended#
Step 2.1. Why not just apt install hugo#
The standard Debian/Ubuntu repositories usually have a Hugo version that’s too old and not Extended - themes like Blowfish, and others, will simply refuse to build (errors about a missing SCSS transpiler). We install the current release manually, from GitHub.
Step 2.2. Download the current Hugo Extended release#
Open the releases page in your browser:
https://github.com/gohugoio/hugo/releasesFind the topmost release without a Pre-release label. In the file list at the bottom of the page, find the Linux archive with the word extended in its name (for example hugo_extended_0.XXX.X_linux-amd64.tar.gz) - the regular build without that word won’t work.
Don’t type the link by hand - right-click the file name and choose “Copy link address.” This eliminates the risk of a version-number typo and downloading a nonexistent file.
On the server, pasting in the copied link in place of LINK:
cd /opt
curl -L -o hugo.tar.gz "LINK"Before extracting, check that you downloaded an archive and not an error page:
file hugo.tar.gzIt should show something like gzip compressed data - if instead it’s ASCII text or HTML document, the link was broken; go back to the releases page and copy the link again.
tar -xzf hugo.tar.gz hugo
mv hugo /usr/local/bin/
rm hugo.tar.gzStep 2.3. Verify the installation#
hugo versionThe output must include the word extended - for example:
hugo v0.163.3-...+extended linux/amd64 ...If the word extended is missing - you downloaded the wrong archive, repeat Step 2.2.
Stage 3. Creating the new site#
Step 3.1. Create the project structure#
mkdir -p /home/prohomelab
cd /home/prohomelab
hugo new site . --forceBreakdown: hugo new site . --force creates the standard Hugo site structure right in the current folder (.), and --force allows this to happen in an already-existing (but empty) folder.
You’ll get folders: archetypes/, assets/, content/, data/, i18n/, layouts/, static/, and a hugo.toml file. We won’t touch this hugo.toml directly going forward - in Stage 4 it will be replaced with a multi-file structure.
Step 3.2. Initialize git#
git init
git config user.name "Your Name"
git config user.email "you@example.com"Stage 4. Connecting the Blowfish theme#
Step 4.1. Add the theme as a submodule#
git submodule add -b main https://github.com/nunocoracao/blowfish.git themes/blowfishThe theme developer’s site has instructions on connecting the theme using built-in tools. During that kind of installation, you’ll be asked a huge number of questions, and answering them gets you a ready-to-use theme - but we’re going to do it the old-fashioned way.
Breakdown of the values: -b main - track the main branch of the theme’s repository. The command will create a .gitmodules file (recording where to pull the theme from) and download the theme itself into themes/blowfish.
Step 4.2. Copy the Blowfish example configuration#
The Blowfish theme has a ready-made configuration example (the exampleSite folder), which is convenient to start from instead of building a config from scratch:
cp -r themes/blowfish/config/_default config/This will create a config/_default/ folder with several files instead of a single hugo.toml - this is what the Blowfish theme itself recommends; the configuration is organized by topic:
hugo.toml- basic site settings (title, baseURL, default languages)params.toml- theme parameters (color scheme, footer, social links, etc.)menus.<language>.toml- menu items (for examplemenus.ru.toml)languages.<language>.toml- settings for a specific language (for examplelanguages.ru.toml)markup.toml- markdown processing settingsmodule.toml- Hugo Modules settings (if you use modules instead of a submodule - not needed in our case, you can delete it or leave it empty)
params.toml is where all the theme’s look-and-feel settings live - color scheme, card and article layouts, social links, search, comments, and dozens of other parameters. This article is about installation, not styling, so we won’t go into those - Blowfish has its own detailed configuration docs covering every parameter far better than I could summarize here. From this point on it’s just a matter of taste.
Important: as soon as the config/_default/ folder appears, Hugo switches entirely to it and stops reading the root hugo.toml from Step 3.1. From here on, edit only the files inside config/_default/.
Step 4.3. Configure basic site parameters#
nano config/_default/hugo.tomlCheck/adjust:
theme = "blowfish"
baseURL = "https://your-domain.com/"
title = "Your Site Name"
locale = "ru-RU"The locale field is the current name for the parameter that sets the site’s language and region (used, for example, in social media meta tags). In older examples online you may find languageCode instead - that’s a deprecated name, and Hugo will print a warning about it during the build, so we use locale right away.
Step 4.4. Configure the language#
Open config/_default/languages.ru.toml:
nano config/_default/languages.ru.tomlAt the very top of the file (not inside [params]), check/adjust:
locale = "ru-RU"
label = "Russian"
weight = 1(If you see languageCode/languageName instead of locale/label - replace them, these are the same deprecated names as in Step 4.3.)
Step 4.5. First local run (preview)#
hugo server -D --bind 0.0.0.0 --baseURL "http://YOUR_SERVER_IP"Breakdown: -D - show drafts too, --bind 0.0.0.0 (you can specify your container’s specific IP) - listen on all network interfaces (by default Hugo listens only on 127.0.0.1, and you can’t reach it from another computer on the network), --baseURL - what address to reference within the generated pages.
Open http://YOUR_SERVER_IP:1313 in a browser (the Hugo dev server listens on port 1313 by default).
If the page doesn’t open, but the command in the terminal ran without errors - port 1313 is likely blocked by a firewall (on the LXC container itself, on the Proxmox host, or on your network’s edge router/firewall). Temporarily open port 1313 for test access, or connect to the preview only from the same machine via an SSH tunnel (ssh -L 1313:localhost:1313 root@CONTAINER_IP, then http://localhost:1313 in your own browser).
The Blowfish theme’s starting page should open (still empty, no articles). Stop the server with Ctrl+C in the terminal.
Stage 5. Content structure (matching your article format)#
I’ll walk through everything using my own site as an example, but of course you may have your own structure. My established format has articles set up as page bundles (a folder per article with a slug inside, plus a featured.png next to it), and the front matter includes a specific set of fields. Let’s document this here so the format stays predictable when creating new articles manually (without Obsidian) or when deploying a copy of the site.
Step 5.1. Create an archetype (new article template)#
An archetype is the boilerplate that Hugo fills in when you create a new article with the hugo new command. Edit archetypes/default.md:
nano archetypes/default.mdEnter the front matter template for your format - mine looks like this, for example:
---
title: "{{ replace .File.ContentBaseName "-" " " | title }}"
published: {{ .Date }}
pinned: false
description: ""
tags: []
slug: "{{ .File.ContentBaseName }}"
categories: ""
licenseName: "CC BY 4.0"
author: "your name"
draft: true
series: ""
toc: true
showDate: true
showDateUpdated: true
showReadingTime: true
showAuthor: true
cover: ./featured.png
summary: ""
---This template mirrors the front matter structure I use (without separate date/pubDate fields - the publication date comes from published).
Step 5.2. Create a new article as a page bundle#
hugo new content/posts/Selfhosting/My-New-Article/index.mdThis creates a content/posts/Selfhosting/My-New-Article/ folder with an index.md file inside it, formatted according to the archetype from Step 5.1. Put the cover image in the same folder under the name featured.png (or change cover: in the front matter to a different file name).
Step 5.3. Categories and tags#
In my current structure, each top-level category (content/posts/Traefik/, content/posts/Proxmox/, etc.) has an _index.md file - it describes the category/section itself (not an individual article), for example setting the section title and a featured.png/featured.webp cover for the category’s listing page. It’s created the same way, manually:
mkdir -p content/posts/NewCategory
cat > content/posts/NewCategory/_index.md <<'EOF'
---
title: "Category Name"
---
EOFStage 6. Production build#
Step 6.1. Build the site#
cd /home/prohomelab
hugo -D --minifyBreakdown: -D - include drafts in the build (remove this flag once you want to publish only fully-finished articles), --minify - compress the resulting HTML/CSS/JS.
The result appears in the public/ folder - that’s the finished static site, ready to upload to any hosting. Inside you’ll find plain HTML/CSS/JS files and images, one folder per page/article, plus a couple of housekeeping files worth checking separately.
Step 6.2. Checking sitemap.xml and robots.txt#
These two files directly affect how search engines see your site - and they can behave in ways you don’t expect if you don’t know what Hugo does by default.
sitemap.xml is generated automatically, no configuration needed - Hugo drops it into public/sitemap.xml on every build, using the embedded sitemap template, listing every page on the site. Check that it showed up:
cat public/sitemap.xml | head -20If you need to exclude a specific article from the sitemap (a utility page, say) or change its priority/update frequency, that’s done through the article’s own front matter, not through the global config:
sitemap:
changefreq: weekly
disable: true
priority: 0.8Note the nesting - changefreq/disable/priority are fields inside sitemap:, not a single line like sitemap.disable: true. A dot in a YAML key name doesn’t turn into a path to a nested field - it’s just a literal key name, and Hugo won’t pick it up that way.
robots.txt, unlike the sitemap, isn’t generated at all by default - the enableRobotsTXT setting defaults to false, and without it there simply won’t be a robots.txt in public/. Two ways to get one:
- Turn on the built-in template - set
enableRobotsTXT = trueinconfig/_default/hugo.toml. It outputs a minimal file along the lines ofUser-agent: *(i.e. allow crawling everything) - fine if you don’t need any specific rules. - Write your own file - drop a
robots.txtintostatic/robots.txt. Anything instatic/gets copied intopublic/as-is, with no templating - handy if you need specific rules (say, blocking/drafts/from indexing, or pointing at a non-default sitemap path).
Don’t mix the two approaches - either enableRobotsTXT = true with the built-in template, or your own file in static/robots.txt. Turn on both at once and Hugo will print a Duplicate target paths warning for robots.txt during the build - it’s trying to put two different files with the same name into public/: one generated from the template, one copied from static/.
Before your first real deploy, open both files by hand and make sure they aren’t blocking search engines from the entire site - especially if you copied the config over from a dev/staging setup where crawling might have been deliberately disallowed.
Step 6.3. Local check of the built site#
If you want to see the actual built (production) version rather than through the dev server:
cd public
python3 -m http.server 8080Open http://SERVER_IP:8080 in your browser (the same firewall caveat from Step 4.5 applies here too). Stop with Ctrl+C.
What’s next#
At this point the site is deployed and able to build. Next, you need to decide how you want to publish your site.
I used to upload the public/ folder to hosting manually (via FileZilla). But there are far more options than that - this is a decision you’ll need to make yourself, checking the official documentation for the theme and the engine.
P.S. A couple of things worth thinking about separately, not covered in this guide:#
Backups: the Hugo repository itself is version-controlled in git, but the generated
public/isn’t (we exclude it via.gitignore) - that’s fine, it can be regenerated from source at any time.HTTPS/TLS for the local dev server - not needed,
hugo serveris meant only for local preview, not public access.Updating the theme: every so often it’s worth updating the theme submodule to a newer version:
cd themes/blowfish git pull origin main cd ../.. git add themes/blowfish git commit -m "Update Blowfish theme"Before updating, it’s worth checking the theme’s changelog on GitHub - sometimes configuration field names change (as in the
languageCode/localeexample above), and after updating you may need to adjustconfig/_default/*.toml.
at the time this article was published, the Obsidian guide hadn’t been published yet ↩︎




