HTML Bundler Plugin is the right way to bundle all resources with your HTML files

This is a new powerful plugin that does exactly what you want, automatically extracts JS, CSS, images, fonts
from their sources loaded directly in HTML.
The generated HTML contains output hashed filenames of processed source files.
The plugin allow to use an HTML file or a template as an entry point in Webpack.
The purpose of this plugin is to make the developer's life much easier than it was using
html-webpack-plugin
mini-css-extract-plugin
and other plugins.
💡 Highlights:
- Define your HTML templates in Webpack entry.
- The HTML template is the entry point for all source scripts and styles.
- Source scripts and styles can be loaded directly in HTML using
<script>
and <link>
tags. - All JS and CSS files will be extracted from their sources loaded in HTML tags.
- You can easily inline JS, CSS, SVG, images without additional plugins and loaders.
- You can easily use a template engine, e.g. Nunjucks, Handlebars and others without template loaders
This plugin works like the pug-plugin but the entry point is a HTML
template.
How to easily build a multipage website with this plugin, see the Webpack boilerplate used the html-bundler-webpack-plugin
;
Simple usage example
Add source scripts, styles, images directly to HTML using a relative path or a Webpack alias:
<html>
<head>
<link href="./style.scss" rel="stylesheet">
<script src="./main.js" defer="defer"></script>
</head>
<body>
<h1>Hello World!</h1>
<img src="@images/logo.png">
</body>
</html>
The generated HTML contains hashed output filenames of processed source files:
<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>
Add the HTML templates in the Webpack entry:
const path = require('path');
const HtmlBundlerPlugin = require('html-bundler-webpack-plugin');
module.exports = {
entry: {
index: './src/views/home/index.html',
},
resolve: {
alias: {
'@images': path.join(__dirname, './src/images'),
},
},
plugins: [
new HtmlBundlerPlugin(),
],
module: {
rules: [
{
test: /.html/,
loader: HtmlBundlerPlugin.loader,
},
],
},
};
Contents
- Install and Quick start
- Features
- Plugin options
- Loader options
- Recipes
Features
- HTML file is the entry point for all resources (styles, scripts)
- handels HTML files defined in Webpack entry
- extracts CSS from source style loaded in HTML via a
<link>
tag - extracts JS from source script loaded in HTML via a
<script>
tag - resolves source files in the CSS
url()
and in HTML attributes - extracts resolved resources to output directory
- generated HTML contains hashed CSS, JS, images, fonts output filenames
- support the module types
asset/resource
asset/inline
asset
inline CSS
in HTMLinline JavaScript
in HTMLinline image
as base64 encoded
data-URL for PNG, JPG, etc. in HTML and CSSinline SVG
as SVG tag in HTMLinline SVG
as utf-8
data-URL in CSS- support the
auto
publicPath
Just one HTML bundler plugin replaces the most used functionality of the plugins and loaders:
Install and Quick start
Install the html-bundler-webpack-plugin
:
npm install html-bundler-webpack-plugin --save-dev
Install additional packages for styles:
npm install css-loader sass sass-loader --save-dev
Change your webpack.config.js
according to the following minimal configuration:
const path = require('path');
const HtmlBundlerPlugin = require('html-bundler-webpack-plugin');
module.exports = {
output: {
path: path.join(__dirname, 'dist/'),
publicPath: '/',
},
entry: {
index: './src/views/home/index.html',
'pages/about': './src/views/about/index.html',
},
plugins: [
new HtmlBundlerPlugin({
js: {
filename: 'assets/js/[name].[contenthash:8].js',
},
css: {
filename: 'assets/css/[name].[contenthash:8].css',
},
}),
],
module: {
rules: [
{
test: /\.html$/,
loader: HtmlBundlerPlugin.loader,
},
{
test: /\.(css|sass|scss)$/,
use: ['css-loader', 'sass-loader'],
},
],
},
};
Plugin options
verbose
Type: boolean
Default: false
Display information about extracted files.
test
Type: RegExp
Default: /\.html$/
The test
option allows то handel only those entry points that match their source filename.
For example, if you has *.html
and *.hbs
entry points, then you can set the option to match all needed files: test: /\.(html|hbs)$/
.
outputPath
Type: string
Default: webpack.options.output.path
The output directory for processed file. This directory can be relative by webpack.options.output.path
or absolute.
filename
Type: string | Function
Default: [name].html
The name of output file.
- If type is
string
then following substitutions (see output.filename for chunk-level) are available in template string:
[id]
The ID of the chunk.[name]
Only filename without extension or path.[contenthash]
The hash of the content.[contenthash:nn]
The nn
is the length of hashes (defaults to 20).
- If type is
Function
then following arguments are available in the function:
@param {PathData} pathData
has the useful properties (see the type PathData):
pathData.filename
the full path to source filepathData.chunk.name
the name of entry key
@param {AssetInfo} assetInfo
Mostly this object is empty.@return {string}
The name or template string of output file.
css
Type: Object
Default properties:
{
test: /\.(css|scss|sass|less|styl)$/,
verbose: false,
filename: '[name].css',
outputPath: null,
}
The filename
property see by filename option.
The outputPath
property see by outputPath option.
The option to extract CSS from a style source file loaded in the HTML tag:
<link href="./style.scss" rel="stylesheet">
Warning
Don't import source styles in JavaScript! Styles must be loaded directly in HTML.
The default CSS output filename is [name].css
.
You can specify your own filename using webpack filename substitutions:
const HtmlBundlerPlugin = require('html-bundler-webpack-plugin');
module.exports = {
plugins: [
new HtmlBundlerPlugin({
css: {
filename: 'assets/css/[name].[contenthash:8].css',
},
}),
],
};
The [name]
is the base filename of a loaded style.
For example, if source file is style.scss
, then output filename will be assets/css/style.1234abcd.css
.
If you want to have a different output filename, you can use the filename
options as the function.
Warning
Don't use mini-css-extract-plugin
or style-loader
, they are not required more.
The html-bundler-webpack-plugin
extracts CSS much faster than other plugins and resolves all asset URLs in CSS, therefore the resolve-url-loader
is redundant too.
js
Type: Object
Default properties:
{
verbose: false,
filename: '[name].js',
outputPath: null,
}
The filename
property see by filename option.
The outputPath
property see by outputPath option.
The test
property not exist because all JS files loaded in <script>
tag are automatically detected.
The option to extract JS from a script source file loaded in the HTML tag:
<script src="./main.js"></script>
The default JS output filename is [name].js
.
You can specify your own filename using webpack filename substitutions:
const HtmlBundlerPlugin = require('html-bundler-webpack-plugin');
module.exports = {
plugins: [
new HtmlBundlerPlugin({
js: {
filename: 'assets/js/[name].[contenthash:8].js',
},
}),
],
};
The [name]
is the base filename script.
For example, if source file is main.js
, then output filename will be assets/js/main.1234abcd.js
.
If you want to have a different output filename, you can use the filename
options as the function.
Type: boolean
Default: false
Enable / disable extraction of comments to *.LICENSE.txt
file.
When using splitChunks
optimization for node modules containing comments,
Webpack extracts those comments into a separate text file.
By default, the plugin don't create such unwanted text files.
But if you want to extract files like *.LICENSE.txt
, set this option to true
:
const HtmlBundlerPlugin = require('html-bundler-webpack-plugin');
module.exports = {
plugins: [
new HtmlBundlerPlugin({
extractComments: true,
}),
],
};
Loader options
sources
Type:
type sources =
| boolean
| Array<{
tag?: string;
attributes?: Array<string>;
filter?: ({
tag: string,
attribute: string,
value: string,
attributes: string,
resourcePath: string
}) => boolean|undefined;
}>;
Default: true
By default, resolves source files in the following attributes:
Tag | Attributes |
---|
link | href (for type="text/css" or rel="stylesheet" ) imagesrcset (for as="image" ) |
script | src |
img | src srcset |
input | src (for type="image" ) |
source | src srcset |
audio | src |
track | src |
video | src poster |
object | data |
To disable the processing of all attributes, set the source
option as false
.
Note
Automatically are processed only attributes containing a relative path or Webpack alias, e.g.:
src="./image.png"
- relative path to local directorysrc="../../assets/image.png"
- relative path to parent directorysrc="@images/image.png"
- image directory as Webpack alias
Url values like following are not processed, e.g.:
src="https://example.com/img/image.png"
src="//example.com/img/image.png"
src="/img/image.png"
Other not file values are ignored, e.g.:
src="data:image/png; ..."
src="javascript: ..."
The default tags can be extended with new attributes or add new tags and attributes.
For example, add the processing of the data-src
and data-srcset
attributes to the img
tag:
const HtmlBundlerPlugin = require('html-bundler-webpack-plugin');
module.exports = {
module: {
rules: [
{
test: /\.html$/,
loader: HtmlBundlerPlugin.loader,
options: {
sources: [
{
tag: 'img',
attributes: ['data-src', 'data-srcset'],
}
],
},
},
],
},
};
You can use the filter
function to allow processing for specific attributes.
For example, use filter
to allow processing for images in meta
tag by content
attribute:
<html>
<head>
<meta name="theme-color" content="#ffffff">
<meta name="twitter:image" content="./image.png">
<meta name="logo" content="./logo.png">
</head>
<body>
<img src="./image.png">
</body>
</html>
webpack.config.js
const HtmlBundlerPlugin = require('html-bundler-webpack-plugin');
module.exports = {
module: {
rules: [
{
test: /\.html$/,
loader: HtmlBundlerPlugin.loader,
options: {
sources: [
{
tag: 'meta',
attributes: ['content'],
filter: ({ tag, attribute, value, attributes, resourcePath }) => {
const allowedNames = ['twitter:image', 'logo'];
if ('name' in attributes && allowedNames.indexOf(attributes.name) < 0) {
return false;
}
},
}
],
},
},
],
},
};
The filter
parameter is an object containing the properties:
tag
the name of the HTML tagattribute
the name of the HTML attributevalue
the value of the HTML attributeattributes
all attributes of the tagresourcePath
the path of the HTML file
The attribute can be ignored by returning false
, otherwise the function should return nothing.
Filter can disable an attribute of a tag.
For example, disable the processing of default attribute srcset
of the img
tag:
const HtmlBundlerPlugin = require('html-bundler-webpack-plugin');
module.exports = {
module: {
rules: [
{
test: /\.html$/,
loader: HtmlBundlerPlugin.loader,
options: {
sources: [
{
tag: 'img',
filter: ({ attribute }) => attribute !== 'srcset',
}
],
},
},
],
},
};
preprocessor
Type:
type preprocessor = (
content: string,
loaderContext: LoaderContext
) => HTMLElement;
Default: undefined
The content
argument is raw content of a file.
The loaderContext
argument is an object contained useful properties, e.g.:
mode
- the Webpack mode: production
, development
, none
rootContext
- the path to Webpack contextresource
- the template file
Complete API see by the Loader Context.
The preprocessor is called before handling of the content.
This function can be used to replace a placeholder with a variable or compile the content with a template engine, e.g. Handlebars.
For example, set variable in the template
index.html
<html>
<head>
<title>{{title}}</title>
</head>
<body>
<h1>Hello World!</h1>
</body>
</html>
Webpack config
const path = require('path');
const HtmlBundlerPlugin = require('html-bundler-webpack-plugin');
module.exports = {
output: {
path: path.join(__dirname, 'dist/'),
},
entry: {
index: './src/index.html',
},
plugins: [new HtmlBundlerPlugin()],
module: {
rules: [
{
test: /\.html$/,
loader: HtmlBundlerPlugin.loader,
options: {
preprocessor: (content, loaderContext) => content.replace('{{title}}', 'Homepage'),
},
},
],
},
};
Note
Using the preprocessor
you can use anyone template engine without its loader.
The preprocessor
will be called for each entry file.
For multipage configuration, you can use the loaderContext.resource
property to differentiate data for diverse pages.
See the usage example.
How to use source images in HTML
Add to Webpack config the rule:
module: {
rules: [
{
test: /\.(png|jpe?g|ico)/,
type: 'asset/resource',
generator: {
filename: 'assets/img/[name].[hash:8][ext]',
},
},
],
}
Add a source file using a relative path or Webpack alias in HTML:
<html>
<head>
<link href="./favicon.ico" rel="icon" />
</head>
<body>
<img src="./apple.png" srcset="./apple1.png 320w, ./apple2.png 640w" alt="apple">
<picture>
<source srcset="./fig1.jpg, ./fig2.jpg 320w, ./fig3.jpg 640w">
</picture>
</body>
</html>
The generated HTML contains hashed output images filenames:
<html>
<head>
<link href="/assets/img/favicon.05e4dd86.ico" rel="icon" />
</head>
<body>
<img src="/assets/img/apple.f4b855d8.png" srcset="/assets/img/apple1.855f4bd8.png 320w, /assets/img/apple2.d8f4b855.png 640w" alt="apple">
<picture>
<source srcset="/assets/img/fig1.605e4dd8.jpg, /assets/img/fig2.8605e4dd.jpg 320w, /assets/img/fig3.e4605dd8.jpg 640w">
</picture>
</body>
</html>
How to preload source fonts in HTML
Add to Webpack config the rule:
module: {
rules: [
{
test: /\.(eot|ttf|woff|woff2)/,
type: 'asset/resource',
generator: {
filename: 'assets/fonts/[name][ext]',
},
},
],
}
Add a source file using a relative path or Webpack alias in HTML:
<html>
<head>
<link href="./font1.woff2" rel="preload" as="font" type="font/woff2" />
<link href="./font2.woff2" rel="preload" as="font" type="font/woff2" />
</head>
<body>
<h1>Hello World!</h1>
</body>
</html>
The generated HTML contains output fonts filenames:
<html>
<head>
<link href="/assets/fonts/font1.woff2" rel="preload" as="font" type="font/woff2" />
<link href="/assets/fonts/font2.woff2" rel="preload" as="font" type="font/woff2" />
</head>
<body>
<h1>Hello World!</h1>
</body>
</html>
Note
You don't need a plugin to copy files from source directory to public.
How to inline CSS in HTML
For example, the style.scss:
$color: red;
h1 {
color: $color;
}
Add the ?inline
query to the source filename which you want to inline:
<html>
<head>
<link href="./main.scss" rel="stylesheet" />
<link href="./style.scss?inline" rel="stylesheet" />
</head>
<body>
<h1>Hello World!</h1>
</body>
</html>
The generated HTML contains inline CSS already processed via Webpack:
<html>
<head>
<link href="/assets/css/main.05e4dd86.css" rel="stylesheet">
<style>
h1{color: red;}
</style>
</head>
<body>
<h1>Hello World!</h1>
</body>
</html>
Note
To enable source map in inline CSS set the Webpack option devtool
.
How to inline JS in HTML
For example, the script.js:
console.log('Hello JS!');
Add the ?inline
query to the source filename which you want to inline:
<html>
<head>
<script src="./main.js" defer="defer"></script>
<script src="./script.js?inline"></script>
</head>
<body>
<h1>Hello World!</h1>
</body>
</html>
The generated HTML contains inline JS already compiled via Webpack:
<html>
<head>
<script src="assets/js/main.992ba657.js" defer="defer"></script>
<script>
(()=>{"use strict";console.log("Hello JS!")})();
</script>
</head>
<body>
<h1>Hello World!</h1>
</body>
</html>
Note
If Webpack is started as serve
or watch
,
the inlined JS code will contain additional HMR code.
Don't worry it is ok, so works Webpack live reload
.
To enable source map in inline JS set the Webpack option devtool
.
How to inline SVG, PNG images in HTML
You can inline images in two ways:
- force inline image using
?inline
query - auto inline by image size
Add to Webpack config the rule:
module: {
rules: [
{
test: /\.(png|jpe?g|svg|webp|ico)$/i,
oneOf: [
{
resourceQuery: /inline/,
type: 'asset/inline',
},
{
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 1024,
},
},
generator: {
filename: 'assets/img/[name].[hash:8][ext]',
},
},
],
},
],
}
How to use a template engine
For example, using the Handlebars templating engine, there is an
index.hbs
<html>
<head>
<title>{{title}}</title>
</head>
<body>
<h1>{{headline}}</h1>
<div>
<p>{{firstname}} {{lastname}}</p>
</div>
</body>
</html>
Add the preprocessor
option to compile the content with Handlebars.
const path = require('path');
const HtmlBundlerPlugin = require('html-bundler-webpack-plugin');
const Handlebars = require('handlebars');
module.exports = {
output: {
path: path.join(__dirname, 'dist/'),
},
entry: {
index: './src/views/home/index.hbs',
},
plugins: [
new HtmlBundlerPlugin({
test: /\.(html|hbs)$/,
}),
],
module: {
rules: [
{
test: /\.(html|hbs)$/,
loader: HtmlBundlerPlugin.loader,
options: {
preprocessor: (content, loaderContext) =>
Handlebars.compile(content)({
title: 'My Title',
headline: 'Breaking Bad',
firstname: 'Walter',
lastname: 'Heisenberg',
}),
},
},
],
},
};
How to pass data into template
You can pass variables into template using a lightweight template engine, e.g. Handlebars.
See the usage example by How to use a template engine or How to pass different data by multipage configuration.
How to pass different data by multipage configuration
For multipage configuration, better to use the Nunjucks templating engine maintained by Mozilla.
For example, you have several pages with variables.
Both pages have the same layout src/views/layouts/default.html
<!DOCTYPE html>
<html>
<head>
<title>{{title}}</title>
{% block styles %}{% endblock %}
{% block scripts %}{% endblock %}
</head>
<body>
<main class="main-content">
{% block content %}{% endblock %}
</main>
</body>
</html>
src/views/pages/home/index.html
{% extends "src/views/layouts/default.html" %}
{% block styles %}
<link href="./home.scss" rel="stylesheet">
{% endblock %}
{% block scripts %}
<script src="./home.js" defer="defer"></script>
{% endblock %}
{% block content %}
<h1>{{ filmTitle }}</h1>
<p>Location: {{ location }}</p>
<img src="@images/{{ imageFile }}">
{% endblock %}
src/views/pages/about/index.html
{% extends "src/views/layouts/default.html" %}
{% block styles %}
<link href="./about.scss" rel="stylesheet">
{% endblock %}
{% block scripts %}
<script src="./about.js" defer="defer"></script>
{% endblock %}
{% block content %}
<h1>Main characters</h1>
<ul>
{% for item in actors %}
<li class="name">{{ item.firstname }} {{ item.lastname }}</li>
{% endfor %}
</ul>
{% endblock %}
Webpack config
const path = require('path');
const HtmlBundlerPlugin = require('html-bundler-webpack-plugin');
const Nunjucks = require('nunjucks');
const findData = (sourceFile, data) => {
for (const [key, value] of Object.entries(data)) {
if (sourceFile.endsWith(key)) return value;
}
return {};
};
const entryData = {
'home/index.html': {
title: 'Home',
filmTitle: 'Breaking Bad',
location: 'Albuquerque, New Mexico',
imageFile: 'map.png',
},
'about/index.html': {
title: 'About',
actors: [
{
firstname: 'Walter',
lastname: 'White, "Heisenberg"',
},
{
firstname: 'Jesse',
lastname: 'Pinkman',
},
],
},
};
module.exports = {
output: {
path: path.join(__dirname, 'dist/'),
},
entry: {
index: 'src/views/pages/home/index.html',
about: 'src/views/pages/about/index.html',
},
resolve: {
alias: {
'@images': path.join(__dirname, 'src/assets/images'),
},
},
plugins: [
new HtmlBundlerPlugin({
js: {
filename: 'assets/js/[name].[contenthash:8].js',
},
css: {
filename: 'assets/css/[name].[contenthash:8].css',
},
}),
],
module: {
rules: [
{
test: /\.html/,
loader: HtmlBundlerPlugin.loader,
options: {
preprocessor: (content, { resource }) => {
const data = findData(resource, entryData);
return Nunjucks.renderString(content, data);
},
},
},
{
test: /\.(css|sass|scss)$/,
use: ['css-loader', 'sass-loader'],
},
{
test: /\.(png|svg|jpe?g|webp)$/i,
type: 'asset/resource',
generator: {
filename: 'assets/img/[name].[hash:8][ext]',
},
},
],
},
};
The generated dist/index.html
<!DOCTYPE html>
<html>
<head>
<title>Home</title>
<link href="assets/css/home.2180238c.css" rel="stylesheet">
<script src="assets/js/home.790d746b.js" defer="defer"></script>
</head>
<body>
<main class="main-content">
<h1>Breaking Bad</h1>
<p>Breaking Bad is an American crime drama</p>
<p>Location: Albuquerque, New Mexico</p>
<img src="assets/img/map.697ef306.png" alt="location" />
</main>
</body>
</html>
The generated dist/about.html
<!DOCTYPE html>
<html>
<head>
<title>About</title>
<link href="assets/css/about.2777c101.css" rel="stylesheet">
<script src="assets/js/about.1.c5e03c0e.js" defer="defer"></script>
</head>
<body>
<main class="main-content">
<h1>Main characters</h1>
<ul>
<li class="name">Walter White, "Heisenberg"</li>
<li class="name">Jesse Pinkman</li>
</ul>
</main>
</body>
</html>
HMR live reload
To enable live reload by changes any file add in the Webpack config the devServer
option:
module.exports = {
devServer: {
static: {
directory: path.join(__dirname, 'dist'),
},
watchFiles: {
paths: ['src/**/*.*'],
options: {
usePolling: true,
},
},
},
};
Note
Live reload works only if in HTML used a JS file. This is specific of Webpack.
If your HTML has not a JS, then create one empty JS file, e.g. hmr.js
and add it in the HTML:
<script src="./hmr.js"></script>
Also See
- ansis - The Node.js lib for ANSI color styling of text in terminal
- pug-loader The Pug loader for Webpack
- pug-plugin The Pug plugin for Webpack
License
ISC