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 |
mini-css-extract-plugin |
injects |
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),
},
},
],
},
};
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.