The Right Way to Utilize Webpack for Bundling a HTML Page With CSS and JS

Written by biodiscus | Published 2023/02/13
Tech Story Tags: webpack | html | css | js | javascript | javascript-development | web-development | webdev

TLDRThe new HTML Bundler plugin makes Webpack setup incredibly simple. All the config happens in one place. All source styles and scripts specified in HTML are processed, and the extracted JS and CSS are saved to the output directory. The plugin automatically substitutes output filenames into the generated HTML file.via the TL;DR App

Usually, a junior web developer is faced with the problem of using a complex Webpack setup to create a simple HTML page with JS and CSS. First, you need to know where and how to connect source styles and scripts with HTML, and where to define the HTML template itself. You need to know what plugins and loaders are needed for this and how to configure them. The whole process isn’t simple or intuitive. Moreso, configurations happen in different places.

Until now, it was necessary to use such plugins and loaders as:

Package

Description

html-webpack-plugin

creates HTML and inject script tag for compiled JS file into HTML

mini-css-extract-plugin

injects link tag for processed CSS file into HTML

webpack-remove-empty-scripts

removes generated empty JS files

html-loader

exports HTML

style-loader

injects an inline CSS into HTML

posthtml-inline-svg

injects an inline SVG icon into HTML

resolve-url-loader

resolves a relative URL in CSS

svg-url-loader

encodes a SVG data-URL as utf8

Finally, the new HTML Bundler Plugin for Webpack has emerged, replacing this zoo of plugins and making Webpack setup incredibly simple, logical, and intuitive. What’s more? all the config happens in one place.

This plugin allows you to use an HTML template as a starting point for all the dependencies used in your web application. All source styles and scripts specified in HTML are processed, and the extracted JS and CSS are saved to the output directory.

Simple usage example

For example, you have an HTML template containing a script, a style, and an image.

You can add script and style source files directly to HTML using a relative path or a Webpack alias:

<html>
<head>
  <!-- load source style here -->
  <link href="./style.scss" rel="stylesheet">
  <!-- load source script here -->
  <script src="./main.js" defer="defer"></script>
</head>
<body>
  <h1>Hello World!</h1>
  <!-- @images is Webpack alias for the source images directory -->
  <img src="@images/logo.png">
</body>
</html>

The source files are processed using Webpack loaders and the plugin automatically substitutes output filenames into the generated HTML file:

<html>
<head>
  <link href="assets/css/style.05e4dd86.css" rel="stylesheet">
  <script src="assets/js/main.f4b855d8.js" defer="defer"></script>
</head>
<body>
  <h1>Hello World!</h1>
  <img src="assets/img/logo.58b43bd8.png">
</body>
</html>

In the Webpack config define an HTML template as the entry point in the entry option:

const path = require('path');
const HtmlBundlerPlugin = require('html-bundler-webpack-plugin');

module.exports = {
  output: {
    path: path.join(__dirname, 'dist/'),
  },

  plugins: [
    new HtmlBundlerPlugin({
      entry: {
        // define HTML files here

        // output dist/index.html
        index: 'src/views/home/index.html',
        // output dist/pages/about.html
        'pages/about': 'src/views/about/index.html',
        // ...
      },
      js: {
        // output filename of extracted JS
        filename: 'assets/js/[name].[contenthash:8].js',
      },
      css: {
        // output filename of extracted CSS
        filename: 'assets/css/[name].[contenthash:8].css',
      },
    }),
  ],

  module: {
    rules: [
      // HTML templates
      {
        test: /\.html$/,
        loader: HtmlBundlerPlugin.loader, // HTML template loader
      },
      // styles
      {
        test: /\.(css|sass|scss)$/,
        use: ['css-loader', 'sass-loader'],
      },
      // images
      {
      test: /\.(png|jpe?g|ico|svg)$/,
      type: 'asset/resource',
      generator: {
        filename: 'assets/img/[name].[hash:8][ext]',
      },
    },
    ],
  },
};

Using a template engine

You can use any template engine like EJS, Handlebars, Nunjucks, and others without template loaders.

For example, there is a Handlebars template file index.html:

<html>
<head>
  <title>{{ title }}</title>
