How to write a Hugo theme, and why it can be worth writing one even if you’re the only user

If you have more than one Hugo site, it can be worth making a theme, even if you’re the only user. But you’ll want to read these tips first.

Ron Erdos • Updated January 21, 2021

Why it can be worth making a theme, even if you’re the only user

If you have more than one Hugo site, it can be worth making a theme, even if you’re the only user.

That way, whenever you make an improvement or fix, you make it in your theme repo and then pull those changes into your Hugo site repos.

I have a few sites, and all but one use the Hugo SEO theme I created. I much prefer this setup to copying and pasting code between Hugo repos like I did for many years.

Let’s get into the details.

Repository structure

I recommend a dedicated repo for your theme, and another dedicated repo for your Hugo site. Then you can add the theme to your Hugo repo using git submodule, which allows you to nest one repo inside another. Here are the steps in more detail:

Step 1: Create a dedicated repo for your theme

If you don’t already have a dedicated repo for your theme, create one. For example, my SEO theme for Hugo websites is in a private GitHub repo named seo-theme-hugo.

Step 2: Make another repo for your Hugo site

If you haven’t already, create a repo for your Hugo site. For example, the code for this website (moonbooth.com) is in another private repo called moonbooth.

Step 3: Delete the themes folder that ships with Hugo

If you don’t have any themes yet, this folder will be empty and you can delete it without consequence.

If you do have one or more themes in there, cut the whole /themes/ folder and put it somewhere else for now. We can add the other themes back later.

Step 4: Run the git submodule command

Open a terminal and cd to the root of your Hugo repo. Run this command:

git submodule add https://github.com/THEME_AUTHOR_USERNAME/THEME_REPO themes

Voila! The /themes/ folder will be recreated, but this time, it will have the desired Hugo theme inside as a nested Git repo.

Step 5: Create a .gitmodules file

Create a .gitmodules file in the root of your repo with the following code:

[submodule "themes"]
	path = themes
	url = git@github.com:<USER>/<THEME_REPO>.git

Step 6: If your theme repo is private, add a public deploy key to your Hugo site repo

If your theme repo is private, you’ll need to add a public deploy key from the Hugo site repo to the theme repo. This gives the repo for your Hugo site access to your theme repo.

Here’s how to generate a public deploy key on Netlify, the host I use.

To add the public deploy key to your theme repo, visit the repo containing your theme on GitHub.com. Next, go to Settings > Deploy keys. At the top right, you’ll see a button labelled Add deploy key. Click that, and you’ll be taken to a form where you can paste in your deploy key. You can also give it a title (such as the name of the site/repo consuming the theme) for easy reference. You’ll need to repeat this process for each site you want using your theme.

Step 7: Reference the theme in your config file

In your config file (usually /config.toml), add this line without any indents:

theme = "YOUR_THEME_NAME"

This will tell Hugo to go ahead and actually use the theme.

CSS

My initial approach, and the problems it created

When I first started writing my SEO theme, I set it up so the user could add extra CSS styles which would appear in a custom stylesheet. The theme stylesheet was named theme.css and if the user wanted additional styles, they would create a file called custom.css and put them in there. As soon as the user created custom.css, my conditional logic would notice and automatically add a link to it from the <head> section, to go alongside the hard-coded link to theme.css.

So far, so good—the only problem was that this meant there were two stylesheets for the end user to download, with the second (custom.css) likely often containing only a few CSS rules. It seemed pretty wasteful to me.

Another problem with this approach was that if the user wanted to change theme colours, they would have to overwrite several CSS rules to do so. For example, the accent colour in this theme appears in the border for breakout boxes (technically <aside> elements), table borders, <code>, input backgrounds and submit button borders and hover fills. I needed to find a more elegant method for the user to change the theme colours—one of the most basic tasks of any theme.

Enter SCSS

I had read about SCSS long ago, but never used it in production. For the uninitiated, SCSS is a superset of CSS3 that lets you use partials and variables, amongst other features. By switching from CSS to SCSS, I was able to use partials and variables to solve the problems I mentioned earlier. Let’s go over each in turn.

Using SCSS partials to inject custom user styles into the theme stylesheet

Partials, the first feature I mentioned, allows you to inject SCSS code from one file into another—much like Hugo partials do.

In my new (and current) solution, I have the user add their custom styles to an SCSS partial called _custom.scss. They can write CSS, SCSS, or both in that file.

Then, at the bottom of the theme stylesheet, theme.scss, I inject the user’s custom styles. It looks like this:

// (styles not shown for brevity)

@import "custom";

I ensured the user’s style rules would take precedence over the default theme styles by placing the @import "custom"; at the bottom of the theme stylesheet. This harnessed the “last rule wins” principle in CSS, where later rules override earlier ones.

Variables

Earlier, I mentioned that I wanted an easy way for the user to change the theme colours. In my theme’s current setup, I achieved this by using an SCSS variable for each colour. Let’s take a look.

All the style variables live in an “SCSS partial” at /themes/assets/scss/_variables.scss. They look like this:

$color-accent: red;
$color-background: black;
$color-gray: #ccc;
$color-text: white;
$increment: 10px;
$wrapper-max-width: 700px;

Then I use @import to call these variables at the top of themes.scss. I’ve also include a bit of SCSS code so you can see one of the variables in use:

@import "variables";

button {
    border: 1px $color-accent solid;
{

// (other styles not shown for brevity)

@import "custom";

The above SCSS will compile to a CSS file which will set the border of buttons to 1px solid red (red being the value of $color-accent in _variables.scss, as above).

Above, I mentioned how using SCSS variables made changing theme colors a lot simpler for the user. However, you can use SCSS variables for other things too, as I’ve done above. For example, you can set your content column width using an SCSS variable, which means the user can change it without having to create an additional style rule. It’s more efficient for the browser that way, rather than setting and resetting the width of the content column, in this example.

JavaScript

I think the cleanest way to handle JS in Hugo themes is as follows. If your theme depends on JS (mine doesn’t), include it. For example, you might put your theme JS in /static/scripts/theme.js, and link to that:

<script src="/scripts/theme.js"></script>

Then, to allow the user to add their own JS without having to create a custom <head> section, simply add the following code (or similar) just below where you linked to the theme JS:

{{ if (fileExists "static/scripts/custom.js") }}
	<script src="/scripts/custom.js"></script>
{{ end }}

Let’s walk through the above code. If the user has created a custom JS file at a location we specify—I chose /static/scripts/custom.js, but you can obviously change this—then create the link.

However, if the user doesn’t need JS, then no link will be created.

Said another way, using an if statement paired with Hugo’s in-built fileExists function spares sites from linking to a non-existent JS file.

"Thanks so much for your work ... I'm migrating my WordPress blog to Hugo and it's been really helpful" — Francisco S., engineer and blogger

The planets in our solar system