How I built this web site - part 1
By JF Poilprêt
Introduction
This post is the first of a series of 3 that describe my experience with Hugo to create this web site:
- Part 1: Fundamentals & Practice
- Part 2: Advanced Customization & Improvements
- Part 3: Deployment
Why do I need to write a post about how I setup this blog?
Probably because:
- this was not my first attempt to create a blog, but previous one failed
- I wanted to “learn something new” and after a few months, I feel the need to share what I’ve learnt, in case someone would like to do the same
This series will explain:
- why I chose Hugo for my blog
- how I set it up from scratch
- how I learned about Hugo configuration, usage, patterns and idioms, step by step
- what I had to do (CSS, Tachyons usage) to improve my site layout according to my wishes
- how I setup my VPS as server for my blog with nginx
- what shall come next in building and improving my web site
Quick History of my Blog
I had started writing a blog more than 2 years ago, during last quarter 2022:
- I wanted to write about hiking (which I discovered earlier that year) in Switzerland
- I needed to “amortize” the VPS I had rented for a couple of years already
- I wanted a place where I could put my résumé
Also, I wanted:
- a lean system (no database, no off-the-shelf CMS) that I could directly publish to apache or nginx
- a simple way to write new pages, and blog posts in particular
- I wanted to “learn something new”
Late 2022, I opted for a “Static Site Generator”, there were already many on the open source market; however, at that time, I used the wrong selection criteria for such a tool:
- Which programming language it is developed with
- How much time it takes to build your first (extra simple) site with it
But I let alone important criteria like:
- ease of use
- ease of internationalization
- documentation (tutorials, reference, completeness)
- available themes
- possible customization
- community
Thus I had selected Pelican for the sole reason that it was written in Python, and that Python is one of my favorite programming languages!
After several weeks spent on it, I got a few pages published to my VPS but:
- the site was ugly (and hard to customize in terms of style, colors, layouts)
- customization was not as easy as I expected
- creating posts was not as straightforward as I would have liked
I kept this site as-is for 2 years, until I decided, this Summer, that it was time to “resurrect” it, like a phoenix reborn from its ashes.
But first, I had to select a new tool, according to the right criteria this time!
Why Hugo
Based on my new criteria (listed above), I performed a quick check of “2024 best static site generators”, and I quickly retained Hugo in the list of candidates, because:
- it is well documented
- it has plenty of features, including out-of-the-box internationalization support
- it has a bunch of tutorials on almost all features (including advanced ones)
- it has a lot of various themes of all kinds, some quite good-looking
- it seems to offer a lot of customization (but themes also need to be customizable)
- it has a broad community (forums, Stack Overflow, blogs…)
The selection of Hugo was performed in parallel with the best theme to choose for my blog. I chose Ananke because:
- it looked cool and beautiful
- it seemed easy to use and customize
Also, in the list of available themes, a few are dedicated to publish your Résumé, I selected Almeida CV which looked good, supported internationalization too, and provided a good level of customization.
Do note that the fact Hugo is written in Go programming language, which I do not know at all, was a not a showstopper (but it had been, back in 2022…)
First setup
Install Hugo
For my Desktop PC, I use Linux Fedora distribution (version 39 currently) along with XFCE desktop environment (which I like because it is lighter than most other “common” desktops such as Gnome).
Fedora distributions use dnf
as package manager/installer along with several official repositories.
Initially I wanted to install Hugo with dnf
(which I did) but I noted that:
- available version for Fedora 39 was 0.111, while latest Hugo release at that time was already 0.128
- Hugo comes in 2 flavours: “standard” and “extended” (honestly, I find the difference between both is not well documented) and only the standard version is available with
dnf
It turned out, after just a few days working with standard Hugo 0.111, that:
- I faced bugs that were known and already fixed in more recent releases
- I needed some of the features available only to the extended version
That is why I finally installed the latest extended Hugo package at that time.
I directly installed it (just gunzip
and tar xvf
) in my home directory and made a symbolic link to it from /usr/local/bin
:
$ sudo ln -s /home/jfpoilpret/hugo/hugo /usr/local/bin/hugo
Note that I have performed a few updates since, and today I use Hugo 0.135.0.
Setup a new site
First I setup a new site on one of my home subdirectories “~/webdev
”, for that I followed this tutorial:
$ hugo new site hugoweb-blog
$ cd hugoweb-blog
$ git init
$ git submodule add https://github.com/theNewDynamic/gohugo-theme-ananke.git themes/ananke
$ echo "theme = 'ananke'" >> hugo.toml
$ hugo server -D &
The above commands:
- create a new
hugoweb-blog
directory with a “skeleton” Hugo site: complete directory structure, default configuration file (hugo.toml
)… - install the Ananke theme from github into my new Hugo site
- launch the server locally (for live viewing from a browser while changing settings, adding or modifying posts)
To better learn about Hugo, I also used this video tutorial which is quite complete, but a bit outdated; this was still very useful anyway.
Then I created a new private repository on github so I can have version control and issues management for my web site.
First Hugo commands
Hugo comes with a certain number of commands that you can use in various situations. During post creation you shall use one of:
hugo new content hiking/second-ride/index.md
: this creates a new content post, underhiking
category, with the name (found in URL path)second-ride
, in the form of a “page bundle”, the meaning of that will be explained in a minute.hugo new content hiking/second-ride.md
: does the same as above, but without a page bundle; I have exclusively used this command initially but that was a bad idea, page bundles should be preferred in most (if not all) situations, because they allow embedding related images at the same location as your post content
As you may have noted from the .md
extension, Hugo enables you to use Markdown for writing posts content (no HTML code, no complex language to learn; in general, I find Markdown very easy to learn, read and write). That made me very proficient for writing all my first blog posts.
Hugo uses CommonMark implementation of Markdown “standard”.
To get started with Markdown, here are a few useful links:
- A very basic cheatsheet
- A more complete basic reference to Markdown
- An online CommonMark editor with live view
- The complete CommonMark specification
- Useful Markdown extensions available to Hugo
First content
Now was time to write my first post! This was a simple “About me” post, in French language (as a reminder, my intent was to create a bilingual blog, with posts in French, English or both).
Before that, I first had to define where (in what category) to put it. I decided to create a “home
” category for all “general” stuff on my web site, typically, the about page and also legal mentions.
In Hugo, you create a content
subdirectory for each category of your site, then you can add posts to any category.
Using the 1st of 2 commands hugo new content...
mentioned above, I created the “About me” post:
$ hugo new content home/about/index.fr.md
This command created the following:
- the directory
content/home/about
- the file
index.fr.md
within this new directory
Do note the “fr
” just before the “.md
” extension: it tells Hugo this content is in French, it allows several languages for the same post. More details about internationalization later.
Hugo generates this post content file with some “content” at the beginning (called “front matter”) which constitutes metadata for this post:
---
title: "About"
date: 2024-07-09T13:55:23+02:00
tags: []
featured_image: ""
description: ""
---
This section, delimited by ---
lines, is using YAML to define metadata for your content. Content shall be added after this section.
title
: will be used in several places in the blog site, defaults to the name of the child directory (about
in my example), capitalized; you can change it to a better title.date
: the creation date, initialized with current timetags
: an empty list of tags that you can modify to tag your post for better searchfeatured_image
: an empty string you can replace with the path to an image that will be used in several places in the blog sitedescription
: an empty string you can replace, and that will be used in the header part of your post
There are other metadata, and you can even specify your own if you need.
In particular, the following attributes are worth knowing:
draft
: boolean value to mark the post as draft, meaning that it won’t get published when creating your site with Hugo; however, it is still viewable with local Hugo server if you provided it with--buildDrafts
(or-D
) argument. As a good rule of thumb, always adddraft: true
for every new post and set tofalse
when you deem it is ready for prime time.publishDate
: page will not be published until that date when creating your site with Hugo; however, it is still viewable with local Hugo server if you provided it with--buildFuture
(or-F
) argumentcategories
: a list of categories this post “belongs” to
You should immediately fill this front matter part:
---
title: "À propos de moi"
date: 2024-07-09T13:55:23+02:00
categories: ["famille"]
tags: ["vie-privée"]
featured_image: "images/IMG_1846.JPG"
description: ""
draft: false
---
Notice the “featured_image
” property: it contains a relative path to an image I put in “content/home/about/images
”; the relative path starts at the root directory of the current post. After several weeks working with Hugo, I find creating an “images/
” subdirectory for each post (if it contains images of course) is a good habit and I try to stick to it whenever possible. This practice is based on “Page Bundles” which I’ll describe later in this post.
Now you may wonder: “Where does this initial content in my new post come from?” The answer is in Hugo concept of “archetypes” which are templates used to create new content.
Typically, each theme comes with one (or more) such archetypes, but you may add your own for specific needs, e.g. if you produce a series of posts that always have a similar structure and specific metadata.
Finally the hardest part comes: write the actual content of the first post! Fortunately, the “about me” is quite short as I did not want to tell the whole story of my life but only the most important facts.
All content is written using Markdown which mainly looks like text with some enhancements:
- headers
- bulleted or numbered lists
- italic, bold, strikethrough …
- links
- images
- …
Here are concrete examples of such Markdown style inside content:
Headers
1st Level Title
===============
2nd Level Title
---------------
# 1st Level Title
## 2nd Level Title
### 3rd Level Title
Lists
- bulleted list item 1
- bulleted list item 2
- bulleted list item 3
1. numbered list item 1
2. numbered list item 2
3. numbered list item 3
Font styles
**italic**
*bold*
~~strikethrough~~
Links
[Link Name](Link URL)
Images

