Marmelab Blog

Webpack HTML plug-in in a nutshell

We have been using Webpack for several months now, and we ended up to some more optimized solutions than the one given in the Webpack introduction post. Using the Webpack HTML Plugin allows to overcome some limitations of the HTML file we used in the previous post.

Hard-Written Paths, Cache Busting... Help!

If we apply literally the previous post way to setup our HTML, it results to something like:

<!DOCTYPE html>
<html>
    <head>
        <link href="http://localhost:8080/css/style.css" rel="stylesheet" />
    </head>
    <body>
        <h1>My Application</h1>
        <script src="http://localhost:8080/js/main.js"></script>
    </body>
</html>

It may look like a correct solution. However, it is not satisfactory enough for a production deployment. For instance, the hard-written URL with localhost won't work elsewhere than on your machine.

On my machine, it works!

Of course, you can simply sed your build output, or use two distinct indexes (an index.dev.html and an index.html files), but that's far from optimal. And what about adding some cache busters at each generation?

That's where the HTML Webpack Plugin comes into play.

HTML Webpack Plugin: the Right Way to Load your HTML Template

We install the HTML Webpack Plugin like all other Webpack packages, using npm:

npm install --save-dev html-webpack-plugin

Basic configuration

Then, we add the plugin to our Webpack configuration:

var HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    // ...
    plugins: [
        new HtmlWebpackPlugin({
            hash: true,
            filename: 'index.html',
            template: __dirname + '/index.html',
        })
    ]
};

This is the most standard configuration. We configured the plugin using the file at /index.html as a template and serving it as index.html (the filename property). We set hash to true to let Webpack handle the cache busting automatically.

If we run Webpack with this extra configuration, we can access our application directly on http://localhost:8080. However, we shouldn't notice any change. And indeed, our index.html file has still hard-written paths.

So, let's just use some variables available thanks to the HTML plugin:

<!DOCTYPE html>
<html>
    <head>
        <title>My Awesome Project</title>
        <link href="{%= o.htmlWebpackPlugin.files.css %}" rel="stylesheet">
    </head>
    <body>
        <h1>My Application</h1>
        <script src="{%= o.htmlWebpackPlugin.files.js %}"></script>
    </body>
</html>

The o.htmlWebpackPlugin.files variable is an object containing especially two useful keys: css and js. These keys allow us to loop through all our entries, inserting them properly in our page. It solves our both issues: hard-written paths and cache-busting (thanks to the hash configuration property).

Note that we have to relaunch Webpack as template changes are not taken into account by the watch daemon.

Passing Variables to our HTML Template

If we want to specify other variables in our HTML, we just have to retrieve them directly using the o.htmlWebpackPlugin.options variable. For instance, let's imagine that we updated our configuration to add current environment to our config object:

var HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    // ...
    plugins: [
        new HtmlWebpackPlugin({
            hash: true,
            filename: 'index.html',
            template: __dirname + '/index.html',
            environment: process.env.NODE_ENV
        })
    ]
};

Then, we would be able to retrieve it in our HTML using:

<!DOCTYPE html>
<html>
    <head>
        <title>({%= o.htmlWebpackPlugin.options.environment %}) My Awesome Project</title>
        <link href="{%= o.htmlWebpackPlugin.files.css %}" rel="stylesheet">
    </head>
    <body class="{%= o.htmlWebpackPlugin.options.environment %}">
        <h1>My Application</h1>
        <script src="{%= o.htmlWebpackPlugin.files.js %}"></script>
    </body>
</html>

This code allows us to differentiate our site depending of the environment. For instance, we may put a red background on production. It would prevent us from committing to the world some testing data.

Bonus: Generating Static Websites with Webpack

This plug-in is really useful. But, let's take it to its logical conclusion by generating a whole static website. It may be useful to add a few presentation pages to an existing single page application. And this plug-in helps us to do so, without having to add a new technology such as Jekyll to our stack.

As this plug-in doesn't allow to include other HTML files into the main one, we need some JavaScript here:

import fs from 'fs';
import glob from 'glob';
import HtmlWebpackPlugin from 'html-webpack-plugin';

export default () => {
    const realContentFolderPath = fs.realpathSync(__dirname + '/static/pages/');
    const layout = fs.readFileSync(`static/layout.html`, { encoding: 'utf8' });

    const generatePage = template => {
        const pageContent = fs.readFileSync(template, { encoding: 'utf-8' });
        return layout.replace('{# PAGE_CONTENT #}', pageContent);
    }

    const pages = glob.sync(contentDir + '/**/*.html');
    return pages.map(page => new HtmlWebpackPlugin({
        templateContent: generatePage(page),
        filename: page.replace(realContentFolderPath, ''),
        hash: true
    }));
};

This ES6 snippet above shows a function we use to generate static web pages from a given folder (here: /static/pages). For each HTML file in this folder, we plug a new instance of the HTML plug-in using the file name as an entry-point.

Unlike the first configuration sample, we are not using template here but templateContent, which is either a string or a function returning a string: the page content. And to prevent from repeating the same menu everywhere, we have embedded all our contents into a higher layout.html template, just replacing the {# PAGE_CONTENT #} string by the final file content.

That's a good solution for really basic needs. We can also easily customize the title, or add an active class to current page in the navigation bar, still using some basic JavaScript. But if our needs become more complex, let's be pragmatic, and switch to a more robust solution such as Jekyll.