Modules
By default the modules should only perform safe transforms, see the module documentation below for details.
You can disable modules by passing false as option, and enable them by passing true.
The order in which the modules are documented is also the order in which they are applied.
Attributes
normalizeAttributeValues
- Normalize ASCII casing of specific attribute values that are case-insensitive (for example
form[method],img[crossorigin],script[type],link[sizes],img[fetchpriority]). - Enumerated values are matched after decoding character references, without trimming whitespace. Invalid padded keywords retain their invalid value default.
- Apply the invalid value default for specific attributes (for example
img[loading],img[decoding],img[fetchpriority],link[fetchpriority],script[fetchpriority],track[kind],button[type],textarea[wrap],crossorigin,referrerpolicy,hidden,autocapitalize,marquee[behavior],marquee[direction]).
Invalid value defaults are only applied to the specific elements listed above
(for example invalid button[type] values are normalized, but invalid input[type] values are not changed).
crossorigin and referrerpolicy are only normalized on elements that support them.
Surrounding ASCII whitespace is trimmed only for token lists, charset labels, and
nonempty script types. Whitespace-only script types remain inactive data blocks.
Outside the enumerated defaults, values containing character references and MIME
types containing parameters are preserved to avoid changing case-sensitive data.
Invalid button types become the empty Auto state, preserving command behavior.
Empty autocapitalize values are kept so they can inherit the form's setting.
Example
Source:
<form method="GET"></form>
<img loading="">
<button type="EXAMPLE"></button>
Minified:
<form method="get"></form>
<img loading="eager">
<button type=""></button>
removeEmptyAttributes
Removes empty attributes when it is safe, based on the tag and attribute name. The removal rules are a fixed allowlist in the module.
Notes
- Allowlisted attributes are removed when empty. Whitespace-only values are also removed for token lists, styles and numeric attributes, using HTML's ASCII whitespace rules. Meaningful whitespace in text, identifiers and URLs is preserved.
- Known event handler attributes (for example
onclick,onfocus) are removed when empty or whitespace-only. Unknownon*attributes are preserved, and window handlers such asonmessageare only removed on<body>and<frameset>. - Some attributes are only removed on specific tags, such as
colson<textarea>orminlength/maxlengthon<input>and<textarea>. - Empty attributes with distinct browser behavior are preserved, including
alt,title,lang,form,formaction,headers,pattern, checkbox/radio/submit/reset inputvalue, resourcesrc/href, and<meta charset>. - Empty
diris preserved on<bdi>, where removing it enables automatic direction detection. - Whitespace-only script/style
typevalues are preserved because removing them can enable execution or styling. Empty scripttypeis also preserved when a legacylanguageattribute is present. Resource MIME types on<link>,<source>,<embed>and<object>are preserved.
Side effects
This module could break your styles or JS if you use selectors with attributes:
img[style=""] {
margin: 10px;
}
Example
Source:
<div id="" class="" title=""></div>
<button onclick="" onfocus=" "></button>
<textarea cols=""></textarea>
<img src="foo.jpg" alt="" style="">
Minified:
<div title=""></div>
<button></button>
<textarea></textarea>
<img src="foo.jpg" alt="">
collapseAttributeWhitespace
Collapse redundant whitespace in attribute values where it is safe:
- List-like attributes are normalized by collapsing internal ASCII whitespace and trimming ends:
classglobally,relon<a>,<area>,<form>and<link>,pingon<a>and<area>,sandboxon<iframe>,headerson<td>and<th>, andsizeson<link>. Legacydropzonevalues are normalized except on custom elements. - Single-value attributes have leading and trailing ASCII whitespace removed (for example
href,style,src,width,height) when they are on the correct elements. Nonbreaking spaces and other significant characters are preserved. - Known event handler attributes (like
onclick) are trimmed only at the ends; inner whitespace is preserved. Unknownon*attributes are kept as data, and window event handlers such asonmessageare only trimmed on<body>and<frameset>. srcset(on<img>and<source>) andimagesrcset(on<link>) are re-serialized with a single whitespace between an URL and its descriptors, and without any whitespace after the commas separating the image candidates. A whitespace is kept after the comma when the preceding candidate has no descriptor, since the parser would otherwise read the comma and the next URL as a part of the URL. Malformed descriptors are retained so rejected candidates stay rejected. Values containing encoded character references, or with no candidates to tokenize, are left untouched.
sizes on <img> is not modified.
usemap is preserved because image-map references require an exact match. Whitespace-only
single-value attributes and padded step="any" keywords are preserved to avoid changing
browser behavior. For style values containing quotes or backslashes (including encoded
ones), only leading ASCII whitespace is removed; trailing whitespace might belong to
a CSS string or escape.
Example
Source:
<a class=" content page " style=" display: block; " href=" https://example.com"></a>
<img srcset="image.png 480w , image2.png 2x">
Minified:
<a class="content page" style="display: block;" href="https://example.com"></a>
<img srcset="image.png 480w,image2.png 2x">
removeRedundantAttributes
Removes redundant attributes from tags when they match HTML defaults:
method="get"from<form>type="text"from<input>type="submit"from<button>withoutcommandorcommandfor, unless it is directly inside<select>language="javascript"and redundant JStypevalues from<script>charsetfrom inline<script>(only whensrcis not present)media="all"from<style>and<link>type="text/css"from<style>type="text/css"from<link rel="stylesheet">loading="eager"from<img>and<iframe>decoding="auto"from<img>fetchpriority="auto"from<img>,<link>and<script>kind="subtitles"from<track>wrap="soft"from<textarea>shape="rect"from<area>dir="ltr"from<html>
Attribute values are matched using ASCII case-insensitive comparisons. Surrounding ASCII whitespace is ignored for script MIME types and media values; meaningful whitespace in other values is preserved.
dir="ltr" is only removed from <html>: an element without a dir attribute inherits the direction of its parent, and the direction of an element without a parent is ltr — so the attribute is redundant on the root element, but not anywhere else.
Script type="module" is preserved. A JavaScript type is also preserved when removing it would let a legacy language value disable the script. Inert script and style types are preserved.
link[rel] is treated as an ASCII whitespace-separated token list. CSS type is removed only when the relationships are stylesheet and optionally alternate, since other relationships can use type to control resource fetching.
Options
This module is disabled by default, change option to true to enable this module.
Side effects
This module could break your styles or JS if you use selectors with attributes:
form[method="get"] {
color: red;
}
Example
Source:
<form method="get">
<input type="text">
</form>
<script type="module"></script>
<script type="text/javascript" charset="utf-8"></script>
<script src="app.js" charset="utf-8"></script>
Minified:
<form>
<input>
</form>
<script type="module"></script>
<script></script>
<script src="app.js" charset="utf-8"></script>
removeXmlLeftovers
Removes XHTML-era leftovers that are meaningless in HTML documents (commonly found in CMS output):
xmlns="http://www.w3.org/1999/xhtml"on<html>. The HTML parser ignores it —<html>is always in the HTML namespace regardless. Only the exact XHTML namespace value is removed, and only on the<html>tag;xmlnson<svg>,<math>, or any other value/element is left untouched.xml:langwhen an identical (ASCII case-insensitive)langattribute is present on the same element. Iflangis absent or differs,xml:langis left alone, since it may be the only language signal for XML tooling.
Attribute names are matched case-insensitively. Values are compared after decoding character references, without trimming whitespace. Namespace values are case-sensitive; language values use ASCII case-insensitive comparison. Case variants of duplicate attributes are preserved to avoid exposing a different value after removal. The module leaves documents processed with PostHTML's xmlMode: true untouched.
Options
This module is only enabled in the max preset (disabled by default). Set the option to true to enable it explicitly.
Side effects
This changes documents that are re-served as XHTML/XML: the xmlns declaration and xml:lang are significant in true XML processing. Only enable this if your document is served and consumed as HTML.
Example
Source:
<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en"></html>
Minified:
<html lang="en"></html>
collapseBooleanAttributes
- Collapses HTML boolean attributes (like
disabled,checked,readonly) to the minimized form, regardless of their string value (for examplechecked="false"still becomeschecked). - Collapses attributes whose value is an empty string to the minimized form (for example
href=""becomeshref). - Collapses missing value default attributes when they match the default, currently
audio[preload=auto]andvideo[preload=auto]. - Collapses
crossorigin="anonymous"(case-insensitive) tocrossorigin. - Collapses
popover="auto"(case-insensitive) topopover, since the empty string maps to the same state.popover="manual"andpopover="hint"are left untouched. - Collapses the declarative shadow DOM template booleans
shadowrootclonable,shadowrootdelegatesfocus, andshadowrootserializable(but not the enumeratedshadowrootmode). - Leaves
hidden="until-found"untouched (including case variants and character references), since it is a distinct state from the collapsedhidden. Surrounding whitespace makes this keyword invalid, selecting the ordinary hidden state. - Preserves attribute values on custom elements, including A-Frame's
visible="false"and attributes such asenabledordisabled. Global HTML states (autofocus,hidden,inert,itemscope, andpopover) still collapse, as do AMP flags whenamphtmlis enabled.
Options
If your document uses AMP, set the amphtml flag
to collapse additional, AMP-specific boolean attributes:
"collapseBooleanAttributes": {
"amphtml": true
}
When amphtml is enabled, AMP boolean attributes are collapsed when their
value is empty, true, or matches the attribute name.
Native input[placeholder] and textarea[placeholder] text and nonempty AMP
lightbox group names are preserved. Consent-policy values such as
data-block-on-consent="_auto_reject" are also preserved.
Side effects
This module could break your styles or JS if you use selectors with attributes:
button[disabled="disabled"] {
color: red;
}
Example
Source:
<button disabled="disabled">click</button>
<script defer=""></script>
<a href=""></a>
<script src="example-framework.js" crossorigin="anonymous"></script>
<video preload="auto"></video>
<a-entity visible="false"></a-entity>
Minified:
<button disabled>click</button>
<script defer></script>
<a href></a>
<script src="example-framework.js" crossorigin></script>
<video preload></video>
<a-entity visible="false"></a-entity>
deduplicateAttributeValues
Remove duplicate values from list-like attributes (class, rel, sandbox, dropzone, sizes on link, headers).
For case-insensitive token lists (rel, sandbox, dropzone, sizes), duplicates are removed using ASCII case-insensitive comparison (first occurrence kept).
Character references are decoded for comparison, while the first occurrence's source spelling is preserved.
Whitespace is preserved where possible: repeated tokens are removed, but existing spacing between remaining tokens is kept.
Non-list-like attributes are not modified.
Repeated ping URLs are preserved because each occurrence triggers a separate request.
Example
Source:
<link rel="nofollow NoFoLlOw noopener">
<a class="foo foo bar">click</a>
Minified:
<link rel="nofollow noopener">
<a class="foo bar">click</a>
minifyAttributes
Minify specific attribute values. This module targets
meta[http-equiv="refresh"] by removing the url= prefix when safe and
trimming ASCII whitespace. Explicit empty URLs and prefixes needed to preserve
the destination are retained. Refresh content containing character references
is left unchanged. It also minifies
meta[name="viewport"] content by normalizing whitespace around separators
(commas/semicolons) and =, and by dropping redundant trailing zeros in
numeric values (e.g. initial-scale=1.0 → initial-scale=1). The author's
separator characters (, or ;) are preserved.
Viewport whitespace normalization follows its ASCII grammar and leaves content
containing character references unchanged. If a meta element also declares a
refresh, only the refresh rewrite is applied to preserve its destination.
Options
metaContent(boolean) - enable minification formeta[http-equiv="refresh"]andmeta[name="viewport"]content.redundantWhitespaces- remove redundant whitespace from attribute values.safe- collapse whitespace in list-like attributes and trim single-value attributes (similar tocollapseAttributeWhitespace).aggressive- also trims other attribute values. (agressiveis a deprecated alias kept for backwards compatibility.)false- disable this behavior.
Example
Source:
<meta http-equiv="refresh" content=" 5; ">
<meta http-equiv="refresh" content="5; url=http://example.com/">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
Minified:
<meta http-equiv="refresh" content="5">
<meta http-equiv="refresh" content="5; http://example.com/">
<meta name="viewport" content="width=device-width,initial-scale=1">
minifyUrls
Convert absolute URL to relative URL using relateurl.
You have to install relateurl, terser and srcset in order to use this feature:
npm install --save-dev relateurl terser srcset
# if you prefer yarn
# yarn add --dev relateurl terser srcset
# if you prefer pnpm
# pnpm install --save-dev relateurl terser srcset
Options
The base URL to resolve against. Support String & URL.
htmlnano.process(html, {
minifyUrls: 'https://example.com' // Valid configuration
});
htmlnano.process(html, {
minifyUrls: new URL('https://example.com') // Valid configuration
});
htmlnano.process(html, {
minifyUrls: false // The module will be disabled
});
htmlnano.process(html, {
minifyUrls: true // Invalid configuration, the module will be disabled
});
Notes
- Only
http(s)and relative URLs are related. Non-HTTP schemes (e.g.mailto:,tel:,data:) are left untouched. - Hash-only (
#...) and query-only (?...) URLs are left untouched. Identifier attributes such asusemapandscript[for]are never treated as URLs. - A default port matching the scheme is stripped (
https://example.com:443/x→https://example.com/x,http://example.com:80/x→http://example.com/x). A mismatched port is kept (e.g.:443is not removed forhttp, nor:8443forhttps). - URL destinations are preserved, including explicit
index.htmlresources and query/fragment delimiters. Rewrites that change the browser-resolved URL are skipped. - Documents containing
<base href>are left unchanged because their base can override the configured URL. - Canonical links are never rewritten;
canonicalis recognized as a case-insensitive token inrel. srcset(including on<source>) andimagesrcsetare processed whensrcsetis installed; invalid strings or inputs the dependency cannot tokenize faithfully are left unchanged. Resource URLs are never shortened to empty strings.javascript:URLs are minified withterserwhen available, preserving global scope and completion values, decoding HTML/URL escapes, and protecting the output from further decoding. Invalid or ambiguous inputs are left unchanged; leading ASCII whitespace is preserved and the protocol is normalized to lowercase.- This module is not enabled by the default presets; you must pass a base URL to turn it on.
Example
Basic Usage
Configuration:
htmlnano.process(html, {
minifyUrls: 'https://example.com'
});
Source:
<a href="https://example.com/foo/bar/baz">bar</a>
Minified:
<a href="foo/bar/baz">bar</a>
With sub-directory
Configuration:
htmlnano.process(html, {
minifyUrls: 'https://example.com/foo/baz/'
});
Source:
<a href="https://example.com/foo/bar">bar</a>
Minified:
<a href="../bar">bar</a>
srcset
Configuration:
htmlnano.process(html, {
minifyUrls: 'https://example.com/foo/baz/'
});
Source:
<img srcset="https://example.com/foo/bar/image.png 1x, https://example.com/foo/bar/image2.png.png 2x">
Minified:
<img srcset="../bar/image.png 1x, ../bar/image2.png.png 2x">
sortAttributes
Sort attributes inside elements.
The module won't impact the plain-text size of the output. However it will improve the compression ratio of gzip/brotli used in HTTP compression.
Options
alphabetical: Default option. Sort attributes in alphabetical order.frequency: Sort attributes by frequency across the whole document.true: Alias foralphabetical.false: Disable the module.
Both sorting modes preserve attribute names and values, and keep case variants in their original relative order. This preserves HTML's first-duplicate behavior and retains distinct attributes in XML mode. Alphabetical sorting compares lowercased attribute names. Frequency sorting counts lowercased attribute names across the document and uses alphabetical ordering to break ties.
Example
alphabetical
Source:
<input type="text" class="form-control" name="testInput" autofocus="" autocomplete="off" id="testId">
Processed:
<input autocomplete="off" autofocus="" class="form-control" id="testId" name="testInput" type="text">
frequency
Source:
<input type="text" class="form-control" name="testInput" id="testId">
<a id="testId" href="#" class="testClass"></a>
<img width="20" src="../images/image.png" height="40" alt="image" class="cls" id="id2">
Processed:
<input class="form-control" id="testId" type="text" name="testInput">
<a class="testClass" id="testId" href="#"></a>
<img class="cls" id="id2" alt="image" height="40" src="../images/image.png" width="20">
sortAttributesWithLists
Sort values in list-like attributes (class, rel, ping, sandbox, dropzone, headers, and sizes on <link>). sizes on <img> is not modified.
The module won't impact the plain-text size of the output. However it will improve the compression ratio of gzip/brotli used in HTTP compression.
Options
alphabetical: Default option. Sort attribute values in alphabetical order.frequency: Sort attribute values by frequency across the document.true: Alias foralphabetical.false: Disable the module.
Frequency sorting preserves duplicates, ignores redundant whitespace in the attribute value, and uses alphabetical ordering to break ties.
Attribute name casing is preserved.
Values with conflicting dropzone operations (copy, move, link) are left unchanged because the first operation takes precedence.
Example
alphabetical
Source:
<div class="foo baz bar">click</div>
Processed:
<div class="bar baz foo">click</div>
frequency
Source:
<div class="foo baz bar"></div><div class="bar foo"></div>
Processed:
<div class="foo bar baz"></div><div class="foo bar"></div>
HTML Content
collapseWhitespace
Collapses redundant white spaces (including new lines).
It doesn’t affect white spaces in the elements <style>, <textarea>, <script>, <pre>, <template>, or the legacy preformatted elements <listing>, <plaintext>, and <xmp>.
Options
conservative— collapses all redundant whitespace to 1 space (default). Whitespace around inline elements (like<a>,<span>,<code>) is preserved when possible.aggressive— collapses redundant whitespace and trims around nodes when it is safe. This may remove indentation and drop whitespace-only text nodes between comments and non-inline elements.all— collapses all redundant whitespace and trims every text node, except where the whitespace is rendered: between two pieces of inline content exactly one space is kept, so the text still reads the same. This is the most aggressive behavior.
Notes
- Comments are preserved, but whitespace around them can be collapsed depending on the option.
- Non-rendering elements such as
<script>,<style>, and<template>do not interrupt word separators between surrounding text or inline elements. - Custom elements with hyphenated names are treated as inline elements when deciding whether to keep word separators.
- Template content is left untouched.
- Element and attribute names are matched case-insensitively, so uppercase or mixed-case markup receives the same whitespace protection.
- Character references in inline styles are decoded for the whitespace-preservation check. Whitespace inside a protected element does not replace a separator outside it.
- Elements carrying an inline
white-spacestyle that preserves whitespace (white-space: pre,pre-wrap,pre-line, orbreak-spaces) are treated like<pre>: their content and their whole subtree are left untouched, so layout is not broken. The check is a simple regexp on thestyleattribute value (no full CSS parsing), andpre-line(which technically collapses spaces but keeps newlines) is treated as fully protected as the conservative choice.
Notes on all
all trims every text node, but keeps exactly one space wherever the trimmed
whitespace is what separates two pieces of inline content — that is what makes it
lossless for text. Compared to aggressive it looks through those boundaries
instead of leaving whitespace alone as soon as an inline element is involved,
so it can trim inside inline elements too:
<!-- source -->
<p>Read the <a href="#"> docs </a> first</p>
<!-- all -->
<p>Read the <a href="#">docs</a> first</p>
<!-- aggressive -->
<p>Read the <a href="#">docs </a>first</p>
The whitespace of an inline element that another module removes afterwards
(removeEmptyElements) is not reconsidered, so such a removal can leave a space
behind where nothing needs one anymore.
Example
Source:
<div>
hello world!
<a href="#">answer</a>
<style>div { color: red; } </style>
<main></main>
</div>
Minified (with all):
<div>hello world! <a href="#">answer</a><style>div { color: red; } </style><main></main></div>
Minified (with aggressive):
<div>hello world! <a href="#">answer</a><style>div { color: red; } </style><main></main></div>
Minified (with conservative):
<div> hello world! <a href="#">answer</a> <style>div { color: red; } </style> <main></main> </div>
minifyCharacterReferences
Decodes HTML character references (entities) into shorter literal UTF-8
characters in text nodes and attribute values. Most named references are
5–8 bytes, while the equivalent character is only 2–3 bytes as UTF-8
(— → —, … → …, © → ©). Decimal (—)
and hexadecimal (—) numeric references are decoded as well.
Options
true— decode a conservative allowlist of common typographic/symbol named references plus safe numeric references (default in thesafepreset).{ decodeAll: true }— additionally decode every named reference of the HTML standard, such as♥or⟺(default in themaxpreset).
Notes
- The decoding is lossless: a reference is only decoded when the literal character is parsed back into exactly the same character in that context.
- Carriage-return references (
and
) are kept because literal carriage returns are normalized to line feeds by the HTML parser. Decoding also preserves nearby incomplete references and existing line breaks. &becomes a bare&unless that would create an ambiguous ampersand, i.e. unless the following text would turn it back into a character reference.&copy;,&copyand&#169;are therefore kept, while?a=1&b=2becomes?a=1&b=2. At the very end of a text node the following context is not known yet, so&is kept there as well. An encoded ampersand before#is also kept, including&#topic, to avoid introducing invalid numeric-reference parse errors.<is never decoded in text nodes, since posthtml-render does not re-escape text and a literal<would open a tag.>is decoded, and both are decoded in attribute values — values containing<or>are always rendered quoted."and"are kept in attribute values (posthtml-render would re-encode or break on a literal"), but decoded in text nodes.'/'/'are decoded in attribute values, except in the ones posthtml-render renders single-quoted (JSON-ish values, or every value when thequoteStylerender option asks for single quotes).- Raw-text content (
<script>,<style>,<xmp>,<iframe>,<noembed>, and<noframes>) is left untouched, including markup parsed by PostHTML as descendants. Tag names are matched regardless of case.<noscript>content is preserved for both scripting modes, and everything after<plaintext>is preserved through the end of the document.<textarea>content is also conservatively left untouched, although browsers do decode its character references. - Invalid, unknown, or non-terminated references (for example
&fake;or&mdashwithout a semicolon) are left untouched, and double-encoded input like&mdash;never collapses into—.
Example
Source:
<p title="a — b">Copyright © 2024 — the end…</p>
<a href="/search?q=1&page=2">R & D</a>
Minified:
<p title="a — b">Copyright © 2024 — the end…</p>
<a href="/search?q=1&page=2">R & D</a>
removeComments
Options
safe– removes HTML comments but keeps:- conditional comments (
<!--[if ...]>...<![endif]-->and downlevel-revealed forms) <!--noindex-->...<!--/noindex-->(case/spacing tolerant)<!--sse-->...<!--/sse-->Server-Side Excludes markers (case/spacing tolerant)- excerpt markers that start with
more(case/spacing tolerant), e.g.<!-- more -->,<!-- MORE -->,<!-- more Read more --> - license comments
<!--! ... -->(the HTML analogue of the/*! ... */comments kept by Terser/cssnano) - Astro hydration comments: all comments inside
<astro-island>elements and comment blocks immediately before them (allowing whitespace between comments and the element) (default)
- conditional comments (
all— removes all HTML comments, including conditional/noindex/sse/excerpt and Astro hydration comments- A
RegExp— removes HTML comments that match the regexp (non-matching comments are kept) - A string — treated as a regexp pattern. Supports
/pattern/flagsor a plain pattern string (useful in JSON config files) - A
Functionthat returns boolean — removes HTML comments for which the callback returns a truthy value
Regexp matching starts at the beginning of each comment, including with g or y
flags. The supplied regexp's lastIndex is left unchanged.
Only HTML comments are removed. Comment-looking text inside script, style,
textarea, title, xmp, iframe, noembed, noframes, and noscript, and
everything after plaintext, is kept. When removing a comment would join text
into HTML markup or change a character reference, the text boundary is escaped
to preserve its meaning.
Security warning: the removeComments option must come from a source you trust.
A string option is compiled into a RegExp and then tested against every comment, so a
pattern crafted to backtrack catastrophically turns comment-heavy input into a hang
(ReDoS). A function option is called for every comment, so it runs arbitrary code by
definition. Both are fine for options you write yourself — that is what the API is for —
but don't build the option out of user input, and keep in mind that a
config file discovered on the filesystem
counts as developer input too. The HTML being minified never influences which pattern is
compiled, only how often it is run.
Example
Source:
{
removeComments: 'all'
}
<div><!-- test --></div>
Minified:
<div></div>
Source:
{
removeComments: 'safe'
}
<!--noindex-->indexed?<!--/noindex-->
<!--[if IE 8]><link href="ie8only.css" rel="stylesheet"><![endif]-->
Lorem ipsum <!-- more --> dolor sit amet <!-- comment -->
Minified:
<!--noindex-->indexed?<!--/noindex-->
<!--[if IE 8]><link href="ie8only.css" rel="stylesheet"><![endif]-->
Lorem ipsum <!-- more --> dolor sit amet
Source:
{
removeComments: /<!--(\/)?noindex-->/
}
<div><!--noindex-->this text will not be indexed<!--/noindex-->Lorem ipsum dolor sit amet<!--more-->Lorem ipsum dolor sit amet</div>
Minified:
<div>this text will not be indexedLorem ipsum dolor sit amet<!--more-->Lorem ipsum dolor sit amet</div>
Source (JSON config):
{
"removeComments": "/<!--(\\/)?noindex-->/"
}
Source:
{
removeComments: (comment) => {
if (comment.includes('noindex')) return true;
return false;
}
}
<div><!--noindex-->this text will not be indexed<!--/noindex-->Lorem ipsum dolor sit amet<!--more-->Lorem ipsum dolor sit amet</div>
Minified:
<div>this text will not be indexedLorem ipsum dolor sit amet<!--more-->Lorem ipsum dolor sit amet</div>
removeEmptyElements
Removes elements that have no meaningful content.
Options
true— removes empty elements without attributes.{ removeWithAttributes: 'presentational' }— also removes empty elements whose attributes are all presentational:class,styleandaria-hidden. Used by themaxpreset.{ removeWithAttributes: ['data-decoration', 'class'] }— same as'presentational', but with your own list of attributes that don't prevent the removal.{ removeWithAttributes: true }— removes empty elements no matter what they carry.
Empty elements are defined as elements with no child elements and no text content;
ordinary comments do not count as content, but IE conditional comments do.
Whitespace is text content and is preserved because it can separate inline content or be significant in preformatted text.
Text joined across a removed element is escaped when necessary to avoid creating
new markup or changing a character reference.
Comment-looking content in text-only elements such as script, style, title
and textarea is literal text. Their contents are preserved, as are those of
noscript (which is text with scripting enabled), SVG and MathML subtrees, and
everything from a plaintext start tag to the end of the document.
Void elements (like <img> or <br>) are never removed, and neither are elements that
keep doing their job while empty, whatever removeWithAttributes says:
td,th,tr,caption,colgroup— they hold a position in the table grid. Dropping an empty cell shifts every following cell of the row into the wrong column.textarea,select,option— form controls that are submitted and scripted while empty; an empty<textarea>is simply one the user hasn't typed into yet.canvas,iframe,audio,video,object,slot— they are painted or filled by something other than their own markup: scripts, a nested document, the resource of theirsrc, or the light DOM projected into them.scriptwith asrcattribute — it executes an external resource even without inline content.
The 'presentational' mode
An element is removed only when every attribute it has is in the list, so anything
that gives the element a meaning beyond its looks keeps it: id, name, role,
aria-* (except aria-hidden), data-*, event handlers, href, src, title,
framework attributes like x-data or hx-get, and the geometry attributes of SVG
shapes (<path d="…"> is empty markup-wise, but it is the drawing).
aria-hidden is in the list because an element that is hidden from the accessibility
tree contributes nothing to it once it's empty.
On top of the elements that are never removed, this mode also keeps custom elements —
any tag with a dash in it, like <my-widget class="widget"></my-widget>, which builds
its own content once the element definition is upgraded — and the interactive elements
a, button, details, label and summary, whose icon is often drawn by CSS while
a script binds the behaviour through the very class that would allow the removal
(<button class="hamburger-menu"></button>). Note that removeWithAttributes: true
doesn't make either exception: it removes every empty element except the ones listed
above.
Side effects
This module removes elements that are used for styling or scripting, so it's disabled
in the safe preset.
removeWithAttributes: 'presentational' is lossy on purpose: it drops empty
elements that are only there to be seen — carousel dots, skeleton loaders, spinner
bars, gradient overlays, spacers. If your page relies on those, the rendering will
change. The removal is safe for the meaning and the accessibility of the page, but
class is a behaviour hook as much as a styling one: a script that looks an element
up by class (document.querySelector('.spinner')) will no longer find it. Interactive
elements are kept for exactly that reason, and you can use removeEmptyElements: true
(or a narrower removeWithAttributes list) if you want the rest of the decorations
back.
Example
Source:
<div>hello<span><b></b></span></div>
<div><span class="icon"></span>Download</div>
<div><span id="anchor"></span><my-widget class="widget"></my-widget>Widget</div>
Minified (removeEmptyElements: true):
<div>hello</div>
<div><span class="icon"></span>Download</div>
<div><span id="anchor"></span><my-widget class="widget"></my-widget>Widget</div>
Minified (removeEmptyElements: { removeWithAttributes: 'presentational' }):
<div>hello</div>
<div>Download</div>
<div><span id="anchor"></span><my-widget class="widget"></my-widget>Widget</div>
Minified (removeEmptyElements: { removeWithAttributes: true }):
<div>hello</div>
<div>Download</div>
<div>Widget</div>
minifyConditionalComments
Minifies HTML inside IE conditional comments (both downlevel-hidden and downlevel-revealed forms).
The conditional comment wrappers are preserved while the inner HTML is processed with the same htmlnano options,
so other modules (like collapseWhitespace, minifyCss, or minifyJs) can apply within the conditional block.
This module is disabled in the safe and ampSafe presets. On the evaluated
pages it changed very little output while adding another recursive htmlnano
pass, so it remains an opt-in there. The max preset enables it.
Notes
- Empty conditional comments and standalone
<!--<![endif]-->markers are left untouched. - Hidden blocks are left untouched if minification would introduce
-->or--!>inside them, preventing hidden markup from becoming active in other browsers. - Comment-like text inside raw-text and RCDATA elements (such as
script,style,textarea, andtitle), or afterplaintext, is not processed as conditional HTML. - Blocks containing nested conditional wrappers are preserved rather than reparsed,
including nested downlevel-revealed
<![if ...]>conditions. - If the comment content includes an opening
<html>without a closing tag, any auto-inserted</html>from the internal parse step is removed so it is not injected into the conditional block.
Example
Source:
<!--[if lte IE 7]>
<style type="text/css">
.title {
color: red;
}
</style>
<![endif]-->
Minified with minifyConditionalComments: true:
<!--[if lte IE 7]><style type="text/css">.title{color:red}</style><![endif]-->
removeOptionalTags
Remove certain tags that can be omitted, see HTML Standard - 13.1.2.4 Optional tags.
The safe preset enables this module as { removeStartTags: false }, and
ampSafe inherits that setting: both retain explicit start tags and remove only
eligible end tags. The max preset enables the module as true and removes
eligible start tags too.
Set the module to true (or { removeStartTags: true }) to remove eligible
start-and-end-tag pairs as well as eligible end tags. Use
{ removeStartTags: false } to retain every explicit start tag while still
removing end tags that can be omitted safely. Set it to false to disable all
optional-tag removal.
Notes
- Attributes only block the start tag of the element that carries them:
<li class="x">…</li>keeps its start tag but still loses</li>, and a<html class="no-js">still gets its</html>and the optional tags of everything inside it removed. An element whose start tag can be omitted must have no attributes at all. - htmlnano can omit an end tag on its own, and it can omit a start and an end tag together, but it can’t omit only a start tag — posthtml-render has no way to express that, see issue #99. When only the start tag is omissible, both tags are kept.
- Whitespace and comments between two elements count as content, so they block
the omissions that require one element to immediately follow another. Run
this module together with
collapseWhitespace: 'all'(as themaxpreset does) to get the most out of it. - The module runs after every other module, on the final tree.
- Nodes that another posthtml plugin left without a tag (posthtml-include builds those to splice a file in) render as their content only, and the elements inside them are minified as well.
Optional start tags
html— Can be omitted when the first child is not a comment.head— Can be omitted when the element is empty or the first child is an element.body— Can be omitted when the element is empty or the first child is not ASCII whitespace or a comment. Can’t be omitted if the first child element ismeta,link,script,style, ortemplate.colgroup— Can be omitted when the first child element iscol, and the element is not immediately preceded by anothercolgroupwhose end tag was omitted.tbody— Can be omitted when the first child element istr, and the element is not immediately preceded by atbody,thead, ortfootwhose end tag was omitted.
Optional end tags
html,body— when not immediately followed by a comment.head,caption,colgroup— when not immediately followed by ASCII whitespace or a comment.li— when immediately followed by anotherli, or last in its list.dt— when immediately followed by adtordd.dd— when immediately followed by addordt, or last in its list.p— when immediately followed by one ofaddress,article,aside,blockquote,details,div,dl,fieldset,figcaption,figure,footer,form,h1–h6,header,hgroup,hr,main,menu,nav,ol,p,pre,section,ul, or when it is the last child of a parent that closes it.rt,rp— when immediately followed by anrtorrp, or last in theruby.optgroup— when immediately followed by anotheroptgroup, or last in theselect.option— when immediately followed by anoptionoroptgroup, or last in its parent.thead— when immediately followed by atbodyortfoot.tbody— when immediately followed by atbodyortfoot, or last in thetable.tfoot— when last in thetable.tr— when immediately followed by anothertr, or last in its table section.td,th— when immediately followed by atdorth, or last in thetr.
Notes on correctness
htmlnano is stricter than the specification in a few places, because the specification’s wording assumes markup that already follows the content model:
- The “no more content in the parent element” rules are only applied when the
parent is one the element is actually allowed to live in (
liin aul,tdin atr, …). In<span><p>x</p></span>the parser does not close thepon</span>, so</p>is kept. </p>before a<table>is only omitted in a document with an<!doctype html>: in quirks mode a<table>start tag does not close an openp.- End tags are never omitted inside
<svg>and<math>, where the parser requires every element to be closed explicitly. </head>is retained before elements that the parser would otherwise consume inside the head, including<noscript>with either scripting setting.- Ruby annotation end tags are omitted only under an explicit
<ruby>, and</optgroup>only under an explicit<select>. Elsewhere, including fragments whose context is unknown, a following sibling can become nested instead.
Example
Source:
<html><head><title>Title</title></head><body><ul><li>One</li><li>Two</li></ul></body></html>
Minified by the default safe preset:
<html><head><title>Title</title><body><ul><li>One<li>Two</ul>
Minified by the max preset:
<title>Title</title><ul><li>One<li>Two</ul>
normalizeDoctype
Normalize doctypes to the short HTML5 form <!doctype html>.
The option has two enabled modes:
'html5'canonicalizes casing and redundant ASCII whitespace only when the complete input is already the short HTML5 doctype. This conservative mode is used by thesafepreset and inherited byampSafe.truerewrites complete top-level doctype strings beginning with<!doctypeand ASCII whitespace, including legacyPUBLIC/SYSTEMand malformed doctypes. This broad, backward-compatible mode is used by themaxpreset.
Notes
- Only the top-level doctype string node is inspected; posthtml-parser emits the doctype as a raw string, so no DOM node is created for it.
- The
'html5'mode cannot change standards/quirks mode because it leavesabout:legacy-compat, legacy HTML/XHTML, malformed, and quirks-triggering doctypes unchanged. - Quirks-mode caveat for
true: rewriting a legacy doctype can change a document from almost-standards or quirks mode to full standards mode. This can subtly alter rendering, such as inline image spacing inside table cells. - XML declarations and non-doctype strings are never treated as doctypes. A document without a doctype is left unchanged.
- Neither mode rewrites strings with content after the declaration, an absent
closing
>, or non-ASCII whitespace immediately after<!doctype.
Example
Source:
<!DOCTYPE HTML >
<html></html>
Minified with either 'html5' or true:
<!doctype html>
<html></html>
removeAttributeQuotes
Remove quotes around attributes when possible, see HTML Standard - 12.1.2.3 Attributes - Unquoted attribute value syntax.
This module is enabled by the safe preset, inherited by ampSafe, and remains
enabled in max. It only removes quotes when the PostHTML renderer considers
them optional and its configuration permits unquoted attributes.
In PostHTML's xmlMode, this module leaves quoting options unchanged, even
with force: true, because XML requires quotes around attribute values.
When quotes are required, literal quote delimiters are escaped as needed to
preserve the value and prevent it from introducing additional attributes.
Options
force— iftrue, forcesquoteAllAttributestofalseeven when other PostHTML options or plugins set it totrue.
Example
Source:
<div class="foo" title="hello world"></div>
Minified:
<div class=foo title="hello world"></div>
Notice
The feature is implemented by posthtml-render's quoteAllAttributes, which is a PostHTML option.
With removeAttributeQuotes: true (including the preset default), htmlnano sets
this renderer option to false only when it is not already defined. An explicit
PostHTML setting or an earlier plugin can therefore override the preset.
For example:
posthtml([
htmlnano({
removeAttributeQuotes: true
})
]).process(html, {
quoteAllAttributes: true
})
The quotes remain because PostHTML's quoteAllAttributes setting takes priority.
If you need to ensure quotes are removed even when quoteAllAttributes is already true, enable the force option:
posthtml([
htmlnano({
removeAttributeQuotes: { force: true }
})
]).process(html, {
quoteAllAttributes: true
})
<style>, <script> and <svg> Tags
mergeStyles
Merges multiple <style> with the same normalized media and type into one tag,
as long as all other attributes match (for example: nonce, title, data-*).
Attribute matching is strict: any differing attribute (including nonce) prevents merging.
Normalization details:
- Attribute names are compared without ASCII case, and character references in values are decoded for matching. The first duplicate attribute wins, as in HTML.
- Missing or empty
typeis treated astext/css(case-insensitive). Other types, including whitespace-padded values, are left separate. - Missing or empty
mediais treated asall. ASCII whitespace and case are normalized for simple queries; queries containing strings, escapes or custom identifiers are matched conservatively. - Boolean attributes like
amp-customanddisabledare treated as present when set.
Skipped styles:
<style scoped>...</style><style integrity>...</style>- AMP boilerplate styles (
amp-boilerplate,amp4ads-boilerplate,amp4email-boilerplate) - Styles containing
@importor@namespace, which depend on stylesheet boundaries - Styles with unfinished CSS constructs, escapes outside strings or legacy HTML comment wrappers. These are kept separate conservatively so one stylesheet cannot consume or reinterpret another's rules.
Source order is preserved
CSS rules of equal specificity resolve by their source order, so moving a
<style> past another stylesheet can silently change which rules win. To stay
safe, only styles that form a contiguous run in document order are merged:
the merge group is closed whenever a stylesheet source appears between two
otherwise-mergeable styles, namely:
- a
<link rel="stylesheet">(any otherrel, such aspreload, is not a stylesheet source and does not break a group); - a
<style>with different group attributes (e.g. a differentmediaortype), or a skipped style, including AMP boilerplate; - a CSP
<meta>,<script>or<base>element, to preserve authorization, script observations and relative URL resolution; <svg>or<math>content, whose styles are left untouched.
Ordinary content between styles (plain elements, text, comments, preload links) does not break a group. When a group is closed a new one starts, so a document can produce several independent merged groups.
<noscript> and <template> are separate scopes
Styles inside <noscript> only apply when scripting is disabled, and styles
inside <template> are inert until the template is cloned. Their styles are
merged among themselves, but never with the styles of the surrounding document
in either direction. A <noscript> also closes an open group, because with
scripting disabled its styles do apply at that exact position:
<style>h1 { color: red }</style>
<style>div { color: blue }</style>
<noscript>
<style>h1 { color: green }</style>
<style>div { color: black }</style>
</noscript>
becomes:
<style>h1 { color: red } div { color: blue }</style>
<noscript>
<style>h1 { color: green } div { color: black }</style>
</noscript>
Example
Source:
<style>h1 { color: red }</style>
<style>div { font-size: 20px }</style>
<style media="print">div { color: blue }</style>
<style type="text/css" media="print">a {}</style>
Minified:
<style>h1 { color: red } div { font-size: 20px }</style>
<style media="print">div { color: blue } a {}</style>
The following is not merged, because the external stylesheet sits between the two inline styles and could change the cascade:
<style>p { color: red }</style>
<link rel="stylesheet" href="theme.css">
<style>p { color: green }</style>
mergeScripts
Merge adjacent sibling inline <script> tags, separated only by whitespace, when they share the same normalized attributes (all except src, integrity, and type).
The merged content is appended into the last script in the group and the earlier scripts are removed.
Notes
- Only inline scripts with mergeable types are considered:
text/javascriptandapplication/javascript(default istext/javascript). Other types (includingtype="module") are left untouched. - Scripts with
srcorintegrity, scripts with different attributes or types, and intervening markup break a merge group, so code on each side stays separate. Groups never cross parent boundaries. - Scripts inside
<noscript>,<template>,<svg>, or<math>are left untouched. These elements also break the surrounding merge group. - Boolean attributes (
async,defer,nomodule) are normalized and treated as present, sodeferanddefer="defer"match. - Tag and attribute names are compared without ASCII case distinctions, and attribute character references are decoded for comparison. The first attribute wins when names are duplicated with different casing. Script type detection follows the HTML rules for
typeand legacylanguage. - Scripts are separated by
noncevalue, bynomodule, and byasync/deferdifferences. - Concatenated scripts are separated by
\n;\nto preserve line comments and prevent expressions from joining across script boundaries. - Scripts containing a quoted
use strict, a leading hashbang, or HTML tokenizer markers (<!--or<script/</script) are conservatively left untouched and break a merge group. This preserves strict mode and avoids changes to HTML script parsing without parsing JavaScript. - Scripts containing the keywords
let,const, orclassalso break a merge group, preserving lexical declaration boundaries and avoiding redeclaration errors in the merged source. This conservative check also matches keywords in comments and strings.
Side effects
Merging changes where the code physically lives in the document.
Code must tolerate sharing a script: merging can change declaration hoisting, lexical bindings, and error isolation. Code that inspects document.currentScript, uses document.write(), or depends on individual script elements can also behave differently. Generate CSP hashes after minification, since merging changes script text. This module is disabled in the safe preset.
Example
Source:
<script>window.foo = 1;</script>
<script type="text/javascript">window.bar = 2;</script>
<script defer>window.baz = 3;</script>
<script defer="defer">window.qux = 4;</script>
Minified:
<script type="text/javascript">window.foo = 1;
;
window.bar = 2;</script>
<script defer="defer">window.baz = 3;
;
window.qux = 4;</script>
minifyCss
Minifies CSS with cssnano inside <style> tags and style attributes.
Only <style> tags with a CSS type (text/css or no type attribute) are minified.
Other style types (for example text/less) are left untouched.
CDATA wrappers are preserved, even when surrounded by whitespace.
Skipped nodes:
- Any element with an
integrityattribute (covers both<style>tags andstyleattributes). - AMP boilerplate styles (
amp-boilerplate,amp4ads-boilerplate,amp4email-boilerplate).
Notes:
- Identical CSS is minified once per document. Processors are initialized only for CSS that needs minification.
- CSS that PostCSS cannot parse, or whose selectors cssnano cannot parse, is preserved in its original style node or attribute. Other CSS is still minified. Configuration and other plugin errors continue to throw.
- With the built-in
defaultpreset and no custom options, distinct inline declaration lists are processed together as independent PostCSS roots, sharing plugin setup without merging styles. Custom presets, plugin lists, and per-plugin options retain separate processing. styleattributes are wrapped in a temporary selector (a{...}) before minification so cssnano can parse them, then the wrapper is removed.- For
styleattributes htmlnano disables the optimizations that a single declaration list can't give enough context for:mergeRules,minifySelectors,minifyParams,normalizeCharset,uniqueSelectors,normalizeUnicode, and (for theadvancedpreset)reduceIdentsandzindex. This happens for every preset, including custom preset factories. - If you explicitly configure any of those plugins in
preset: [..., { ... }], or pass a custom cssnanopluginslist, htmlnano keeps your settings instead of overwriting them.
You have to install cssnano and postcss in order to use this feature:
npm install --save-dev cssnano postcss
# if you prefer yarn
# yarn add --dev cssnano postcss
# if you prefer pnpm
# pnpm install --save-dev cssnano postcss
Options
See the documentation of cssnano for all supported optimizations.
By default CSS is minified with preset default, which shouldn't have any side-effects.
To use another preset or disable some optimizations pass options to minifyCss module:
htmlnano.process(html, {
minifyCss: {
preset: ['default', {
discardComments: {
removeAll: true,
},
}]
}
});
The advanced preset
cssnano also ships an advanced preset.
htmlnano does not use it in any preset, including max, and does not depend on it — install
cssnano-preset-advanced yourself if you want it:
htmlnano.process(html, {
minifyCss: {
preset: 'advanced'
}
});
Be aware of what you are opting into. htmlnano minifies every <style> tag and every style
attribute on its own, and it never sees the page's external stylesheets or its scripts, so the
advanced plugins that reason about a whole document are unsound here:
discardUnuseddrops@font-face,@keyframesand@counter-stylerules that nothing in the same<style>tag references — including fonts and animations used by an external stylesheet or added at runtime. On a real page this silently changes the rendering.reduceIdentsandmergeIdentsrename those identifiers, which breaks the same cross-stylesheet and JavaScript references (element.style.animationName,grid-template-areas, …).zindexrebasesz-indexvalues, which is only safe for a self-contained stylesheet.autoprefixer(inadd: falsemode) removes vendor prefixes according to the Browserslist configuration and caniuse-lite version found on the build machine, so the output is no longer a function of the input alone.
htmlnano does disable reduceIdents and zindex for style attributes, where they are always
wrong (see the notes above), but it cannot make the <style>-tag transformations safe for you.
Example
Source:
<div>
<style>
h1 {
margin: 10px 10px 10px 10px;
color: #ff0000;
}
</style>
</div>
Minified:
<div>
<style>h1{margin:10px;color:red}</style>
</div>
minifyJs
Minifies JS using Terser inside <script> tags.
You have to install terser in order to use this feature:
npm install --save-dev terser
# if you prefer yarn
# yarn add --dev terser
# if you prefer pnpm
# pnpm install --save-dev terser
Options
See the documentation of Terser for all supported options.
Terser options can be passed directly to the minifyJs module:
htmlnano.process(html, {
minifyJs: {
output: { quote_style: 1 },
},
});
Script types follow HTML's script type detection: values with parameters (for example
text/javascript; charset=utf-8) and whitespace-only values are data blocks and are left untouched.
Tag and attribute names are matched case-insensitively, character references in type values are decoded,
and the legacy language attribute is considered when type is absent.
For type="module" scripts, it enables Terser's module option unless you explicitly set module yourself.
Legal comments (@license, @preserve, /*!)
By default Terser keeps "legal" comments — those matching @license, @preserve, @cc_on or starting with /*!
(this covers @licstart/@licend blocks as well). The safe and ampSafe presets keep that default,
so license notices stay in the output.
The max preset turns them off with Terser's own option:
minifyJs: {
format: { comments: false }
}
This is a legal decision, not a technical one. Many JS licenses (MIT, Apache-2.0, GPL, …) require the
copyright notice to be distributed with the code, and dropping the comment can put you in breach of them.
Only use it when you ship the notices elsewhere (a LICENSE/NOTICE file, a separate license page, a preserved
bundle header), or when the inlined scripts are your own.
To keep legal comments while still using the max preset, override the module:
htmlnano.process(html, { minifyJs: { format: { comments: 'some' } } }, htmlnano.presets.max);
Notes
- Only JavaScript script types are processed: the default type,
text/javascript,application/javascript, and legacytext/ecmascript. Other types (for exampleapplication/json) are left untouched. - Any
<script>with anintegrityattribute is skipped to preserve SRI safety. Generate SRI after minification if you rely on it. - Content of scripts with a
srcattribute is left untouched; their event handlers can still be minified. - Scripts with JavaScript parse errors are preserved while other scripts continue to be minified. Terser configuration and callback errors still propagate.
- If Terser output would introduce a closing script tag or a double-escaped script sequence, the original
content is preserved. This also applies to custom preambles and
inline_script: false. - Inline event handler attributes (like
onclickoronClick) are minified as JavaScript. - Handler bodies support
returnand trailing line comments, and character references are decoded before minification and escaped for HTML afterward. Invalid handlers are preserved. For handlers,module,toplevel(including nested compressor/mangler options), andencloseare disabled to preserve their implicit parameters and execution context; nested functions can still be compressed and mangled. - Bare SVG CDATA sections and CDATA wrappers in boundary comments are preserved; the inner JS is minified and re-wrapped. CDATA markers inside JavaScript strings and template literals are left intact.
- For AMP documents, inline handlers like
on="tap:..."are not modified when using the AMP-safe preset.
Example
Source:
<div>
<script>
/* comment */
const foo = function () {
};
</script>
</div>
Minified:
<div>
<script>const foo=function(){};</script>
</div>
minifyJson
Minifies JSON inside <script> tags whose type ends in /json or +json (case-insensitive), including when MIME parameters are present.
Notes
- Only
<script>tags with an explicit JSON MIME type are processed. Tags without atypeattribute are left untouched. - MIME parameters are allowed (
application/json; charset=utf-8) and casing is ignored. - Tags with
integrityare skipped to preserve SRI safety. - Invalid JSON is left unchanged.
- Number literals, duplicate keys and member order are preserved; whitespace outside strings and unnecessary string escapes are removed.
- Less-than signs in JSON strings are escaped as
\u003Cto prevent the payload from closing the script or changing HTML parsing. - JSON-like suffixes such as
application/jsonpare not considered JSON and are skipped.
Example
Source:
<script type="application/json">
{
"user": "me"
}
</script>
Minified:
<script type="application/json">{"user":"me"}</script>
Source:
<script type="application/ld+json; charset=UTF-8">
{
"id": 1
}
</script>
Minified:
<script type="application/ld+json; charset=UTF-8">{"id":1}</script>
minifyHtmlTemplate
Minifies HTML inside template containers (for example <script type="text/x-handlebars-template"> or <template>).
It runs htmlnano on the inner HTML using the current minification and PostHTML parsing/rendering options.
Options
minifyHtmlTemplate: true enables the default template rules.
You can pass an array of object rules:
htmlnano.process(html, {
minifyHtmlTemplate: [
{ tag: 'template', attrs: { id: 'my-template' } },
{ tag: 'script', attrs: { type: 'text/x-handlebars-template' } },
]
});
Passing any array replaces the built-in rules. To keep defaults and add more, spread them in:
import { modules } from 'htmlnano';
htmlnano.process(html, {
minifyHtmlTemplate: [
...modules.minifyHtmlTemplate.defaultRules,
{ tag: 'script', attrs: { id: 'my-template' } }
]
});
Notes
- The built-in rules include common script template MIME types and the
<template>tag. - Rules are objects:
{ tag, attrs? }. - Attribute name matching is case-insensitive. For
type, matching ignores MIME parameters and casing. - Any tag with an
integrityattribute is skipped, and<script src="...">is not processed. - Text-only template content is preserved if minification would introduce a closing container tag. Script templates also preserve content that would introduce a double-escaped script sequence.
- Apparent templates inside text-only elements (such as
<textarea>) or after<plaintext>are not processed. Nested templates are left to the inner run, whereminifyHtmlTemplateis disabled. - The actual changes depend on the enabled modules (for example
collapseWhitespace).
Example
Source:
<script type="text/x-handlebars-template">
<div class="entry">
<h1>{{title}}</h1>
</div>
</script>
Minified:
<script type="text/x-handlebars-template"><div class="entry"><h1>{{title}}</h1></div></script>
minifySvg
Minifies SVG inside <svg> tags using SVGO.
SVGO is an optional dependency. If it is not installed, SVG content is left untouched.
Options
See the documentation of SVGO for all supported options.
SVGO options can be passed directly to the minifySvg module:
htmlnano.process(html, {
minifySvg: {
plugins: [
{
name: 'preset-default',
params: {
overrides: {
builtinPluginName: {
optionName: 'optionValue'
},
},
},
}
]
}
});
minifySvg: true and minifySvg: {} enable SVGO with htmlnano's inline SVG defaults.
htmlnano enables SVGO multipass by default; set multipass: false to disable it.
Notes
- htmlnano disables
cleanupIds,removeUselessDefs, andremoveHiddenElemsin SVGO'spreset-default. Each SVG is optimized separately, but its IDs, symbols, gradients, and other definitions can be referenced elsewhere in the HTML document. These defaults preserve shared sprites and definitions and prevent ID renaming from creating duplicate IDs across SVGs. - SVGO's
inlineStylesusesremoveMatchedSelectors: falseso inlining local CSS rules does not remove IDs or classes used elsewhere in the page. - Explicit
preset-defaultoverrides take precedence over these defaults. For example,params: { overrides: { cleanupIds: {} } }re-enables ID cleanup. Plugins explicitly listed outside the preset, custom plugins, and an empty plugin list are passed through as configured. - SVGs are rendered with quoted attributes before SVGO runs to keep the output stable.
- SVGs containing
foreignObject, or HTML markup insidetitle/desc, are left to the other HTML minification modules. SVGO's XML parsing and serialization can change embedded HTML structure, raw-text content, and whitespace, and can turn escaped text into executable markup. - HTML tags that cause browsers to leave SVG content, such as
p, also cause the enclosing SVG to bypass SVGO, preserving content that PostHTML represents as SVG descendants. - SVGs containing numeric references with different HTML/XML meanings (such as
€), or carriage-return references ( ), also bypass SVGO to preserve their browser-parsed values. - Parser errors from SVGO leave the original
<svg>as-is. Other SVGO errors are logged and the module falls back to a no-plugin SVGO run; if that also fails, the original SVG is preserved. - Set
skipInternalWarnings: trueto suppress SVGO error logging.
Example
Source:
<svg version="1.1" baseProfile="full" width="300" height="200" xmlns="http://www.w3.org/2000/svg">
<rect width="100%" height="100%" fill="red" />
<circle cx="150" cy="100" r="80" fill="green" />
<text x="150" y="125" font-size="60" text-anchor="middle" fill="white">SVG</text>
</svg>
Minified:
<svg baseProfile="full" width="300" height="200" xmlns="http://www.w3.org/2000/svg"><rect width="100%" height="100%" fill="red"/><circle cx="150" cy="100" r="80" fill="green"/><text x="150" y="125" font-size="60" text-anchor="middle" fill="#fff">SVG</text></svg>
removeUnusedCss
Removes unused CSS inside <style> tags with either uncss or PurgeCSS.
PurgeCSS is the default and only extracts selectors from the HTML as strings.
Security warning: the tool: 'uncss' option renders the HTML you are minifying in a real DOM (jsdom) with script execution enabled, which means any <script> in that HTML runs inside your build process.
uncss is also abandoned, and installing it pulls a chain of known-vulnerable dependencies into your project.
Never use tool: 'uncss' on HTML you don't fully trust — see With uncss below.
With PurgeCSS (recommended)
Use PurgeCSS instead of uncss by adding tool: 'purgeCSS' to the options.
You have to install purgecss in order to use this feature:
npm install --save-dev purgecss
# if you prefer yarn
# yarn add --dev purgecss
# if you prefer pnpm
# pnpm install --save-dev purgecss
Options
See the documentation of PurgeCSS for all supported options.
PurgeCSS options can be passed directly to the removeUnusedCss module:
htmlnano.process(html, {
removeUnusedCss: {
tool: 'purgeCSS',
safelist: ['.do-not-remove']
}
});
The following PurgeCSS options are ignored if passed to the module:
contentcssextractors
With uncss
Security warning: uncss executes the scripts of the HTML you are minifying.
To find out which selectors are used, uncss loads the HTML into jsdom with runScripts: 'dangerously' and script fetching enabled.
uncss offers no option to turn that off, so with tool: 'uncss' the HTML passed to htmlnano is not just parsed, it is executed:
- Arbitrary code execution: every inline
<script>in the HTML runs in the build process, with the privileges of whoever runs the build. - Local file read:
<script src="/absolute/path">is read from your filesystem (relative to thehtmlrootoption) and executed, so the script can read local files and leak them. - Outbound network requests: other
<script src="...">URLs and other external resources are fetched over the network (SSRF).
Only use tool: 'uncss' on HTML that you fully control and trust, in an environment where running that HTML's scripts is acceptable.
For anything else — user-supplied HTML, templates rendering untrusted content, HTML from third-party packages — use tool: 'purgeCSS' (the default), which only extracts selectors from the HTML as strings and never builds a DOM or runs JavaScript.
htmlnano prints this warning once per process when tool: 'uncss' is used; set skipInternalWarnings: true to silence it.
Security warning: uncss is abandoned and depends on packages with known vulnerabilities.
The last uncss release, 0.17.3, is from February 2020, and the project isn't maintained anymore. Its dependencies are therefore pinned to versions with published advisories that will never be fixed upstream:
- jsdom 14 depends on
request, which is itself deprecated and unmaintained, and drags inform-data(critical: unsafe random function for the multipart boundary, CRLF injection via unescaped field names),tough-cookie< 4.1.3 (prototype pollution),qs(denial of service) anduuid(missing buffer bounds check). postcss7 (high: XSS via an unescaped</style>in the stringifier, and arbitrary.mapfile read through an attacker-controlledsourceMappingURL).
So installing uncss adds all of those to your dependency tree, and npm audit will report them.
tool: 'purgeCSS' (the default) depends on none of this; uncss is still supported only for backwards compatibility.
You have to install uncss in order to use this feature (please read the warnings above first):
npm install --save-dev uncss
# if you prefer yarn
# yarn add --dev uncss
# if you prefer pnpm
# pnpm install --save-dev uncss
Options
See the documentation of uncss for all supported options.
uncss options can be passed directly to the removeUnusedCss module:
htmlnano.process(html, {
removeUnusedCss: {
tool: 'uncss',
ignore: ['.do-not-remove']
}
});
The following uncss options are ignored if passed to the module:
stylesheetsignoreSheetsraw
Notes
- Only
<style>tags with a CSS type (text/cssor notypeattribute) are processed. Other style types are left untouched. - CDATA wrappers in
<style>tags are preserved after removing unused rules. - Stylesheets with CSS syntax errors or selectors that PurgeCSS cannot parse are preserved; unused rules in other stylesheets are still removed. Configuration and callback errors still propagate.
- Empty styles are removed entirely when all rules are stripped.
- AMP boilerplate styles (
amp-boilerplate,amp4ads-boilerplate,amp4email-boilerplate) are not touched. - PurgeCSS reads decoded class names and IDs from the HTML. Attribute selectors are retained whenever their attribute is present, regardless of its value, to preserve case-insensitive matches.
- PurgeCSS keeps tag selectors based on the HTML (for example
section {}), including omitted document elements. Classes are split on ASCII whitespace; IDs are kept as complete values. - PurgeCSS's optional
keyframes,fontFace, andvariablescleanup is disabled when there are multiple CSS style blocks or nonempty inline styles, because definitions can be used outside the block being processed. Unused selectors are still removed.
Example
Source:
<div class="b">
<style>
.a {
margin: 10px 10px 10px 10px;
}
.b {
color: #ff0000;
}
</style>
</div>
Optimized:
<div class="b">
<style>
.b {
color: #ff0000;
}
</style>
</div>
Miscellaneous
custom
It's also possible to pass custom modules in the minifier. As a function:
const options = {
custom: function (tree, options) {
// Some minification
return tree;
}
};
Or as a list of functions:
const options = {
custom: [
function (tree, options) {
// Some minification
return tree;
},
function (tree, options) {
// Some other minification
return tree;
}
]
};
htmlnano's options are passed to your custom plugin by the second parameter options.
Custom functions may be asynchronous. If a custom function returns a Promise, htmlnano awaits it before running the next custom function:
const options = {
custom: async function (tree, options) {
await someAsyncMinification(tree);
return tree;
}
};
Errors thrown (or rejected promises) from a custom function propagate to the caller of htmlnano.process() and are not swallowed.