In links and images examples above, “URL” may actually be a relative path to a resource (other post or local image) rather than a real Internet URL.
Small configuration improvements
After just a few days working with Hugo, I quickly made a few improvements to the global configuration.
Configuration of your Hugo site is stored at the root directory as one unique file: “hugo.toml
”. This file can be expressed in one of 3 flavors (formats):
- TOML: this is the default when creating a new site with Hugo
- YAML: which has become quite common in the recent years
- JSON: which is a quite old format, a bit too verbose from my viewpoint
I naturally prefer YAML, which I am used to, and I find more concise and readable than other 2 formats.
That is why I quickly transformed “hugo.toml
” into “hugo.yaml
”:
Before: hugo.toml
title = "JF Poilpret: mon blog"
baseURL = "https://www.poilpret.ch/"
languageCode = "fr-ch"
theme = "ananke"
After: hugo.yaml
title: "JF Poilpret: mon blog"
baseURL: https://www.poilpret.ch/blog
languageCode: fr-ch
theme: ananke
Later on, I have further added configuration to hugo.yaml
:
title: "JF Poilprêt: mon blog"
copyright: "JF Poilprêt"
baseURL: https://www.poilpret.ch/blog
languageCode: fr-ch
theme: ananke
defaultContentLanguage: fr
paginate: 6
enableRobotsTXT: true
enableEmoji: true
params:
author: JF Poilprêt
date_format: 2 January 2006
favicon: "images/jfpoilpret3.jpg"
recent_posts_number: 5
ananke_socials:
- name: rss
- name: mastodon
url: https://mamot.fr/@jfpoilpret
rel: me noopener
- name: github
url: https://github.com/jfpoilpret
- name: linkedin
url: https://www.linkedin.com/in/jean-fran%C3%A7ois-poilpret-b0245320/
- name: stackoverflow
url: https://stackexchange.com/users/15857/jfpoilpret
- name: facebook
follow: false
- name: twitter
follow: false
markup:
goldmark:
extensions:
extras:
delete:
enable: true
insert:
enable: true
mark:
enable: true
subscript:
enable: true
superscript:
enable: true
Some of these new attributes are:
- specific to Ananke theme:
paginate
,recent_posts_number
,ananke_socials
- advanced Hugo configuration:
enableEmoji
: enables graphical emoji within rendered markdown contentmarkup.goldmark.extensions
: enables specific Markdown that I used in some later posts
The latest hugo.yaml
for my site, as of today, can be found here.
Taxonomy & Site Structure
Quick Introduction
One very important point when building a web site with Hugo (or with any other tool actually) is to define a proper taxonomy, along with the proper structure of your site, which will depend on this taxonomy to some extent.
Taxonomies are a way to classify content. By default, Hugo (and many Hugo themes) supports two taxonomies that are very common to blog sites:
- categories
- tags
In each content of your site, you then specify terms for each taxonomy, in its front-matter, like in the examples above:
title: "À propos de moi"
categories: ["famille"]
tags: ["vie-privée"]
In addition, Hugo defines “sections” which are also a way to classify content. Contrarily to taxonomies, where a post can be classified with multiple values (one post may possibly belong to several categories and have several tags), sections impose a tree-like structure of three levels or more:
root (not a section per se)
section
[sub section level 1]
[sub section level 2]
...
posts (tree leaves)
Sections are reflected in “content/
” (the root level) directory content. Sub-sections if defined are simply subdirectories of section directories, they are named “nested sections” under Hugo vocable.
For simplicity reasons, I would advise to follow the rule “1 section = 1 category”, as I do not see real added value otherwise.
Generally, taxonomies for your site will have an impact on:
- organization (directories) of your content
- URL to access specific content on deployed site
- URL to access content list on deployed site (per taxonomy: e.g. list of categories or tags and posts in each…)
I did not take time to dig further into Hugo features regarding taxonomies (much configuration is possible), and I just used defaults for it.
For useful (and deep) information about Hugo sections, I suggest reading the “Ultimate Guide to Hugo Sections” but this is advanced as it discusses customization.
Conventions
What is important when building your site is to:
- have a clear vision of sections you will use
- have a clear vision of categories you will use; as mentioned above, in my site, I decided they will be equivalent to sections and thus used to name subdirectories to my site
content/
directory, then all content for one category should be located underneath. - have a good vision of tags (this can be refined while writing new content, though) to ensure tags are consistently named and used across all related content
In general, I suggest to use a few “minimal” conventions (for categories and for tags) and never derogate to these:
- naming conventions: casing, abbreviations usage
- allow multiple tags with same meaning or not
- possibly specialize conventions per language if it makes sense
In my blog, I use the following conventions:
- for categories
- all lowercase
- digits allowed
- accented characters are allowed in French
- use hyphen
-
to isolate distinct words (e.g. “information-technology”) - only use nouns, possibly with adjectives; verbs allowed only in “-ing” form (English), or “-ant” (French participe présent), e.g. “hiking”
- do not use abbreviations, use complete terms only
- for tags
- all lowercase (even for brand names, e.g. a “Hugo” post shall have tag “hugo”)
- digits allowed (e.g. hiking year “2024”)
- accented characters are allowed in French, e.g. “vie-privée”
- use hyphen
-
to isolate distinct words - only use nouns, possibly with adjectives; verbs allowed only in “-ing” form (English), or “-ant” (French participe présent)
- avoid using abbreviations, prefer complete terms, use them only if it makes sense and they are understood by your audience
- use synonyms tags only if that brings value and then, use them consistently, i.e. if you use “tag1” and “tag2” with the same meaning, then all posts having “tag1” shall also have “tag2”, and reciprocally
Structure

