Setting up TailwindCSS with PostCSS in Hugo
Initial Setup
First, make a new hugo app:
hugo new site hugotails
Next, create a new theme
hugo new theme tailwind-theme
After we create the theme, we need to tell hugo to use it. To do this, let’s update our config.toml file like so:
baseURL = "http://example.org/"
languageCode = "en-us"
title = "My New Hugo Site"
theme = "tailwind-theme"
After the new theme is created and we’ve told hugo to use it, we’ll want to initialize an npm project so we can add our dependencies for managing the css.
Adding JavaScript Dependencies
npm init -y
Now we want to actually add the dependencies:
"devDependencies": {
"autoprefixer": "^10.1.0",
"postcss": "^8.2.1",
"postcss-cli": "^8.3.1",
"postcss-import": "^13.0.0",
"tailwindcss": "^2.0.2"
}
These are the versions as of this writing. You can install current versions yourself by running
npm i --save-dev autoprefixer postcss postcss-cli postcss-import tailwindcss
Configuring Tailwindcss with postCSS
With the dependencies installed, we’ll want to set up initial configuration. For this, we’ll need to create a couple of directories inside of our theme folder. Then, we’ll add 3 files.
mkdir -p themes/tailwind-theme/assets/css
touch themes/tailwind-theme/assets/css/postcss.config.js
touch themes/tailwind-theme/assets/css/styles.scss
themes/tailwind-theme/assets/css/tailwind.config.js
themes/tailwind-theme/assets/css/postcss.config.js
In the postcss.config.js, we’ll need to configure our plugins and tell postcss where the theme directory is. We also want to instruct postcss to use the custom tailwind configurations that we add to our tailwind.config.js
later on. For now, we’re just going to create this file and leave it empty, but we’ll definitely be adding to it later. You can read more about how to customize tailwind in the offical docs.
// themes/tailwind-theme/assets/css/postcss.config.js
const themeDir = __dirname + '/../../';
module.exports = {
plugins: [
require('postcss-import')({
path: [themeDir]
}),
require('tailwindcss')(themeDir + 'assets/css/tailwind.config.js'),
require('autoprefixer')({
path: [themeDir]
})
]
}
themes/tailwind-theme/assets/css/styles.scss
Next, we’ll import the base, components and utilities modules from tailwindcss in the styles.scss file:
/* themes/tailwind-theme/assets/css/styles.scss */
@import "node_modules/tailwindcss/base";
@import "node_modules/tailwindcss/components";
@import "node_modules/tailwindcss/utilities";
themes/tailwind-theme/assets/css/tailwind.config.js
Finally, we’ll finish out the configuration process by adding in the (currently) empty boilerplate for our tailwind.config.js
file.
// themes/tailwind-theme/assets/css/tailwind.config.js
module.exports = {
theme: {
extend: {
}
},
variants: {
},
plugins: []
}
Testing out Tailwind in our Hugo Templates
To get this working in a way where we can see it in action in our hugo server, we have a few more tasks to complete. I’ll talk about a potential gotcha that may come up for newcomers to hugo at the end of this section. First, let’s take a look at the files we’ll be changing to demonstrate how we add tailwind to our existing hugo templates.
themes/tailwind-theme/layouts/partials/head.html
In this file, we need to tell hugo about our styles.scss
file. We also want to make sure it’ gets converted to css and then piped through our postcss configuration before being added to the head tag in our html. There’s also some conditional logic here indicating that we should minify, fingerprint and run post processing on the stylesheet if we’re not using the server currently (it’s a production build).
<head>
{{ $styles := resources.Get "css/styles.scss" | toCSS |
postCSS (dict "config" "./assets/css/postcss.config.js") }}
{{ if .Site.IsServer }}
<link rel="stylesheet" href="{{ $styles.RelPermalink }}">
{{ else }}
{{ $styles := $styles | minify | fingerprint | resources.PostProcess }}
<link rel="stylesheet" href="{{ $styles.Permalink }}"
integrity="{{ $styles.Data.Integrity }}">
{{ end }}
</head>
themes/tailwind-theme/layouts/partials/header.html
In order to see the effect of the changes we make to our styles by adding tailwind, we’ll need to add some tailwind classes to one of our templates. Here, we’ll just add some styling to the included header.html template.
<!-- themes/tailwind-theme/layouts/partials/header.html -->
<header class="w-full bg-blue-300 py-4">
<h1 class="text-center">Welcome to HugoTails!</h1>
</header>
We’re nearing the moment where we’ll be able to see in the browser whether this is working if we run hugo server -D --watch
(runs our hugo server in development mode and watches for changes in order to rebuild the site and reload the server when we make changes to our code.)
If we do this now, we’ll actually see a blank screen, even though we’ve added this nice header with a blue background. This is the gotcha I referred to earlier and it has to do with another file we need to take a look at.
themes/tailwind-theme/layouts/_default/baseof.html
If you’re new to hugo and theme development, there’s a particular part of the code in this file that can cause an issue that I got stuck on for a bit. I’ll point it out to you here. By default, your new Hugo theme will have the following file at this location: themes/tailwind-theme/layouts/_default/baseof.html
. The only change I’ve made to it is adding a couple of styles to the body tag to give it a dark mode feel.
<!-- themes/tailwind-theme/layouts/_default/baseof.html -->
<!DOCTYPE html>
<html>
{{- partial "head.html" . -}}
<body class="bg-gray-800 text-gray-50">
{{- partial "header.html" . -}}
<div id="content">
{{- block "main" . }}{{- end }}
</div>
{{- partial "footer.html" . -}}
</body>
</html>
This baseof.html template file is used as the outer shell for every html document generated by the hugo
command. As such, it’s a great place to put any code that you want to be accessible anywhere on your site. Things like styles and scripts are potential candidates here. Also, markup you want to be present everywhere like a header that presents navigation or a footer with company information might find a nice home here.
For beginners, one thing to note is this line.
{{- block "main" . }}{{- end }}
The reason I’m drawing attention here is that having a block within baseof.html
like this means that our templates need to define main
if hugo is going to actually make use of baseof.
For example, if you have just a bit of html in your index.html file:
<!-- themes/tailwind-theme/layouts/index.html -->
<h1>Hello</h1>
Then when you visit the home page, you’re just going to see the Hello <h1>
tag. You won’t see the header with the blue background that we added to the head.html
partial.
If we want our templates to work properly, they need to define "main"
within them so that baseof can be applied as the shell and the template content can be added properly.
{{ define "main" }}
<h1>Hello</h1>
{{ end }}
Now, when we load up the home page in the browser, we see that our head partial is now loading the tailwind styles.
You’ll notice that after we’ve done this, the Hello header is no longer large like it was before tailwind styles were applied.
Adding plugins to tailwind.config.js
Here is a case where you can test out that the tailwind.config.js
configuration is working properly. Let’s head over there and add some theme configuration for the header sizes.
// themes/tailwind-theme/assets/css/tailwind.config.js
const plugin = require('tailwindcss/plugin')
module.exports = {
theme: {
extend: {
}
},
variants: {
},
plugins: [
plugin(function({ addBase, config }) {
addBase({
'h1': {
fontSize: config('theme.fontSize.3xl'),
marginTop: config('theme.spacing.6'),
marginBottom: config('theme.spacing.6'),
fontWeight: config('theme.fontWeight.semibold')
},
'h2': {
fontSize: config('theme.fontSize.2xl'),
marginTop: config('theme.spacing.4'),
marginBottom: config('theme.spacing.4'),
fontWeight: config('theme.fontWeight.semibold')
},
'h3': {
fontSize: config('theme.fontSize.xl'),
marginTop: config('theme.spacing.3'),
marginBottom: config('theme.spacing.3'),
fontWeight: config('theme.fontWeight.semibold')
},
'h4': {
fontSize: config('theme.fontSize.lg'),
marginTop: config('theme.spacing.2'),
marginBottom: config('theme.spacing.2'),
fontWeight: config('theme.fontWeight.semibold')
}
})
})
]
}
And after changing this configuration, let’s open up the browser again and take a look at what we’ve got.
If you want to use hugo’s built in stylesheets for syntax highlighting. You can run a command like this:
hugo gen chromastyles --style=solarized-dark256 > themes/tailwind-theme/assets/css/syntax.css
Then, to load these styles, you’ll want to update the head.html
partial. In this case, we’re setting this up so that any additional .css
or scss
files within the assets/css
directory will be loaded as well and concatenated into a single file and then run through postCSS.
<!-- themes/tailwind-theme/partials/head.html -->
<head>
{{ $scss := resources.Match "css/**.scss" | resources.Concat "css/compiled.css" | toCSS }}
{{ $css := resources.Match "css/**.css" | resources.Concat "css/styles.css" }}
{{ $bundle := slice ($scss) ($css) | resources.Concat "css/bundle.css" | postCSS (dict "config" "./assets/css/postcss.config.js") }}
{{ if .Site.IsServer }}
<link rel="stylesheet" href="{{ $bundle.RelPermalink }}">
{{ else }}
{{ $bundle := $bundle | minify | fingerprint | resources.PostProcess }}
<link rel="stylesheet" href="{{ $bundle.Permalink }}" integrity="{{ $bundle.Data.Integrity }}">
{{ end }}
</head>
Adding a Paperclip Icon to Code Blocks To Enable Copying the Contents
I wanted to add a paperclip icon to the code blocks so the whole content can be copied with a click. I wanted to keep the ability for users to copy just part of the code block if they’d prefer. So you can still highlight part of the block and copy that if you like, but if you click on the paperclip, it will copy the entire contents of the code block to your clipboard.
In order to do this, we actually have to grab the innerText
of the pre
tag first and then create a textarea, fill it with the contents of the pre, put it into the pre (just briefly so it’s in the DOM) and then select the contents of the textarea and invoke document.execCommand('copy')
. After this is complete, we’re removing the textarea. Currently, it also changes the paperclip color to green and switches it back to white after 3 seconds have elapsed.
// themes/tailwind-theme/assets/js/components/paperclip.js
export default class PaperClip {
constructor(pre) {
this.pre = pre;
this.pre.appendChild(this.render())
}
copy() {
console.log(this.pre.innerText);
let code = document.createElement('textarea');
code.value = this.pre.textContent.trimEnd();
this.pre.append(code);
code.select();
document.execCommand('copy');
code.remove();
this.changeClipColor('green');
setTimeout(() => this.changeClipColor('white'), 3000);
}
changeClipColor(color) {
this.path ||= this.div.querySelector('path')
this.path.setAttribute('fill', color);
}
render() {
this.div = document.createElement('div');
this.div.classList.add(..."absolute top-0 right-3".split(' '))
this.div.innerHTML = `
<svg
version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="18px" height="18px" viewBox="0 0 950 950" style="enable-background:new 0 0 950 950; fill: 'white'; stroke: 'white';" xml:space="preserve"
class="absolute top-2 right-0"
>
<g>
<path d="M857.7,141.3c-30.1-30.1-65.1-53.5-104.3-69.4c-37.8-15.3-77.7-23.2-118.7-23.2c-40.9,0-80.9,7.7-118.7,22.9
c-39.1,15.8-74.2,38.9-104.3,68.8L73.1,478.3C49.3,501.9,30.9,529.4,18.3,560.2C6.2,589.9,0,621.3,0,653.6
C0,685.7,6.1,717,18.1,746.7c12.4,30.7,30.7,58.2,54.3,81.899c23.6,23.7,51.2,42,81.9,54.5c29.7,12.101,61.1,18.2,93.3,18.2
c32.2,0,63.6-6.1,93.3-18.1c30.8-12.5,58.399-30.8,82.1-54.4l269.101-268c17.3-17.2,30.6-37.3,39.699-59.7
c8.801-21.6,13.2-44.5,13.2-67.899c0-48.2-18.8-93.2-52.899-127c-34-34.2-79.2-53.1-127.301-53.3c-48.199-0.1-93.5,18.6-127.6,52.7
L269.6,473.3c-8.5,8.5-13.1,19.7-13.1,31.601c0,11.899,4.6,23.199,13.1,31.6l0.7,0.7c17.4,17.5,45.8,17.5,63.3,0.1l168-167.5
c35.1-34.8,92.1-35,127.199-0.399c16.9,16.8,26.101,39.3,26.101,63.399c0,24.3-9.4,47.101-26.5,64.101l-269,268
c-0.5,0.5-0.9,0.899-1.2,1.5c-29.7,28.899-68.9,44.699-110.5,44.5c-41.9-0.2-81.2-16.5-110.6-46c-14.7-15-26.1-32.5-34-52
C95.5,694,91.7,674,91.7,653.6c0-41.8,16.1-80.899,45.4-110.3c0.4-0.3,0.7-0.6,1.1-0.899l337.9-337.8c0.3-0.3,0.6-0.7,0.899-1.1
c21.4-21,46.3-37.4,74-48.5c27-10.8,55.4-16.2,84.601-16.2c29.199,0,57.699,5.6,84.6,16.4c27.9,11.3,52.9,27.8,74.3,49.1
c21.4,21.4,37.9,46.4,49.2,74.3c10.9,26.9,16.4,55.4,16.4,84.6c0,29.3-5.5,57.9-16.5,85c-11.301,28-28,53.2-49.5,74.8l-233.5,232.8
c-8.5,8.5-13.2,19.7-13.2,31.7s4.7,23.2,13.1,31.6l0.5,0.5c17.4,17.4,45.8,17.4,63.2,0L857.5,586.9
C887.601,556.8,911,521.7,926.9,482.6C942.3,444.8,950,404.9,950,363.9c0-40.9-7.8-80.8-23.1-118.5
C911.101,206.3,887.8,171.3,857.7,141.3z" stroke="white" fill="white" class="transition duration-300 ease-out" />
</g>
</svg>
`
this.div.onclick = () => this.copy();
return this.div;
}
}
Finally, to make sure that these paperclips get added on page load, we add an event listener that will get all of the pre
tags in the document after the DOMContentLoaded
event. With those pre
’s, we’ll create a new Paperclip
instance for each, adding the paperclip icon to the pre
and a click
event listener to it that will trigger the copy()
method.
// themes/tailwind-theme/assets/js
import PaperClip from './components/paperclip';
document.addEventListener('DOMContentLoaded', (e) => {
document.querySelectorAll('pre').forEach(pre => new PaperClip(pre));
})