Using HTML Bundler Plugin for Webpack to generate HTML files
Tags: webdev, javascript, html, webpack
Published: 2023-08-06 22:00:00

Webpack works for building JavaScript bundles, but does not support neither the HTML nor the CSS files out of the box.
To bundle compiled JavaScript and CSS with HTML we can use the powerful [html-bundler-webpack-plugin](https://github.com/webdiscus/html-bundler-webpack-plugin).

## Install

The first step is to install the plugin:
```
npm install html-bundler-webpack-plugin --save-dev
```

Install additional packages for styles:
```
npm install css-loader sass-loader sass --save-dev
```

## A simple static site

We've created some static files to demonstrate how the plugin handles references in the HTML to script, style and image source files:

```
src/views/index.html <= entrypoint
src/js/main.js
src/scss/style.scss
src/images/favicon.ico
src/images/picture.png (size > 2 KB)
src/images/icon.png    (size < 2 KB)
```

Create the `index.html` file in the `src/views/` directory:

```html
<!doctype html>
<html lang="en">
<head>
  <title>Home</title>
  <link href="../images/favicon.ico" rel="shortcut icon">
  <link href="../scss/style.scss" rel="stylesheet">
  <script src="../js/main.js" defer="defer"></script>
</head>
<body>
  <h1>Hello World!</h1>
  <img src="../images/picture.png" alt="picture">
  <img src="../images/icon.png" alt="icon">
</body>
</html>
```

To bind our style and script files to HTML, we specify them directly in the HTML using relative paths to the source files.

With a complex file structure, some relative paths might be like `../../../images`. To avoid such unreadable paths, is recommended to use Webpack aliases.

The plugin allows us to use both relative paths and Webpack aliases.

We'll be using aliases, so we'll define them in the Webpack `resolve.alias` option:
```js
{
  '@scripts': path.join(__dirname, 'src/js'),
  '@styles': path.join(__dirname, 'src/scss'),
  '@images': path.join(__dirname, 'src/images'),
}
```

Then change all relative paths in `index.html` to the aliases:

```html
<!doctype html>
<html lang="en">
<head>
  <title>Home</title>
  <link href="@images/favicon.ico" rel="shortcut icon">
  <link href="@styles/style.scss" rel="stylesheet">
  <script src="@scripts/main.js" defer="defer"></script>
</head>
<body>
  <h1>Hello World!</h1>
  <img src="@images/picture.png" alt="picture">
  <img src="@images/icon.png" alt="icon">
</body>
</html>
```

So HTML looks better and cleaner.

## Processing HTML templates

Webpack doesn’t know how to handle HTML templates, but the [html-bundler-webpack-plugin](https://github.com/webdiscus/html-bundler-webpack-plugin) can [render any template](https://github.com/webdiscus/html-bundler-webpack-plugin#template-engine) and place the generated HTML into the output directory.

Configure the plugin in the `webpack.config.js` to handle `index.html`:

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

module.exports = {
  plugins: [
    new HtmlBundlerPlugin({
      entry: {
        // define templates here
        index: 'src/views/index.html', // => dist/index.html
      },
    }),
  ],
  // ...
};
```

We can define HTML templates in the `entry` option of the plugin.
The `entry` key is the output filename without `.html` extension, and the value is the template file. So we can define many templates in the `entry` option.

## Automatically processing many HTML templates

There is one template file, but it would be tedious to update the Webpack config every time templates are added or removed. Instead of manually adding other templates, let’s modify the configuration to automatically include all templates it finds.

```js
module.exports = {
  plugins: [
    new HtmlBundlerPlugin({
      // relative or absolute path to templates
      entry: 'src/views/',
    }),
  ],
  // ...
};
```

If the `entry` option is a path, the plugin finds all templates automatically and keep the same directory structure in the output directory. So we can create many templates in the entry directory, without restarting Webpack running in `serve` mode.

## Processing styles

To compile Sass files specified in HTML to CSS, add the module rule:

```js
module.exports = {
   // ...
   module: {
    rules: [
      {
        test: /\.(scss)$/,
        use: ['css-loader', 'sass-loader'],
      },
    ],
  },  
  // ...
};
```

The plugin extracts CSS from compiled Sass files and save as a separate file into the output directory.

## Processing images

The plugin resolves image references specified in HTML and allows Webpack to copy those images to the output directory. We can define a hashed output filename for images using the `generator.filename` in the rule:

```js
module.exports = {
   // ...
   module: {
    rules: [
      {
        test: /\.(ico|png|jp?g|svg)$/,
        type: 'asset/resource',
        generator: {
          filename: 'img/[name].[hash:8][ext]',
        },
      },
    ],
  },  
  // ...
};
```

To optimise loading of small images we can inline images directly in the HTML using the `type: 'asset'` and `parser.dataUrlCondition.maxSize` in the rule:

```js
module.exports = {
   // ...
   module: {
    rules: [
      {
        test: /\.(ico|png|jp?g|svg)$/,
        type: 'asset',
        generator: {
          // save images to file
          filename: 'img/[name].[hash:8][ext]',
        },
        parser: {
          dataUrlCondition: {
            // inline images < 2 KB
            maxSize: 2 * 1024
          }
        },
      },
    ],
  },  
  // ...
};
```

The plugin automatically inline Images smaller then `maxSize`.

## Output filenames for JS and CSS

By default, the processed JS and CSS files will be saved in the output directory under unhashed filenames. But it's recommended to use hashed output filenames.

We can define the output filenames for JS and CSS in the plugin options:

```js
module.exports = {
  plugins: [
    new HtmlBundlerPlugin({
      entry: {
        index: 'src/views/index.html',
      },
      js: {
        // output filename for JS
        filename: 'js/[name].[contenthash:8].js',
      },
      css: {
        // output filename for CSS
        filename: 'css/[name].[contenthash:8].css',
      },
    }),
  ],
  // ...
};
```

Optional, you can inline JS and CSS in HTML using the [js.inline](https://github.com/webdiscus/html-bundler-webpack-plugin#option-js) and the [css.inline](https://github.com/webdiscus/html-bundler-webpack-plugin#option-css) plugin options.

## Live Reload

To enable reloading of the browser after changes, add the `devServer` option to the Webpack config:

```js
module.exports = {
  // enable HMR with live reload
  devServer: {
    static: path.join(__dirname, 'dist'),
    watchFiles: {
      paths: ['src/**/*.*'],
      options: {
        usePolling: true,
      },
    },
  },
};
```

## Final Webpack configuration

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

module.exports = {
  mode: 'development',

  output: {
    path: path.resolve(__dirname, 'dist'),
    clean: true,
  },

  resolve: {
    alias: {
      '@scripts': path.join(__dirname, 'src/js'),
      '@styles': path.join(__dirname, 'src/scss'),
      '@images': path.join(__dirname, 'src/images'),
    },
  },

  plugins: [
    new HtmlBundlerPlugin({
      // path to templates
      entry: 'src/views/',
      js: {
        // output filename for JS
        filename: 'js/[name].[contenthash:8].js',
      },
      css: {
        // output filename for CSS
        filename: 'css/[name].[contenthash:8].css',
      },
    }),
  ],

  module: {
    rules: [
      {
        test: /\.(scss)$/,
        use: ['css-loader', 'sass-loader'],
      },
      {
        test: /\.(ico|png|jp?g|svg)/,
        type: 'asset',
        generator: {
          // save images to file
          filename: 'img/[name].[hash:8][ext]',
        },
        parser: {
          dataUrlCondition: {
            // inline images < 2 KB
            maxSize: 2 * 1024,
          },
        },
      },
    ],
  },

  // enable HMR with live reload
  devServer: {
    static: path.resolve(__dirname, 'dist'),
    watchFiles: {
      paths: ['src/**/*.*'],
      options: {
        usePolling: true,
      },
    },
  },
};
```

## Conclusion

The generated HTML contains the output filenames of the processed files and inlined small image. All processed files are paced in the `dist/` output directory.

```html
<!doctype html>
<html lang="en">
<head>
  <title>Home</title>
  <link href="img/favicon.34fd67a8.ico" rel="shortcut icon">
  <link href="css/style.f042b772.css" rel="stylesheet">
  <script src="js/main.b35246f4.js" defer="defer"></script>
</head>
<body>
  <h1>Hello World!</h1>
  <img src="img/picture.7b396424.png" alt="picture">
  <!-- src contains "data:image/png;base64,iVBORwSU..." -->
  <img src="..." alt="icon">
</body>
</html>
```

With just one [html-bundler-webpack-plugin](https://github.com/webdiscus/html-bundler-webpack-plugin) you can very easily set up Webpack to generate static HTML files.

[View source code on GitHub](https://github.com/webdiscus/html-bundler-webpack-plugin/tree/master/examples/simple-site)

[Try it out in browser](https://stackblitz.com/edit/webpack-webpack-js-org-diop8g?file=webpack.config.js)

{% cta https://github.com/webdiscus/html-bundler-webpack-plugin %} Give HTML Bundler Plugin for Webpack a ⭐️ on GitHub {% endcta %}