Hierarchy of my Hugo site content directory
In the web site source content directory, we see one subdirectory per category:
- electronics,
- hiking,
- home,
- information-technology,
- life
Under each directory we can find:
- post content directory
- other intermediate subdirectories (e.g. hiking years)
Published structure is similar:

Hierarchy of my Hugo site public directory
public/
directory is generated by Hugo and can directly be published to a web server. In addition to subdirectories per category (sections), it also contains:
- a
categories/
subdirectory, listing all french categories, and within each, anhtml
file listing all posts in the category - a
tags/
subdirectory, listing all french tags, and within each, anhtml
file listing all posts with that tag - an
en/
subdirectory, containing the same public root structure but only for english content
Useful Hugo related concepts
In Hugo, each section may have a kind of header page, e.g. introducing the kind of content found in that section.
This is achieved by adding an “_index.md
” (and “_index.en.md
” if this is a multilingual section) directly in the section directory.
Such Markdown file enables you to:
- define default metadata (through its front matter) for all content within the section, thanks to the “
cascade
” attribute - add some introductory content (as Markdown) for the whole section
For example, for my “hiking” section, I have the following “_index.md
”:
front matter
---
title: "Randonnées"
date: 2024-07-18T12:00:00-05:00
cascade:
featured_image: "background-haut-de-caux.jpg"
---
Here we can see that, for any post without a “featured_image
” defined, the default image will apply.
markdown content
Je décris ici la plupart des randonnées que j'ai effectuées depuis 2022.
Chaque billet contient un synopsis résumant les caractéristiques de
la randonnée.
Les niveaux de difficulté et la durée sont prises pour des
"randonneurs moyens" comme moi (pas chevronnés).
Pour un résumé, année par année, des randonnées que j'ai effectuées,
allez voir [ici](/hiking/all-hikes).
This is normal markdown that will be rendered by Hugo to generate a proper section “landing page”, with this content, and the list of posts (summaries) in it.
One useful aspect of Hugo sections is the possibility to define so-called “headless” sections. This is useful e.g. when you have some content, without an explicit “landing page”, that you want to link directly from some specific parts of your site.
In my situation, I have used a “home
” headless section, which contains the following content (in French and English):
- about me
- legal notice
The “About me” page is directly accessed from the root page of my blog, while “Legal Notice” is linked from the footer of every page of my blog.
Making a section headless is directly done in the front matter of its “_index.md
”:
---
title: "Home"
date: 2024-07-18T12:00:00-05:00
headless: true
---
Resources & Page Bundles
When creating posts with Hugo, several options exist about where to store related content, such as images or documents, linked from a post:
- put them in “
static/
” directory (or subdirectories) - put them along the post Markdown source itself, this is called “Page Bundles”
After writing several posts with many images in each, you will find out “static/” approach does not scale well.
Quickly, I have opted for the “Page Bundle” option because it allows me to have everything related to one post in only one location, which just makes sense!
Using page bundles requires you to use the following command line to create new content: hugo new content hiking/second-ride/index.md
that creates a directory for your post, which will be used as the page bundle storage location.
Having said that, I generally do not mix the post content files (with .md
extension) with linked images or documents, I prefer creating subdirectories within the page bundle, e.g.:

