DEV Community

Cover image for Using HTML Bundler Plugin for Webpack to generate HTML files
webdiscus
webdiscus

Posted on • Edited on

Using HTML Bundler Plugin for Webpack to generate HTML files

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.

Install

The first step is to install the plugin:

npm install html-bundler-webpack-plugin --save-dev
Enter fullscreen mode Exit fullscreen mode

Install additional packages for styles:

npm install css-loader sass-loader sass --save-dev
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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

<!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>
Enter fullscreen mode Exit fullscreen mode

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:

{
  '@scripts': path.join(__dirname, 'src/js'),
  '@styles': path.join(__dirname, 'src/scss'),
  '@images': path.join(__dirname, 'src/images'),
}
Enter fullscreen mode Exit fullscreen mode

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

<!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>
Enter fullscreen mode Exit fullscreen mode

So HTML looks better and cleaner.

Processing HTML templates

Webpack doesn’t know how to handle HTML templates, but the html-bundler-webpack-plugin can render any template and place the generated HTML into the output directory.

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

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
      },
    }),
  ],
  // ...
};
Enter fullscreen mode Exit fullscreen mode

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.

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

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:

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

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:

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

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:

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
          }
        },
      },
    ],
  },  
  // ...
};
Enter fullscreen mode Exit fullscreen mode

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:

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',
      },
    }),
  ],
  // ...
};
Enter fullscreen mode Exit fullscreen mode

Optional, you can inline JS and CSS in HTML using the js.inline and the css.inline plugin options.

Live Reload

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

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

Final Webpack configuration

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,
      },
    },
  },
};
Enter fullscreen mode Exit fullscreen mode

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.

<!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>
Enter fullscreen mode Exit fullscreen mode

With just one html-bundler-webpack-plugin you can very easily set up Webpack to generate static HTML files.

View source code on GitHub

Try it out in browser

Give HTML Bundler Plugin for Webpack a ⭐️ on GitHub

Top comments (1)

Collapse
 
tonynguyen137 profile image
共産主義のト二イ • Edited

resolve alias @images doesnt work, i copied everything from here, but somehow the html cant resolve T_T.
edit: I changed my image from jpeg to png and now it works. the html bundler doesnt work with jpeg?
edit2: found the mistake, its this regex jp?g, it should be jpe?g