In this segment we will set up Webpack to make CSS and JavaScript bundles.
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.
Webpack
Webpack is a module bundler, we will use it to compile all of the JavaScript for a page into a single module (technically two modules, more on that later). Webpack separates the code based on how it’s used, which helps to reduce unused code and speed up page load times because the bundles are smaller, it’s also much faster to download one file than all the individual bits that would be used on a page separately. We’ll also use Babel to ensure your code will run in browser, this means we can use some newer JavaScript features that haven’t made it to the browsers yet, and Babel will transpile that code to make it compatible. Lastly, we will use Webpack for cache-busting in production builds, this way we can set all CSS/JS files to cache forever, when the file changes the filename will also change, so the browser will only download files that have changed since the last visit.
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.
Install Packages
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-cli
, 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
and 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 /sitemap
page.
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.
Folder Structure
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 js
and css
, as well as two special folders called kind
and path
.
The 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 kind
and path
folders, we’ll have two types of files: main
, and critical
. The 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 assets/global.css
to src/css/global.css
, and assets/lazyload.js
to src/js/lazyload/js
.
Configure Webpack
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
Browserslist documentation
.
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
Webpack documentation
.
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.
NPM Scripts
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.
Bundles
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.
CSS
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 src/kind
or src/path
):
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 /blog/
page:
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.
JavaScript
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 kind
and 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 first-post.md
and second-post/index.md
, then re-run npm run build
and npx serve build
.
Caching
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 netlify.toml
format:
Summary
We now have Webpack fully integrated with Hugo, we can create page type and path specific bundles for JavaScript and CSS, and can optimize page loads by splitting CSS and Javascript bundles into critical and non-critical loading methods.
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!