Page Bundle for one hiking post within content directory
It is also possible to put common resources outside page bundles, for instance, in the screenshot above, there is a “gpx/
” directory directly within “hiking
” section: here I put all GPX for all my hikes. Each GPX may be referred to from 2 locations:
- a hiking post for that hike (which may not exist for all my hikes)
- the post summarizing all my hikes
Finally, if you have resources (images typically) that are common to all your site, you can put them in “static/” directory:

Content of 'static' directory
Here I use “static/
” for:
- flag pictures used to display available languages (visible in all site pages)
- personal picture used as favicon
Internationalization
I have created my web site with multilanguage support in mind, I wanted to:
- create posts in both English and French
- create posts in French only
- create posts in English only
Multilingual support was one of the reasons I chose Hugo in the first place; this support comes out of the box and it looked simple to use. Also, most themes available for Hugo do also include multilingual support.
Enabling multiple language in a site built by Hugo starts with configuration:
languageCode: fr-ch
defaultContentLanguage: fr
languages:
fr:
languageName: Français
title: "JF Poilprêt: mon blog"
en:
languageName: English
title: "JF Poilprêt: my blog"
Here is a short explanation for each attribute:
languageCode
: this is used for RSS generation and<html lang="xxx">
in generated HTMLdefaultContentLanguage
: the default language of the web sitelanguages
: the list of languages (language codes) supported by the web site; each language then contains a list of specific properties:languageName
: the full language name that will be used in the language selection menutitle
: the title of the web site translated to the languagecontentDir
(not shown): see belowparams
(not shown): this allows to define additional language-dependent parameters you may use in Hugo templates; templates will be described in the next post.
Hugo provides 2 ways to define multilingual content:
- translation by file directory: here you define one root content directory per language you want to support, namely
content/fr/
andcontent/en/
if you want your site to support both French and English; this also means that you can have totally different organization (structure) of your content, just a bit like you have two independent web site (although Hugo will normally be able to “link” related posts in 2 languages). - translation by file name (I prefer that way): here you have only one content structure for your site, the language is determined by the name of your content (
.md
or other resources); for instance, a post in English would be namedmy-post.en.md
, while the same post in French would bemy-post.fr.md
; the advantage is that all content for a post is at the same location whatever the language, which makes sense, I think. Note that, for content in default language, you may omit the language code in the file name, e.g.my-post.md
would be equivalent tomy-post.fr.md
iffr
is your site default language.
Selection of one among these 2 ways is determined by the contentDir
attribute existence.
With regards to the structure of your site once published, Hugo will:
- generate subdirectories for all languages
- for all languages but default, generate site structure with all content
- for default language, generate
index.html
, in matching language directory, redirecting to root URL - at root level, generate whole site structure for default language

