A tutorial series on building a crazy fast website using Hugo and Webpack, with a little splash of React, or anything else you may want to add.
Here is the GitHub branch for this completed segment:
Example repository for my Hugo and Webpack series. Contribute to dlford/hugo-webpack-demo development by creating an account on GitHub.
We’re going to install a lot here, so let’s run the command and you can read about it while they install:
So, we’ve got
webpack of course, and its command line tool
webpack-dev-server we will use to proxy the Hugo site in development mode, and all the
*-loader packages are used by Webpack for loading different file types. We have
react-dom for pages that will use React. We have Babel and a bunch of plugins for it. We have Sass, Autoprefixer, and PostCSS for handling CSS files. Concurrently we’ll use to start up Webpack and Hugo at the same time. Lastly,
dotenv is used to read
.env files for keeping secrets safe.
How Webpack and Hugo Interact
Hugo gives each page a “Kind” and “Type”, we can use those as generic selectors for CSS and JS files. For example, if kind is “page” and type is “blog”, we can create a
main.js file in
src/kind/page/blog/main.js that will apply to all pages that match the same kind and type.
We can also get specific and create files such as
src/path/sitemap/main.js, which would apply to the
The same concepts apply for CSS files, where we can use
@import to group re-usable CSS files together.
In Hugo, we can then inject those bundles into the HTML with link and script tags. In production builds, Webpack will output bundles as files into the
assets/generated directory, we will check if the files exist and add them to the HTML conditionally, but in development mode there isn’t an easy way to do this because the bundles are served over the Webpack dev server (HTTP), so we’ll just add them whether they exist or not, this isn’t really an issue in development.
If this sounds confusing, don’t panic! There will be examples down below.
Let’s make a directory called
src in the root of the project, anything in this folder should only be consumed by Webpack. This keeps things separate from Hugo file structure. Inside of
src we can make new folders for different file types like
css, as well as two special folders called
kind folder has to do with Hugo page types, here we can put CSS and JS files that apply to all pages of that specific type. The
path folder will mimic URL paths to apply CSS and JS files to specific pages.
In both the
path folders, we’ll have two types of files:
main CSS and JS files will be lazy loaded, and the
critical CSS and JS files will be eager loaded when the page is visited. This allows fine-grained control on how each page loads its resources.
The end result will look something like this:
We’ll get more into this later, but for now let’s move
Webpack can be pretty complex to configure, we have to hook it up to Sass, PostCSS, Babel, and other tools so it will act as a sort of hub for all of it.
First, we need to set up Babel, the
.browserslistrc file tells Babel what browsers to support when transpiling code, this file also affects CSS transformation in other tools, you can learn more about this syntax in the
A quick configuration for Babel to hook up our plugins, more information can be found in the Babel documentation .
Now on to
webpack.config.js, which I’ve added plenty of comments to, again more information can be found in the
Configure Hugo Site
Now we need to get Hugo to load the Webpack bundles.
First, let’s create a partial for injecting Webpack bundles into the HTML, there is a lot going on here but it’s a pretty simple process once you wrap your head around it.
Now, in the
baseof.html template, we can use the new
webpack-assets partial to inject the Webpack bundles, we’ll replace the
global.css link and
lazyload.js script blocks here with the
webpack-assets partial, and also add it to the top of
<head> for critical assets.
We need to update the NPM scripts to include Webpack, we’ll add the Webpack devserver to the start script, and add a
prebuild script to update Browserslist and build Webpack bundles,
prebuild runs before
build automatically, so if you run
npm run build, it will fire off
prebuild first. The
browsers:update script will update the browsers list database for the latest browser versions, it’s important to do that before production builds so everything stays current.
From now on, instead of viewing your site in development mode on port
3001, you’ll use port
3000, which is the Webpack dev server.
If you start your project now, you’ll notice that none of the CSS or JS files are working, that’s because we need to create the bundle files.
First, we’ll fix the CSS by creating a
critical.css file for the home page (note that you will need to restart the development server when new bundle files are added to
Ideally you’d only want styles that apply “above the fold” to be in a
critical.css file, everything else should go in
main.css which will be lazy loaded.
Notice that this only applies to the home page, if you navigate to anothe page all the styles are gone. To fix this, you should open the “Elements” tab in your browsers developer tools, and search for “query”, you’ll find two meta tags (remember these are only rendered in develop mode, not in production builds), here’s what they should look like for the
This makes it easy enough to figure out what file created the bundles for any page, go ahead and add the global styles to all the pages on the demo site as an exercise.
First, let’s break up our lazy load function into re-usable bits. We will want to wait for
document.readyState to be
complete in many scripts, so we’ll move that code to a new file.
Next, we’ll just take out the wait for DOM code from the lazy load file.
Finally, we can combine the two in the
path bundle files.
Now we just need to add the lazy load script as we did above to all bundles for pages that might have images.
You should be able to start the project now and see a fully functional site!
If you run
npm run build and then
npx serve build, this will emulate production builds, but you’ll find the blog pages get a 404. These is because all of the posts are drafts, simply remove the line
draft: true from
second-post/index.md, then re-run
npm run build and
npx serve build.
Since we are using cache-busting by adding a hash of each asset file to their respective filenames, we can be aggresive with caching and know that any changes will be downloaded by the browser because the filename will change. Here are my cache control headers in
In the next segment, we will set up a React app inside of a Hugo page, which we’ll be able to do one or more times on any page to add small or even large reactive applications to any page on the site, make sure you so you don’t miss it!