</head>
<body>
  <h1>{{ headline }}</h1>
  <div>
    <p>{{ firstname }} {{ lastname }}</p>
  </div>
</body>
</html>

To compile a source template into HTML use the preprocessor option of the loader. In the preprocessor any JS templating module can be used. In our case, it is the handlebars.

// ...
module: {
  rules: [
    {
      test: /\.html$/, // must match the files specified in the entry
      loader: HtmlBundlerPlugin.loader,
      options: {
        // add the preprocessor option to compile template into HTML
        // - content is a source of a template file
        // - data is an object defined in the entry option
        preprocessor: (content, { data }) => Handlebars.compile(content)(data),
      },
    },
  ],
},
// ...

For details of the preprocessor option see here.

To pass variables into the template use the advanced syntax of the entry option and the data property:

new HtmlBundlerPlugin({
  entry: {
    index: { // => the key is the HTML output filename w/o extension
      import: './src/views/home/index.html',
      // pass data into the template (via preprocessor)
      data: {
        // ... variables as key:value
      },
    },
   },
 }),

The complete Webpack configuration:

const path = require('path');
const HtmlBundlerPlugin = require('html-bundler-webpack-plugin');
const Handlebars = require('handlebars');

module.exports = {
  output: {
    path: path.join(__dirname, 'dist/'),
  },

  plugins: [
    new HtmlBundlerPlugin({      
      entry: {
        index: {
          import: './src/views/home/index.html',
          data: {
            title: 'Heisenberg',
            headline: 'Breaking Bad',
            firstname: 'Walter',
            lastname: 'White',
          },
        },
      },
    }),
  ],

  module: {
    rules: [
      {
        test: /\.html$/,
        loader: HtmlBundlerPlugin.loader,
        options: {
          preprocessor: (content, { data }) => Handlebars.compile(content)(data),
        },
      },
      // ... other rules, e.g. for styles, images, fonts, etc.
    ],
  },
};

The generated HTML:

<html>
<head>
  <title>Heisenberg</title>
</head>
<body>
  <h1>Breaking Bad</h1>
  <div>
    <p>Walter White</p>
  </div>
</body>
</html>

You can use other templates, here are some examples of using the preprocessor for different template engines.

Handlebars

const HtmlBundlerPlugin = require('html-bundler-webpack-plugin');
const Handlebars = require('handlebars');

module.exports = {
  // ...
  module: {
    rules: [
      {
        test: /\.html$/,
        loader: HtmlBundlerPlugin.loader,
        options: {
          preprocessor: (content, { data }) => Handlebars.compile(content)(data),
        },
      },
    ],
  },
};

Mustache

const HtmlBundlerPlugin = require('html-bundler-webpack-plugin');
const Mustache = require('mustache');

module.exports = {
  // ...
  module: {
    rules: [
      {
        test: /\.html$/,
        loader: HtmlBundlerPlugin.loader,
        options: {
          preprocessor: (content, { data }) => Mustache.render(content, data),
        },
      },
    ],
  },
};

Nunjucks

const HtmlBundlerPlugin = require('html-bundler-webpack-plugin');
const Nunjucks = require('nunjucks');

module.exports = {
  // ...
  module: {
    rules: [
      {
        test: /\.html$/,
        loader: HtmlBundlerPlugin.loader,
        options: {
          preprocessor: (content, { data }) => Nunjucks.renderString(content, data),
        },
      },
    ],
  },
};

EJS

const HtmlBundlerPlugin = require('html-bundler-webpack-plugin');
const ejs = require('ejs');

module.exports = {
  // ...
  module: {
    rules: [
      {
        test: /\.html$/,
        loader: HtmlBundlerPlugin.loader,
        options: {
          preprocessor: (content, { data }) => ejs.render(content, data),
        },
      },
    ],
  },
};

Thus, you can use the latest version of any template engine, independent of whether a Webpack loader exists for it and how up-to-date it is.

Often, many original loaders have not been updated for more than 2 years and may contain outdated versions of these templating engines. This means, that you can't use features (or bugfixes) of the last version of a template engine.


Written by biodiscus | Fullstack web developer
Published by HackerNoon on 2023/02/13