The plugin makes easily to bundle HTML pages from templates, source styles, scripts and images
![node](https://img.shields.io/npm/dm/html-bundler-webpack-plugin)
🚀 The best modern alternative to html-webpack-plugin.
This plugin allows to use an HTML template as a starting point for all dependencies used in your web application.
All source files of scripts, styles, images specified in HTML are processed automatically.
All processed files are extracted and saved to the output directory.
The plugin automatically substitutes the output filenames of the processed resources in the generated HTML file.
💡 Highlights
- An entry point is an HTML template.
- Source scripts and styles can be specified directly in HTML using
<script>
and <link>
tags. - Resolving source assets specified in standard attributes
href
src
srcset
etc. - Inline JS, CSS, SVG, PNG without additional plugins and loaders.
- Using template engines Eta, EJS, Handlebars, Nunjucks, LiquidJS and others without template loaders.
- Support for both
async
and sync
preprocessor
✅ Profit
You specify all the source scripts and styles in one right place (in HTML),
instead of defining them in many non-logic places:
defining JS files in Webpack Entry, importing SCSS into a JS file.
❓If you have discovered a bug or have a feature suggestion, feel free to create an issue on GitHub.
Simple usage example
Add source scripts and styles directly to HTML:
<html>
<head>
<link href="./style.scss" rel="stylesheet">
<script src="./main.js" defer="defer"></script>
</head>
<body>
<h1>Hello World!</h1>
<img src="./logo.png">
</body>
</html>
The generated HTML contains the output filenames of the processed source files,
while the script
and link
tags remain in place:
<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 entry
option (syntax is identical to Webpack entry):
const HtmlBundlerPlugin = require('html-bundler-webpack-plugin');
module.exports = {
plugins: [
new HtmlBundlerPlugin({
entry: {
index: 'src/views/home/index.html',
'pages/about': 'src/views/about/index.html',
},
}),
],
};
See the complete Webpack configuration.
How to create multiple HTML pages with html-bundler-webpack-plugin, see the boilerplate.
Contents
- Features
- Install and Quick start
- Webpack options
- Plugin options
- Loader options
- Template engines
- Setup HMR (Live Reload)
- Recipes
- Demo examples
- Multiple page e-shop template (
Handlebars
) demo | source - Design system NIHR: Components, Elements, Layouts (
Handlebars
) demo | source - Asia restaurant (
Nunjucks
) demo | source - 10up / Animation Best Practices demo | source
Features
- HTML template is the entry point for all resources
- extracts CSS from source style specified in HTML via a
<link>
tag - extracts JS from source script specified 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 output filenames
- support the module types
asset/resource
asset/inline
asset
asset/source
(*) 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 - enable/disable extraction of comments to
*.LICENSE.txt
file - supports all JS template engines such as Eta, EJS, Handlebars, Nunjucks, LiquidJS and others
- minification of generated HTML
(*) - asset/source
works currently for SVG only, in a next version will work for other files too
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
For example, there is a template ./src/views/home/index.html:
<html>
<head>
<title><%= title %></title>
<link href="./style.scss" rel="stylesheet">
<script src="./main.js" defer="defer"></script>
</head>
<body>
<h1>Hello <%= name %>!</h1>
<img src="./logo.png">
</body>
</html>
To compile this template use the following Webpack configuration:
const HtmlBundlerPlugin = require('html-bundler-webpack-plugin');
module.exports = {
plugins: [
new HtmlBundlerPlugin({
entry: {
index: {
import: 'src/views/home/index.html',
data: { title: 'Homepage', name: 'Heisenberg' }
},
},
js: {
filename: 'assets/js/[name].[contenthash:8].js',
},
css: {
filename: 'assets/css/[name].[contenthash:8].css',
},
}),
],
module: {
rules: [
{
test: /\.(css|sass|scss)$/,
use: ['css-loader', 'sass-loader'],
},
{
test: /\.(ico|png|jp?g|svg)$/,
type: 'asset/resource',
generator: {
filename: 'assets/img/[name].[hash:8][ext][query]',
},
},
],
},
};
Note
To define the JS output filename, use the js.filename
option of the plugin.
Don't use Webpack's output.filename
, hold all relevant settings in one place - in plugin options.
Both places have the same effect, but js.filename
has priority over output.filename
.
No additional template loader required. The plugin handels templates with base EJS
-like syntax automatically.
The default templating engine is Eta.
For using the native EJS
syntax see Templating with EJS.
For using the Handlebars
see Templating with Handlebars.
For other templates see Template engines.
For custom templates you can use the preprocessor option to handels any template engine.
Webpack options
Important Webpack options used to properly configure this plugin.
output.path
Type: string
Default: path.join(process.cwd(), 'dist')
The root output directory for all processed files, as an absolute path.
You can omit this option, then all generated files will be saved under dist/
in your project directory.
output.publicPath
Type: string|function
Default: auto
The value of the option is prefixed to every URL created by this plugin.
If the value is not the empty string or auto
, then the option must end with /
.
The possible values:
publicPath: 'auto'
- automatically determines a path of an asset relative of their issuer.
The generated HTML page can be opened directly form the local directory and all js, css and images will be loaded in a browser.publicPath: ''
- a path relative to an HTML page, in the same directory. The resulting path is different from a path generated with auto
.publicPath: '/'
- a path relative to document root
directory on a serverpublicPath: '/assets/'
- a sub path relative to document root
directory on a serverpublicPath: '//cdn.example.com/'
- an external URL with the same protocol (http://
or https://
)publicPath: 'https://cdn.example.com/'
- an external URL with the https://
protocol only
output.filename
Type: string|function
Default: [name].js
The output name of a generated JS file.
Highly recommended to define the filename in the Plugin option js.filename
.
The output name of a generated CSS file is determined in the Plugin option css.filename
.
Define output JS and CSS filenames in the Plugin option, in one place:
const HtmlBundlerPlugin = require('html-bundler-webpack-plugin');
module.exports = {
plugins: [
new HtmlBundlerPlugin({
js: {
filename: 'assets/js/[name].[contenthash:8].js',
},
css: {
filename: 'assets/css/[name].[contenthash:8].css',
},
}),
],
};
entry
The starting point to build the bundle.
Note
Using this plugin an entry point
is an HTML template.
All script and style source files must be specified in the HTML template.
You can use the Webpack entry
option to define HTML templates,
but it is highly recommended to define all templates in plugin option entry
,
because it has an additional data
property (not available in the Webpack entry)
to pass custom variables into the HTML template.
For details see the plugin option entry
.
Plugin options
test
Type: RegExp
Default: /\.(html|ejs|eta|hbs|handlebars)$/
The test
option allows to handel only those templates as entry points that match the name of the source file.
For example, if you have other templates, e.g. *.njk
, as entry points, then you can set the option to match used files: test: /\.(html|njk)$/
.
The test
value is used by default template loader.
Why is it necessary to define it? Can't it be automatically processed?
This plugin is very powerful and has many experimental features not yet documented.
One of the next features will be the processing scripts and styles as entry points for library bundles without templates.
To do this, the plugin must differentiate between a template entry point and a script/style entry point.
This plugin can completely replace the functionality of mini-css-extract-plugin and webpack-remove-empty-scripts in future.
entry
Type: object
is identical to Webpack entry
plus additional data
property to pass custom variables into the HTML template.
Define all your HTML templates in the entry
option.
An HTML template is a starting point for collecting all the dependencies used in your web application.
Specify source scripts (JS, TS) and styles (CSS, SCSS, LESS, etc.) directly in HTML.
The plugin automatically extracts JS and CSS whose source files are specified in an HTML template.
Simple syntax
The key of an entry object is the output file
w/o extension, relative by the outputPath
option.
The value is the source file
, absolute or relative by the Webpack config file.
{
entry: {
index: 'src/views/home/index.html',
'pages/about/index': 'src/views/about.html',
},
}
Advanced syntax
The entry value might be an object:
type entryValue = {
import: string,
filename: string
data: object,
}
import
- a source file, absolute or relative by the Webpack config filefilename
- an output file, relative by the 'outputPath' optiondata
- an object passed into preprocessor
to render a template with variables
To pass global variables in all templates use the data loader option.
Usage example:
{
entry: {
'pages/about/index': {
import: 'src/views/about.html',
data: {
title: 'About',
}
},
contact: {
import: 'src/views/contact.html',
filename: 'pages/contact/index.html',
},
},
}
Note
You can define templates both in Webpack entry
and in the entry
option of the plugin. The syntax is identical.
But the data
property can only be defined in the entry
option of the plugin.
Entry as a path to templates
Type: string
You can define the entry as a path to recursively detect all templates from that directory.
When the value of the entry
is a string, it must be a relative or absolute path to the templates' directory.
Templates matching the test option are read recursively from the path.
The output files will have the same folder structure as source template directory.
For example, there are files in the template directory ./src/views/pages/
./src/views/pages/index.html
./src/views/pages/about/index.html
./src/views/pages/news/sport/index.html
./src/views/pages/news/sport/script.js
./src/views/pages/news/sport/style.scss
...
Define the entry option as the relative path to pages:
new HtmlBundlerPlugin({
entry: 'src/views/pages/',
})
Internally, the entry is created with the templates matching to the test
option.
Files that are not templates are ignored.
The output HTML filenames keep their source structure relative to the entry path:
{
index: 'src/views/pages/index.html',
'about/index': 'src/views/pages/about/index.html',
'news/sport/index': 'src/views/pages/news/sport/index.html',
}
If you need to modify the output HTML filename, use the filename option as the function.
For example, we want keep a source structure for all pages,
while ./src/views/pages/home/index.html
should not be saved as ./dist/home/index.htm
, but as ./dist/index.htm
:
new HtmlBundlerPlugin({
entry: 'src/views/pages/',
filename: ({ filename, chunk: { name } }) => {
if (name === 'home/index') {
return 'index.html';
}
return '[name].html';
},
})
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 HTML output filename relative by the outputPath
option.
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]
The 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.
js
Type: Object
Default properties:
{
filename: '[name].js',
outputPath: null,
inline: false,
verbose: false,
}
filename
- an output filename of extracted JS. Details see by filename option.outputPath
- an output path of extracted CSS. Details see by outputPath option.inline
- globally inline all extracted JS into HTML, available values:
false
- extract processed JS in an output file, defaultstrue
- inline processed JS into HTML'auto'
- in development
mode - inline JS, in production
mode - extract in a file
verbose
- enable/disable display process information for scripts
The test
property absent because all JS files specified in <script>
tag are automatically detected.
This is the option to extract JS from a script source file specified 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.
Note
To display all extracted JS files, enable the verbose
option.
css
Type: Object
Default properties:
{
test: /\.(css|scss|sass|less|styl)$/,
filename: '[name].css',
outputPath: null,
verbose: false,
}
test
- an RegEpx to process all source styles that pass test assertionfilename
- an output filename of extracted CSS. Details see by filename option.outputPath
- an output path of extracted CSS. Details see by outputPath option.inline
- globally inline all extracted CSS into HTML, available values:
false
- extract processed CSS in an output file, defaultstrue
- inline processed CSS into HTML via style
tag'auto'
- in development
mode - inline CSS, in production
mode - extract in a file
verbose
- enable/disable display process information for styles
This is the option to extract CSS from a style source file specified in the HTML tag:
<link href="./style.scss" rel="stylesheet">
Warning
Don't import source styles in JavaScript! Styles must be specified directly in HTML.
Don't define source JS files in Webpack entry! Scripts must be specified 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.
Note
To display all extracted CSS files, enable the verbose
option.
postprocess
Type:
type postprocess = (
content: string,
info: ResourceInfo,
compilation: Compilation,
) => string|null;
type ResourceInfo = {
verbose: boolean,
isEntry: boolean,
filename:
| string
| ((pathData: PathData) => string),
sourceFile: string,
outputPath: string,
assetFile: string,
};
Default: null
Called after a source of an asset module is rendered, but not yet processed by other plugins.
The postprocess
have the following arguments:
content: string
- a content of processed fileinfo: ResourceInfo
- an info about current filecompilation: Compilation
- the Webpack compilation object
The ResourceInfo
have the following properties:
verbose: boolean
- whether information should be displayedisEntry: boolean
- if is true
, the resource is the entry point, otherwise is a resource loaded in the entry pointfilename: string|function
- a filename of the resource, see filenamesourceFile: string
- a full path of the source fileoutputPath: string
- a full path of the output directoryassetFile: string
- an output asset file relative by outputPath
Return new content as a string
.
If return null
, the result processed via Webpack plugin is ignored and will be saved a result processed via the loader.
minify
Type: Object|string|boolean
Default: false
For minification generated HTML is used the html-minifier-terser with the following default options
:
{
collapseWhitespace: true,
keepClosingSlash: true,
removeComments: true,
removeRedundantAttributes: true,
removeScriptTypeAttributes: true,
removeStyleLinkTypeAttributes: true,
useShortDoctype: true,
minifyCSS: true,
minifyJS: true,
}
Possible values:
false
- disable minificationtrue
- enable minification with default optionsauto
- in development
mode disable minification, in production
mode enable minification with default options,
use minifyOptions to customize options{}
- enable minification with custom options, this object are merged with default options
see options reference
minifyOptions
Type: Object
Default: null
When the minify option is set to auto
, you can configure minification options using the minifyOptions
.
Type: boolean
Default: false
Enable/disable extracting comments from source scripts to the *.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
.
verbose
Type: string|boolean
Default: false
The verbose option allow to display detailed processing information in console.
Possible values:
false
- do not display informationtrue
- display informationauto
- in development
mode enable verbose, in production
mode disable verbose
Note
If you want to colorize the console output in your app, use the best Node.js lib ansis.
watchFiles
Type:
type watchFiles = {
paths?: Array<string>;
files?: Array<RegExp>;
ignore?: Array<RegExp>;
}
Default:
watchFiles: {
paths: ['./src'],
files: [/\.(html|ejs|eta)$/],
ignore: [
/[\\/](node_modules|dist|test)$/,
/[\\/]\..+$/,
/package(?:-lock)*\.json$/,
/webpack\.(.+)\.js$/,
/\.(je?pg|png|ico|webp|svg|woff2?|ttf|otf|eot)$/,
],
}
Allow to configure paths and files to watch file changes for rebuild in watch
or serv
mode.
Note
To watch changes with a live reload
in the browser, you must additionally configure the watchFiles
in devServer
,
see setup HMR.
Properties:
-
paths
- A list of relative or absolute paths to directories where should be watched files
.
The watching path for each template defined in the entry will be autodetect as the first level subdirectory of the template relative to the project's root path.
E.g., the template ./src/views/index.html
has the watching path of ./src
.
-
files
- Watch the files specified in paths
, except ignore
, that match the regular expressions.
Defaults, are watched only files that match the test
plugin option.
-
ignore
- Ignore the specified paths or files, that match the regular expressions.
For example, all source files are in the ./src
directory,
while some partials included in a template are in ./vendor/
directory, then add it to the paths
:
watchFiles: {
paths: ['vendor'],
},
If you want watch changes in some special files used in your template that are only loaded through the template engine,
add them to the files
property:
watchFiles: {
paths: ['vendor'],
files: [
/data\.(js|json)$/,
],
},
To exclude watching of files defined in paths
and files
, you can use the ignore
property.
This option has the prio over paths and files.
Note
To display all watched files, enable the verbose
option.
loaderOptions
This is the reference to the loader options.
You can specify loader options here in the plugin options to avoid explicitly defining the HtmlBundlerPlugin.loader
in module.rules
.
The HtmlBundlerPlugin.loader
will be added automatically.
For example, both configurations are functionally identical:
1) the variant using the loaderOptions
(recommended for common use cases)
const HtmlBundlerPlugin = require('html-bundler-webpack-plugin');
module.exports = {
plugins: [
new HtmlBundlerPlugin({
entry: {
index: 'src/views/index.ejs',
},
loaderOptions: {
sources: [{ tag: 'img', attributes: ['data-src', 'data-srcset'], }],
preprocessor: 'ejs',
},
}),
],
};
2) the variant using the module.rules
const HtmlBundlerPlugin = require('html-bundler-webpack-plugin');
module.exports = {
plugins: [
new HtmlBundlerPlugin({
entry: {
index: 'src/views/index.ejs',
},
}),
],
module: {
rules: [
{
test: /.(html|ejs)$/,
loader: HtmlBundlerPlugin.loader,
options: {
sources: [{ tag: 'img', attributes: ['data-src', 'data-srcset'], }],
preprocessor: 'ejs',
},
},
],
},
};
For common use cases, the first option is recommended. So your config is smaller and cleaner.
The second variant use only for special cases, e.g. when you have templates with different syntax.
An example see by How to use some different template engines.
Note
Options defined in module.rules
take precedence over the same options defined in loaderOptions
.
Loader options
The default loader
:
{
test: /.(html|ejs|eta)$/,
loader: HtmlBundlerPlugin.loader,
}
You can omit the loader in Webpack modules.rules
.
If the HtmlBundlerPlugin.loader
is not configured, the plugin add it with default options automatically.
The default loader handels HTML files and EJS
-like templates.
Note
It is recommended to define all loader options in the loaderOptions
by the plugin options
to keep the webpack config clean and smaller.
Warning
The plugin works only with the own loader HtmlBundlerPlugin.loader
.
Do not use another loader.
This loader replaces the functionality of html-loader
and many other template loaders.
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 tags and attributes:
Tag | Attributes |
---|
link | href for type="text/css" rel="stylesheet" as="style" as="script"
imagesrcset for as="image" |
script | src |
img | src srcset |
image | href xlink:href |
use | href xlink:href |
input | src (for type="image" ) |
source | src srcset |
audio | src |
track | src |
video | src poster |
object | data |
Warning
It is not recommended to use the deprecated xlink:href
attribute by the image
and use
tags.
The filter
is called for all attributes of the tag defined as defaults and in sources
option.
The argument is an object containing the properties:
tag: string
- a name of the HTML tagattribute: string
- a name of the HTML attributevalue: string
- a value of the HTML attributeattributes: string
- all attributes of the tagresourcePath: string
- a path of the HTML template
The processing of an attribute can be ignored by returning false
.
To disable the processing of all attributes, set the sources
option as false
.
Note
Automatically are processed only attributes containing a relative path or Webpack alias:
src="./image.png"
or src="image.png"
- an asset in the local directorysrc="../../assets/image.png"
- a relative path to parent directorysrc="@images/image.png"
- an image directory as Webpack alias
Url values are not processed:
src="https://example.com/img/image.png"
src="//example.com/img/image.png"
src="/img/image.png"
Others not file values are ignored, e.g.:
src="data:image/png; ..."
src="javascript: ..."
Examples of using argument properties:
{
tag: 'img',
filter: ({ tag, attribute, value, attributes, resourcePath }) => {
if (attribute === 'src') return false;
if (value.endsWith('.webp')) return false;
if ('srcset' in attributes && attributes['srcset'] === '') return false;
if (resourcePath.indexOf('example')) return false;
},
}
The default sources can be extended with 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 the processing only specific attributes.
For example, allow processing only for images in content
attribute of the meta
tag:
<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: ({ attributes }) => {
const allowedNames = ['twitter:image', 'logo'];
if ('name' in attributes && allowedNames.indexOf(attributes.name) < 0) {
return false;
}
},
}
],
},
},
],
},
};
The 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 = 'eta' | 'ejs' | 'handlebars';
The default value is 'eta'
, see Eta templating engine.
The npm package eta
is already installed with this plugin.
The Eta
has the EJS-like syntax, is only 2KB gzipped and is much fasted than EJS.
If the preprocessor
is a string value one of listed above, will be used the preconfigured templating compiler.
You can pass a custom options of the template engine using the preprocessorOptions.
For example, if you have EJS
templates:
install npm package ejs
npm i -D ejs
set the preprocessor
as ejs
const HtmlBundlerPlugin = require('html-bundler-webpack-plugin');
module.exports = {
plugins: [
new HtmlBundlerPlugin({
entry: {
index: 'src/views/pages/home/index.ejs',
},
loaderOptions: {
preprocessor: 'ejs',
},
}),
],
};
Type:
type preprocessor = (
template: string,
loaderContext: LoaderContext
) => string|Promise;
To use any templating engine you can define the preprocessor
as a function.
For example, the default preprocessor
is defined as following function:
const config = {
async: false,
useWith: true,
root: process.cwd(),
};
preprocessor = (template, { data }) => Eta.render(template, data, config);
The template
is the raw content of a template file defined in the entry
option.
The loaderContext
is the Loader Context object contained useful properties:
mode: string
- a Webpack mode: production
, development
, none
rootContext: string
- a path to Webpack contextresource: string
- a template file, including queryresourcePath: string
- a template filedata: object|null
- variables passed form entry
The preprocessor is called for each entry file, before processing of the content.
This function can be used to compile the template with any template engine,
such as Eta, EJS, Handlebars, Nunjucks, etc.
Returns new content as a string
or Promise
for async processing.
The usage example for your own sync
render function:
{
preprocessor: (template, { data }) => render(template, data)
}
The example of using Promise
for your own async
render function:
{
preprocessor: (template, { data }) =>
new Promise((resolve) => {
const result = render(template, data);
resolve(result);
})
}
Note
The plugin supports EJS
-like templates "out of the box" therefore the HtmlBundlerPlugin.loader
can be omitted in the Webpack config.
preprocessorOptions
Type: Object
Default: {}
With the preprocessorOptions
you can pass template engine options when used the preprocessor as the string: eta
ejs
or handlebars
.
Each preprocessor has its own options depend on using template engine.
Options for preprocessor: 'eta'
(default)
loaderOptions: {
preprocessor: 'eta',
preprocessorOptions: {
async: false,
useWith: true,
root: path.join(__dirname, 'src/views/'),
views: [
path.join(__dirname, 'src/views/includes'),
path.join(__dirname, 'src/views/partials'),
],
},
},
The complete list of options see here.
For example, there are template page and partials:
src/views/page/home.html
src/views/includes/gallery.html
src/views/includes/teaser.html
src/views/partials/footer.html
src/views/partials/menu/nav.html
src/views/partials/menu/top/desktop.html
Include the partials in the src/views/page/home.html
template with the includeFile()
:
<%~ includeFile('/includes/gallery.html') %>
<%~ includeFile('teaser.html') %>
<%~ includeFile('menu/nav.html') %>
<%~ includeFile('menu/top/desktop.html') %>
<%~ includeFile('footer.html') %>
If you have partials with .eta
extensions, then the extension can be omitted.
Options for preprocessor: 'ejs'
loaderOptions: {
preprocessor: 'ejs',
preprocessorOptions: {
async: false,
root: path.join(__dirname, 'src/views/'),
views: [
path.join(__dirname, 'src/views/includes'),
path.join(__dirname, 'src/views/partials'),
],
},
},
The complete list of options see here.
For example, there are template page and partials:
src/views/page/home.html
src/views/includes/gallery.html
src/views/includes/teaser.html
src/views/partials/footer.html
src/views/partials/menu/nav.html
src/views/partials/menu/top/desktop.html
Include the partials in the src/views/page/home.html
template with the include()
:
<%- include('/includes/gallery.html') %>
<%- include('teaser.html') %>
<%- include('menu/nav.html') %>
<%- include('menu/top/desktop.html') %>
<%- include('footer.html') %>
If you have partials with .ejs
extensions, then the extension can be omitted.
Options for preprocessor: 'handlebars'
The preprocessor
has built-in include
helper, to load a partial file directly in a template without registration of partials.
The include
helper has the following de facto standard options:
loaderOptions: {
preprocessor: 'handlebars',
preprocessorOptions: {
root: path.join(__dirname, 'src/views/'),
views: [
path.join(__dirname, 'src/views/includes'),
path.join(__dirname, 'src/views/partials'),
],
},
},
For example, there are template page and partials:
src/views/page/home.html
src/views/includes/gallery.html
src/views/includes/teaser.html
src/views/partials/footer.html
src/views/partials/menu/nav.html
src/views/partials/menu/top/desktop.html
Include the partials in the src/views/page/home.html
template with the include
helper:
{{ include '/includes/gallery' }}
{{ include 'teaser' }}
{{ include 'menu/nav' }}
{{ include 'menu/top/desktop' }}
{{ include 'footer' }}
The include
helper automatically resolves .hthm
and .hbs
extensions, it can be omitted.
Handlebars partials
Type: Array<string>|Object
Default: []
If you use the partials syntax {{> footer }}
to include a file, then use the partials
option.
Partials will be auto-detected in paths recursively and registered under their relative paths, without an extension.
loaderOptions: {
preprocessor: 'handlebars',
preprocessorOptions: {
partials: [
'src/views/includes',
path.join(__dirname, 'src/views/partials'),
],
},
},
For example, if the partial path is the src/views/partials
then the file src/views/partials/menu/top/desktop.html
will have the partial name menu/top/desktop
.
You can define all partials manually using the option as an object:
loaderOptions: {
preprocessor: 'handlebars',
preprocessorOptions: {
partials: {
gallery: path.join(__dirname, 'src/views/includes/gallery.html'),
teaser: path.join(__dirname, 'src/views/includes/teaser.html'),
footer: path.join(__dirname, 'src/views/partials/footer.html'),
'menu/nav': path.join(__dirname, 'src/views/partials/menu/nav.html'),
'menu/top/desktop': path.join(__dirname, 'src/views/partials/menu/top/desktop.html'),
},
},
},
Include the partials in the src/views/page/home.html
template:
{{> gallery }}
{{> teaser }}
{{> menu/nav }}
{{> menu/top/desktop }}
{{> footer }}
Handlebars helpers
Type: Array<string>|Object
Default: []
When the helpers
is an array of relative or absolute paths to helpers,
then the name of a helper is the relative path to the helper file without an extension.
For example, there are helper files:
src/views/helpers/bold.js
src/views/helpers2/italic.js
src/views/helpers2/wrapper/span.js
The preprocessor options:
loaderOptions: {
preprocessor: 'handlebars',
preprocessorOptions: {
helpers: [
'src/views/helpers',
'src/views/helpers2',
],
},
},
Usage of helpers:
{{#bold}}The bold text.{{/bold}}
{{#italic}}The italic text.{{/italic}}
{{#[wrapper/span]}}The text wrapped with span tag.{{/[wrapper/span]}}
Note
- The helper located in a subdirectory, e.g.
wrapper/span.js
will be available in template as [wrapper/span]
. - When helper name contain the
/
slash, then the helper name must be wrapped with the []
.
You can define helpers manually using name: function
object:
loaderOptions: {
preprocessor: 'handlebars',
preprocessorOptions: {
helpers: {
bold: (options) => new Handlebars.SafeString(`<strong>${options.fn(this)}</strong>`),
},
},
},
The Handlebars compile
options see here.
data
Type: Object
Default: {}
The properties defined in the data
loader option are available as variables in all templates defined in the entry
option.
Use this option to pass the global variable into all templates.
To pass page variables to a specific template, use the data
property of the entry option.
Note
The entry
data property overrides the same property of loader data
.
For example, there are variables defined in both the entry
property and the loader option:
const HtmlBundlerPlugin = require('html-bundler-webpack-plugin');
module.exports = {
plugins: [
new HtmlBundlerPlugin({
entry: {
index: {
import: './src/home.html',
data: {
title: 'Home',
headline: 'Homepage',
},
},
about: './src/about.html',
},
loaderOptions: {
data: {
title: 'Default Title',
globalData: 'Global Data',
},
},
}),
],
};
In the ./src/home.html
template are available following variables:
{
title: 'Home',
headline: 'Homepage',
globalData: 'Global Data',
}
In the ./src/about.html
template are available following variables:
{
title: 'Default Title',
globalData: 'Global Data',
}
Template engines
Using the preprocessor, you can compile any template with a template engine such as:
Note
For Pug templates use the pug-plugin.
This plugin works on the same codebase but has additional Pug-specific options and features.
Using the Eta
Supported "out of the box"
Eta
is compatible* with EJS
syntax, is smaller and faster than EJS
.
For example, there is the template src/views/page/index.eta
<html>
<body>
<h1><%= headline %></h1>
<ul class="people">
<% for (let i = 0; i < people.length; i++) {%>
<li><%= people[i] %>></li>
<% } %>
</ul>
<%~ includeFile('/src/views/partials/footer') %>
</body>
</html>
The minimal Webpack config:
const HtmlBundlerPlugin = require('html-bundler-webpack-plugin');
module.exports = {
plugins: [
new HtmlBundlerPlugin({
entry: {
index: {
import: './src/views/page/index.eta',
data: {
headline: 'Breaking Bad',
people: ['Walter White', 'Jesse Pinkman'],
},
},
},
}),
],
};
The default preprocessor is eta
, you can omit it:
new HtmlBundlerPlugin({
loaderOptions: {
preprocessor: 'eta',
},
})
For eta
preprocessor options see here.
Warning
For compatibility the Eta compiler with the EJS templates, the default preprocessor use the useWith: true
Eta option
to use variables in template without the Eta-specific it.
scope
Using the EJS
You need to install the ejs
package:
npm i -D ejs
For example, there is the template src/views/page/index.ejs
<html>
<body>
<h1><%= headline %></h1>
<ul class="people">
<% for (let i = 0; i < people.length; i++) {%>
<li><%= people[i] %>></li>
<% } %>
</ul>
<%- include('/src/views/partials/footer.html'); %>
</body>
</html>
Define the preprocessor
as ejs
:
const HtmlBundlerPlugin = require('html-bundler-webpack-plugin');
module.exports = {
plugins: [
new HtmlBundlerPlugin({
entry: {
index: {
import: './src/views/page/index.ejs',
data: {
headline: 'Breaking Bad',
people: ['Walter White', 'Jesse Pinkman'],
},
},
},
loaderOptions: {
preprocessor: 'ejs',
},
}),
],
};
For ejs
preprocessor options see here.
Using the Handlebars
You need to install the handlebars
package:
npm i -D handlebars
For example, there is the template src/views/page/index.hbs
<html>
<body>
<h1>{{ headline }}!</h1>
<ul class="people">
{{#each people}}
<li>{{this}}</li>
{{/each}}
</ul>
{{ include '/src/views/partials/footer.html' }}
</body>
</html>
Define the preprocessor
as handlebars
:
const HtmlBundlerPlugin = require('html-bundler-webpack-plugin');
module.exports = {
plugins: [
new HtmlBundlerPlugin({
entry: {
index: {
import: './src/views/page/index.hbs',
data: {
headline: 'Breaking Bad',
people: ['Walter White', 'Jesse Pinkman'],
},
},
},
loaderOptions: {
preprocessor: 'handlebars',
},
}),
],
};
For handlebars
preprocessor options see here.
Using the Mustache
You need to install the mustache
package:
npm i -D mustache
For example, there is the template src/views/page/index.mustache
<html>
<body>
<h1>{{ headline }}</h1>
<ul class="people">
{{#people}}
<li>{{.}}</li>
{{/people}}
</ul>
</body>
</html>
Add the template compiler to preprocessor
:
const HtmlBundlerPlugin = require('html-bundler-webpack-plugin');
const Mustache = require('mustache');
module.exports = {
plugins: [
new HtmlBundlerPlugin({
test: /\.(html|mustache)$/,
index: {
import: './src/views/page/index.mustache',
data: {
headline: 'Breaking Bad',
people: ['Walter White', 'Jesse Pinkman'],
},
},
loaderOptions: {
preprocessor: (template, { data }) => Mustache.render(template, data),
},
}),
],
};
Using the Nunjucks
You need to install the nunjucks
package:
npm i -D nunjucks
For example, there is the template src/views/page/index.njk
<html>
<body>
<h1>{{ headline }}!</h1>
<ul class="people">
{% for name in people %}
<li class="name">{{ name }}</li>
{% endfor %}
</ul>
</body>
</html>
Define the preprocessor
as nunjucks
:
const HtmlBundlerPlugin = require('html-bundler-webpack-plugin');
module.exports = {
plugins: [
new HtmlBundlerPlugin({
test: /\.(html|njk)$/,
entry: {
index: {
import: './src/views/page/index.njk',
data: {
headline: 'Breaking Bad',
people: ['Walter White', 'Jesse Pinkman'],
},
},
},
loaderOptions: {
preprocessor: 'nunjucks',
},
}),
],
};
Using the LiquidJS
You need to install the liquidjs
package:
npm i -D liquidjs
For example, there is the template src/views/page/index.liquidjs
<html>
<body>
<h1>{{ headline }}!</h1>
<ul class="people">
{% for name in people %}
<li class="name">{{ name }}</li>
{% endfor %}
</ul>
</body>
</html>
Add the template compiler to preprocessor
:
const HtmlBundlerPlugin = require('html-bundler-webpack-plugin');
const { Liquid } = require('liquidjs');
const LiquidEngine = new Liquid();
module.exports = {
plugins: [
new HtmlBundlerPlugin({
test: /\.(html|liquidjs)$/,
entry: {
index: {
import: './src/views/page/index.liquidjs',
data: {
headline: 'Breaking Bad',
people: ['Walter White', 'Jesse Pinkman'],
},
},
},
loaderOptions: {
preprocessor: (content, { data }) => LiquidEngine.parseAndRender(content, data),
},
}),
],
};
Setup HMR (Live Reload)
To enable live reload after changes add in the Webpack config the devServer
option:
module.exports = {
devServer: {
static: {
directory: path.join(__dirname, 'dist'),
},
watchFiles: {
paths: ['src/**/*.*'],
options: {
usePolling: true,
},
},
},
};
How to keep source folder structure in output directory
Define the entry
option as a path to templates. For details see the entry path.
How to use source images in HTML
Add to Webpack config the rule:
module: {
rules: [
{
test: /\.(png|jpe?g|ico|svg)$/,
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 resize and generate responsive images
To resize or generate responsive images is recommended to use the responsive-loader.
Install additional packages:
npm i -D responsive-loader sharp
To resize an image use the query parameter size
:
<img src="./image.png?size=640">
To generate responsible images use in srcset
attribute the query parameter sizes
als JSON5
to avoid parsing error,
because many images must be separated by commas ,
but we use the comma to separate sizes for one image:
<img src="./image.png?size=480"
srcset="./image.png?{sizes:[320,480,640]}">
You can convert source image to other output format.
For example, we have original image 2000px width as PNG and want to resize to 640px and save as WEBP:
<img src="./image.png?size=640&format=webp">
You can create a small inline image placeholder. To do this, use the following query parameters:
placeholder=true
- enable to generate the placeholderplaceholderSize=35
- the size of the generating placeholderprop=placeholder
- the plugin-specific prop
parameter retrieves the property from the object generated by responsive-loader
<img src="./image.png?placeholder=true&placeholderSize=35&prop=placeholder"
srcset="./image.png?{sizes:[320,480,640]}">
The generated HTML:
<img src="data:image/png;base64,iVBORw0K ..."
srcset="/img/image-320w.png 320w,/img/image-480w.png 480w,/img/image-640w.png 640w">
Add to Webpack config the rule for responsive images:
module.exports = {
module: {
rules: [
{
test: /\.(png|jpe?g|webp)$/,
type: 'asset/resource',
use: {
loader: 'responsive-loader',
options: {
name: 'assets/img/[name]-[width]w.[ext]',
sizes: [640],
},
},
},
],
},
};
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
There are two ways to inline CSS in HTML:
- inline all CSS globally with
css.inline
option - inline single CSS with
?inline
query added to a filename
The inline
option can take the following values: false
, true
and 'auto'
.
For details see the inline option.
Note
The individual ?inline
query parameter takes precedence over the globally css.inline
option.
For example, if css.inline = true
and in HTML a single file has the ?inline=false
query,
this file will be extracted in an output file, while all other styles will be inlined.
For example, there are two SCSS files:
main.scss
$bgColor: steelblue;
body {
background-color: $bgColor;
}
style.scss:
$color: red;
h1 {
color: $color;
}
There is the ./src/views/index.html with both style files:
<html>
<head>
<link href="./main.scss" rel="stylesheet" />
<link href="./style.scss" rel="stylesheet" />
</head>
<body>
<h1>Hello World!</h1>
</body>
</html>
To inline all CSS globally add the css.inline
option:
const HtmlBundlerPlugin = require('html-bundler-webpack-plugin');
module.exports = {
plugins: [
new HtmlBundlerPlugin({
entry: {
index: 'src/views/index.html',
},
css: {
inline: true,
filename: 'css/[name].[contenthash:8].css',
},
}),
],
module: {
rules: [
{
test: /\.(css|sass|scss)$/,
use: ['css-loader', 'sass-loader'],
},
],
},
};
The generated HTML contains inlined CSS:
<html>
<head>
<style>
body{ background-color: steelblue; }
</style>
<style>
h1{ color: red; }
</style>
</head>
<body>
<h1>Hello World!</h1>
</body>
</html>
To inline a single CSS, add the ?inline
query to a style file 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
There are two ways to inline CSS in HTML:
- inline all JS globally with
js.inline
option - inline single JS with
?inline
query added to a filename
The inline
option can take the following values: false
, true
and 'auto'
.
For details see the inline option.
Note
The individual ?inline
query parameter takes precedence over the globally js.inline
option.
For example, if js.inline = true
and in HTML a single file has the ?inline=false
query,
this file will be extracted in an output file, while all other scripts will be inlined.
For example, there are two JS files:
main.js
console.log('>> main.js');
script.js
console.log('>> script.js');
There is the ./src/views/index.html with both script files:
<html>
<head>
<script src="./main.js" defer="defer"></script>
</head>
<body>
<h1>Hello World!</h1>
<script src="./script.js"></script>
</body>
</html>
To inline all JS globally add the js.inline
option:
const HtmlBundlerPlugin = require('html-bundler-webpack-plugin');
module.exports = {
plugins: [
new HtmlBundlerPlugin({
entry: {
index: 'src/views/index.html',
},
js: {
inline: true,
filename: 'js/[name].[contenthash:8].js',
},
}),
],
};
The generated HTML contains inlined JS scripts:
<html>
<head>
<script>
(()=>{"use strict";console.log(">> main.js")})();
</script>
</head>
<body>
<h1>Hello World!</h1>
<script>
(()=>{"use strict";console.log(">> script.js")})();
</script>
</body>
</html>
To inline a single JS file, add the ?inline
query to a script file 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(">> script.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 the 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 process a PHP template
The plugin can replace the source filenames of scripts, styles, images, etc. with output filenames in a PHP template.
For example, there is the PHP template src/views/index.phtml:
<?php
$title = 'Home';
?>
<html>
<head>
<title><?= $title ?></title>
<link href="./style.css" rel="stylesheet">
<script src="./main.js" defer="defer"></script>
</head>
<body>
<h1>Hello World!</h1>
</body>
</html>
The PHP template should not be compiled into pure HTML, but only should be processed the source assets.
In this case, the preprocessor
must be disabled.
module.exports = {
output: {
path: path.join(__dirname, 'dist/'),
},
plugins: [
new HtmlBundlerPlugin({
test: /\.(php|phtml)$/i,
filename: '[name].phtml',
entry: {
index: './src/views/index.phtml',
},
js: {
filename: 'assets/js/[name].[contenthash:8].js',
},
css: {
filename: 'assets/css/[name].[contenthash:8].css',
},
loaderOptions: {
preprocessor: false,
},
}),
],
};
The processed PHP template:
<?php
$title = 'Home';
?>
<html>
<head>
<title><?= $title ?></title>
<link href="assets/css/style.026fd625.css" rel="stylesheet">
<script src="assets/js/main.3347618e.js" defer="defer"></script>
</head>
<body>
<h1>Hello World!</h1>
</body>
</html>
How to pass data into multiple templates
You can pass variables into template using a template engine, e.g. Handlebars.
For multiple page configuration, better to use the Nunjucks template 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 HtmlBundlerPlugin = require('html-bundler-webpack-plugin');
const Nunjucks = require('nunjucks');
const entryData = {
home: {
title: 'Home',
filmTitle: 'Breaking Bad',
location: 'Albuquerque, New Mexico',
imageFile: 'map.png',
},
about: {
title: 'About',
actors: [
{
firstname: 'Walter',
lastname: 'White, "Heisenberg"',
},
{
firstname: 'Jesse',
lastname: 'Pinkman',
},
],
},
};
module.exports = {
resolve: {
alias: {
'@images': path.join(__dirname, 'src/assets/images'),
},
},
plugins: [
new HtmlBundlerPlugin({
entry: {
index: {
import: 'src/views/pages/home/index.html',
data: entryData.home,
},
about: {
import: 'src/views/pages/about/index.html',
data: entryData.about,
},
},
js: {
filename: 'assets/js/[name].[contenthash:8].js',
},
css: {
filename: 'assets/css/[name].[contenthash:8].css',
},
loaderOptions: {
preprocessor: (template, { data }) => Nunjucks.renderString(template, data),
},
}),
],
module: {
rules: [
{
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>
How to use some different template engines
When you have many templates with different syntax, you can use a separate module rules for each template engine.
For example, in your project are mixed templates with EJS and Handlebars syntax.
- src/views/ejs/home.ejs
- src/views/hbs/about.hbs
To handle different templates, define the test
plugin option that must match those templates and
add a preprocessor for each template type in the module rules.
const HtmlBundlerPlugin = require('html-bundler-webpack-plugin');
const ejs = require('ejs');
const Handlebars = require('handlebars');
module.exports = {
plugins: [
new HtmlBundlerPlugin({
test: /\.(ejs|hbs)$/,
entry: {
index: 'src/views/ejs/home.ejs',
about: 'src/views/hbs/about.hbs',
},
}),
],
module: {
rules: [
{
test: /\.ejs$/,
loader: HtmlBundlerPlugin.loader,
options: {
preprocessor: (content, { data }) => ejs.render(content, data),
},
},
{
test: /\.hbs$/,
loader: HtmlBundlerPlugin.loader,
options: {
preprocessor: (content, { data }) => Handlebars.compile(content)(data),
},
},
],
},
};
How to config splitChunks
Webpack tries to split every entry file, include template files, which completely breaks the compilation process in the plugin.
To avoid this issue, you must specify which scripts should be split, using optimization.splitChunks.cacheGroups
:
module.exports = {
optimization: {
splitChunks: {
cacheGroups: {
scripts: {
test: /\.(js|ts)$/,
chunks: 'all',
},
},
},
},
};
Note
In the test
option must be specified all extensions of scripts which should be split.
See details by splitChunks.cacheGroups.
For example, in a template are used the scripts and styles from node_modules
:
<html>
<head>
<title>Home</title>
<link href="bootstrap/dist/css/bootstrap.min.css" rel="stylesheet">
<script src="bootstrap/dist/js/bootstrap.min.js" defer="defer"></script>
</head>
<body>
<h1>Hello World!</h1>
<script src="./main.js"></script>
</body>
</html>
Note
In the generated HTML all script tags remain in their original places and split chunks will be added there,
in the order that Webpack generated.
In this use case the optimization.cacheGroups.{cacheGroup}.test
option must match exactly only JS files from node_modules
:
module.exports = {
optimization: {
runtimeChunk: 'single',
splitChunks: {
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/].+\.(js|ts)$/,
name: 'vendor',
chunks: 'all',
},
},
},
},
};
Warning
Splitting CSS to many chunk is principal impossible. Splitting works only for JS files.
If you use vendor styles in your style file, e.g.:
style.scss
@use "bootstrap/scss/bootstrap";
body {
color: bootstrap.$primary;
}
Then vendor styles will not be saved to a separate file, because sass-loader
generates one CSS bundle code.
Therefore vendor styles should be loaded in a template separately.
Warning
If you will to use the test
as /[\\/]node_modules[\\/]
, without extension specification,
then Webpack concatenates JS code together with CSS in one file,
because Webpack can't differentiate CSS module from JS module, therefore you MUST match only JS files.
If you want save module styles separate from your styles, then load them in a template separately:
<html>
<head>
<title>Home</title>
<link href="bootstrap/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="./style.scss" rel="stylesheet">
</head>
<body>
<h1>Hello World!</h1>
<script src="./main.js"></script>
</body>
</html>
How to split multiple node modules and save under own names
If you use many node modules and want save each module to separate file then use optimization.cacheGroups.{cacheGroup}.name
as function.
For example, many node modules are imported in the main.js
:
import { Button } from 'bootstrap';
import _, { map } from 'underscore';
There is a template used the main.js
./src/views/index.html:
<html>
<head>
<script src="./main.js" defer="defer"></script>
</head>
<body>
<h1>Hello World!</h1>
</body>
</html>
Then, use the optimization.splitChunks.cacheGroups.{cacheGroup}.name
as following function:
const HtmlBundlerPlugin = require('html-bundler-webpack-plugin');
module.exports = {
plugins: [
new HtmlBundlerPlugin({
entry: {
index: 'src/views/index.html',
},
js: {
filename: 'js/[name].[contenthash:8].js',
},
}),
],
optimization: {
runtimeChunk: 'single',
splitChunks: {
chunks: 'all',
minSize: 10000,
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/].+\.(js|ts)$/,
name(module, chunks, groupName) {
const moduleName = module.resourceResolveData.descriptionFileData.name.replace('@', '');
return `${groupName}.${moduleName}`;
},
},
},
},
},
};
The split files will be saved like this:
dist/js/vendor.popperjs/core.f96a1152.js <- `popperjs/core` is extracted from bootstrap
dist/js/vendor.bootstrap.f69a4e44.js
dist/js/vendor.underscore.4e44f69a.js
dist/js/runtime.9cd0e0f9.js <- common runtime code
dist/js/script.3010da09.js
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