Repository navigation
Built-in HTML minifier corrupts copy-webpack-plugin .html assets containing {% %} template statements #22013
Description
Have you used AI?
Yes
Which part of webpack is affected?
html
Bug Description
webpack 5.110.0 added a built-in HTML minifier that is on by default in mode: 'production'. It reserializes every .html asset, including files that copy-webpack-plugin passes through unchanged. When such a file contains {% ... %} server-side template statements (the statement delimiter of the Django/Jinja family: Jinja2, Django, Twig, Liquid, Nunjucks, HubSpot HubL), the minifier drops a %} delimiter and emits invalid template markup from valid input.
No HTML loader, html-webpack-plugin, or explicitly configured minimizer is involved. Just mode: 'production' and copy-webpack-plugin copying a .html file.
%} tokenizes as a valid HTML attribute name (the attribute-name state accepts every character except whitespace, /, >, and =), so {% if x %} y {% endif %} produces two %} attribute-name tokens on the same element, and the minifier deduplicates the repeat and loses one %}.
Link to Minimal Reproduction and steps to reproduce
https://github.com/davidding/webpack-5110-html-minify-repro
npm installnpm run verify(runswebpackthen prints each src/*.html next to its dist/ output)
Expected Behavior
The minifier does not alter content inside {% ... %} in a way that changes it.
If template-awareness is out of scope, two alternatives would each resolve this:
- HTML minification of assets that webpack only copied through (rather than produced from an HTML-emitting module) is opt-in rather than on-by-default.
- The
5.110.0change is documented as breaking, with the migration path and the workarounds called out in release notes.
Actual Behavior
The %} delimiter is dropped, producing invalid markup:
== src/corrupt-conditional-attribute.html ==
in : <input type="text"{% if required %} required{% endif %}>
out: <input type=text {% if required %} required{% endif>
== src/corrupt-space-inside-conditional.html ==
in : <div class="btn{% if active %} btn-active{% endif %}"></div>
out: <div class="btn{% if active %} btn-active{% endif"></div>
== src/survive-space-outside-conditional.html == (control: unchanged)
in : <div class="btn {% if active %}btn-active{% endif %}"></div>
out: <div class="btn {% if active %}btn-active{% endif %}"></div>
The type="text" to type=text quote removal is valid minification and is not the problem. The problem is the dropped %}.
Two distinct mechanisms drop the repeated %}:
- Duplicate attribute-name removal (lib/html/syntax.js#L8520-L8528 @ v5.110.3), spec-mandated. Corrupts the conditional-attribute case.
- Token-list deduplication (
_normalizeTokenList, lib/html/syntax.js#L10200-L10231 @ v5.110.3), webpack's ownclass-attribute minify transform, which a browser does not perform. Corrupts theclasscase.
Environment
System:
OS: macOS 26.6.2
CPU: (12) arm64 Apple M3 Pro
Memory: 18.00 GB
Binaries:
Node: 24.19.0
npm: 11.17.0
Packages:
copy-webpack-plugin: 14.0.0
webpack: 5.110.3
webpack-cli: 7.2.3Is this a regression?
Yes (please specify version below)
Last Working Version
v5.109.2
Additional Context
Byte-identical (correct) output on 5.109.2, the final 5.109.x release, which predates the built-in minifier. Identical corruption on 5.110.0, 5.110.1, 5.110.2, and 5.110.3.
Workarounds (verified against the reproduction on 5.110.3):
optimization: { minimize: { html: false } }disables HTML minification only; JS/CSS minification stays on. Schema option added in5.110.0.info: { minimized: true }on thecopy-webpack-pluginpattern marks the copied assets already-minimized so the minifier skips them. Version-agnostic (works pre-5.110too), so it is the right choice for a shared config that must support both.experiments: { html: false }turns off the built-in HTML pipeline entirely.- Pin
webpack < 5.110. Stopgap only.
Metadata
Metadata
Assignees
Labels
Type
Fields
Priority
Effort
Activity
alexander-akait commentedon Sep 8, 2026
That is expected, it is not a part of html syntax, you should exclude them
alexander-akait commentedon Sep 8, 2026
But take me time to think how we can avoid such problems
fix(html): keep a repeated token list token unless the list is sorted
fix(html): keep an attribute name or list token spelling a foreign de…
fix(html): keep an attribute name or list token spelling a foreign de…
fix(html): keep an attribute name or list token spelling a foreign de…
alexander-akait commentedon Sep 8, 2026
Fixed in the main, but it is still not html so some other parts still can be aerialized with problems, anyway feel feee to report them if you faced