Webpack for a Working Website

Table of Contents

Let's say you have a vanilla js site that you want to add webpack to. And you decide that it's finally time to treat webpack like a tool rather than a framework. Let's dive in.

I'm going to start with a basic site with zero dependencies (except some linting and formatting): https://big-yak.surge.sh and turn it into a bundled and minified site: https://madly-haircut.surge.sh. This github tag is the starting point for this article and this one is the finished product. This also gives me a chance to show off one of the coolest visualizations of our universe that is not appreciated nearly enough: our local supercluster of galaxies, Laniakea.

If you have never seen a type="module" script tag, you should read about importing and exporting javascript modules within the browser.

The github project may be helpful if you want to clone it for yourself, but the source code and the website are nearly identical. Open your devtools on https://big-yak.surge.sh and navigate to the sources tab. The folder structure and contents will match the public folder in the git repository.


Figure 1: Sources tab of chrome devtools

What will a webpacked version look like?

One file for all the javascript, one for all the css.

Both filenames will have a contenthash

This will be responsible for invalidating the browser cache each time a new version of the website is published.

They will each be minified.

Both files will be automatically injected into the index.html file

With a <script> or <link> tag respectively.

And here's the kicker: I should not have to meaningfully change or break the existing source code.

Each quoted link is a github commit for you to review that step in the evolution of the website.

First up: add the required dependencies

1a4f0ef • Added webpack dependencies

npm i --save-dev webpack webpack-cli css-loader mini-css-extract-plugin optimize-css-assets-webpack-plugin clean-webpack-plugin copy-webpack-plugin html-webpack-plugin jsdom http-server

Obviously, while I was doing this the first time I didn't know exactly which dependencies I would need but I'll make it easier for everyone else.

Rename the public directory to src

a08faa7 • Renamed public to src. public is now ignored

public will now be the output directory for webpack. The files in the public folder should not be added to git, linted by eslint, or formatted by prettier so I added lines to each tool's respective ignore file.

Create a naïve webpack config

498cdcc • Added naive webpack config

Webpack is responsible for bundling the source code. It's controlled entirely by a single file in the root directory of your project: webpack.config.js. That file exports either a single javascript object or an array of objects that match a particular structure defined here. The first thing to do might as well be minifying the javascript, which requires our webpack config to specify an entry and output. I also added a build script to the package.json.

Next, I'll add a hash for cache-busting

f904366 • Added contenthash to built filenames

Since each build now has a unique filename, they don't overwrite each other every time. So I'll add the clean webpack plugin to make sure old files don't take up unnecessary space.

f2fabd7 • Clean public folder before each build

I'll use the copy webpack plugin to place images in public

be7b1c9 • Copy all files in 'src/favicon' and 'src/img' to public folder

The img and favicon folders do not require any bundling or minification, so I'll simply place them in public without any processing.

Next, the css.

d061673 • Bundling css

In webpack's own documentation they'll say to import css files directly in your javascript. However, that does not work in the browser so I will have the css be its very own entrypoint, along with adding a loader and plugin as described by this documentation. Also notice that I had to specify to use the contenthash again in the MiniCssExtractPlugin options.

Each entrypoint in webpack has a corresponding js file in the output, regardless of the entrypoint extension. So the styles css entry I just added will create an empty styles.eafiaj4324fasd.js file in the output.

That's because webpack is not chiefly designed to handle anything other than javascript. It's just that it's extensible enough for pretty much any language, including css. I'll deal with that a bit later though; it's not harming anything.

The optimize-css-assets-webpack-plugin requires virtually no setup to minify your styles

So I'll take care of that next.

30af964 • Minify css

Next: html

291f773 • Inject built files into index.html

html-webpack-plugin is the go-to for handling injecting these ugly contenthash filenames into your html for you. It's just as extensible as webpack itself which I will make use of. I'll add the plugin, using the existing index.html in the source code as the template.

I now have a functional website!

I added a script: npm run serve, to view the results locally.


Figure 2: Unnecessary empty javascript file

I still have the unnecessary file in the public folder I mentioned earlier but now it is injected into the resulting html as well.


Figure 3: Console errors after injecting scripts and styles into html

On top of that there are errors in the console.

The reason for the latter is the src/index.html file I used as a template already had <script> and <link> tags for the index.js and main.css files. Those files don't exist in the public folder.

Cleaning up

I'll tackle the console errors first. I wrote a webpack plugin just for the occasion: html-webpack-exlude-tags-plugin.js. When added to the webpack config, it will remove all html elements with the dev attribute before injecting the files (this can be overwritten to any css selector(s)). So all I have to do is add this plugin and add the dev attribute to the tags I don't want in production.

b2e9b34 • Exclude existing link and script tags from index.html

Similarly, I wrote I webpack plugin to exclude whichever files you'd like from both the webpack output and html-webpack-plugin injection: html-webpack-exclude-assets-plugin.js. Configuring that plugin is the last step toward a finished website!

fd87c77 • Exclude unnecessary js asset from build

You may be wondering why I'm not using the html-webpack-skip-assets-plugin and the reason is simple: I'm not satisfied with just not injecting the file, I'd like it to not exist at all. I also like the API I've designed, which avoids the need for regex.

The final touch

c1b6b71 • Added second deploy script

I added a second deploy script, npm run deploy:public to upload the minified site to https://madly-haircut.surge.sh.

So, what did I gain?

  • Bundled css and js files into 2 files
  • Minified code
  • Cache busting

What else might I add in the future?

  • Babel for IE support

What didn't I lose by going this route?

The source code is still a valid website. The only change I made to the entire folder (besides renaming it) is adding the dev attribute to the two html tags I want to remove during the build. No necessary build setup. The linting and bundling can all be done in a continuous deployment pipeline. The tasks of keeping dependencies up to date, handling audit warnings and fixing build config errors can be centrally managed without interfering with anyone's local setup.