Hugo site public with multilingual subdirectories
Optionally, you may tell Hugo to generate subdirectories, with all site structure, for all languages including default language.
This can be done with the configuration option defaultContentLanguageInSubdir
. Although this may bring some balance to your published site, I prefer keeping the default false
, but don’t ask me why!
Caveat: if you publish a post in any language but not the default one, then page bundle resources shall also include the language suffix, e.g. mypicture.en.jpg
otherwise they will not get published!
So far, we have seen how to internationalize your content, but it is useful to know that most Hugo themes also come with internationalized templates.
All text used by themes is normally put in files (one per language) in i18n/
theme directory:

Ananke theme i18n translations
Here is an excerpt of en.toml
for Ananke theme:
[more]
other = "More"
[allTitle]
other = "All {{.Title }}"
[recentTitle]
other = "Recent {{.Title }}"
[readMore]
other = "read more"
You may override any of these (or add your own language if not supported by your theme), by simple adding such a file to your Hugo project i18n directory:

Site overridden i18n translations
Here is an excerpt of fr.toml
for my web site:
[builtWithHugo]
other = 'Ce site a été construit avec'
[builtWithAnanke]
other = 'et utilise le thème'
[lastModified]
other = 'Modifié le '
[pageTitle]
other = "Page {{ .Name }}"
In this example, I have:
- modified french translation of existing Ananke “
pageTitle
” (the original was not correct to my viewpoint) - added new labels, not used by Ananke theme, but used by my own layout templates as I will present in the next post
Next Posts
This is the end of this first post about using Hugo to build my web site.
In the next installment “Advanced Customization & Improvements” I will:
- show how to modify Markdown rendering of images
- describe how to perform image compression, with Hugo, to optimize page loading time
- demonstrate how to define “short code” for specific needs
- explain how to modify and improve default layout produced by Ananke theme
- summarize a few tips and tricks I collected along my personal Hugo learning curve
- list future planned work on my web site
The third and last post “Deployment” will be about:
- managing multiple Hugo sites (with different themes)
- site hosting server configuration
- server deployment scripts