Skip to main content
Version: 3.5.0

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 casing of specific attribute values that are case-insensitive (for example form[method], img[crossorigin], script[type], link[sizes], img[fetchpriority]).
  • Trim surrounding whitespace for normalized values.
  • 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 button[type] is normalized, but input[type] is not changed).

Example

Source:

<form method="GET"></form>
<img loading="">
<button type="EXAMPLE"></button>

Minified:

<form method="get"></form>
<img loading="eager">
<button type="submit"></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

  • Attributes are removed when the value is empty or whitespace-only.
  • Event handler attributes (for example onclick, onfocus) are always removed when empty.
  • Some attributes are only removed on specific tags, such as cols on <textarea> or minlength/maxlength on <input> and <textarea>.
  • Attributes like alt are intentionally not removed, even when empty.

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></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 whitespace and trimming ends (class, rel, ping, sandbox, headers, dropzone, sizes on <link>).
  • Single-value attributes are trimmed (for example href, style, src, width, height) when they are on the correct elements.
  • Event handler attributes (like onclick) are trimmed only at the ends; inner whitespace is preserved.
  • srcset (on <img> and <source>) and imagesrcset (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. Values that can't be parsed as a srcset are left untouched.

sizes on <img> is not modified.

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>
  • language="javascript" and redundant JS type values from <script>
  • charset from inline <script> (only when src is 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 case-insensitively with surrounding whitespace ignored. 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, and link[rel] is treated as a space-separated token list when checking for rel="stylesheet".

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; xmlns on <svg>, <math>, or any other value/element is left untouched.
  • xml:lang when an identical (case-insensitive) lang attribute is present on the same element. If lang is absent or differs, xml:lang is left alone, since it may be the only language signal for XML tooling.

Attribute names and values are matched case-insensitively.

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 example checked="false" still becomes checked).
  • Collapses attributes whose value is an empty string to the minimized form (for example href="" becomes href).
  • Collapses missing value default attributes when they match the default, currently audio[preload=auto] and video[preload=auto].
  • Collapses crossorigin="anonymous" (case-insensitive) to crossorigin.
  • Collapses popover="auto" (case-insensitive) to popover, since the empty string maps to the same state. popover="manual" and popover="hint" are left untouched.
  • Collapses the declarative shadow DOM template booleans shadowrootclonable, shadowrootdelegatesfocus, and shadowrootserializable (but not the enumerated shadowrootmode).
  • Leaves hidden="until-found" untouched, since it is a distinct state from the collapsed hidden.
  • Leaves visible untouched on A-Frame elements (<a-*>) to avoid breaking visible="false".

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.

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, ping, sandbox, dropzone, sizes on link, headers). For case-insensitive token lists (rel, sandbox, dropzone, sizes), duplicates are removed regardless of casing (first occurrence kept). Whitespace is preserved where possible: repeated tokens are removed, but existing spacing between remaining tokens is kept.

Non-list-like attributes are not modified.

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 present, trimming whitespace, and dropping empty URLs. 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.0initial-scale=1). The author's separator characters (, or ;) are preserved.

Options

  • metaContent (boolean) - enable minification for meta[http-equiv="refresh"] and meta[name="viewport"] content.
  • redundantWhitespaces - remove redundant whitespace from attribute values.
    • safe - collapse whitespace in list-like attributes and trim single-value attributes (similar to collapseAttributeWhitespace).
    • aggressive - also trims other attribute values. (agressive is a deprecated alias kept for backwards compatibility.)
    • false - disable this behavior.

Example

Source:

<meta http-equiv="refresh" content="5; url=">
<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.
  • A default port matching the scheme is stripped (https://example.com:443/xhttps://example.com/x, http://example.com:80/xhttp://example.com/x). A mismatched port is kept (e.g. :443 is not removed for http, nor :8443 for https).
  • A trailing empty query (?) or empty fragment (#) is removed (foo? and foo#foo). A non-empty query (?x=1) or fragment (#top) is kept.
  • link[rel="canonical"] is never rewritten.
  • srcset and imagesrcset are processed when srcset is installed; invalid srcset strings are left unchanged.
  • javascript: URLs are minified with terser when available, preserving leading whitespace and normalizing the protocol 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 for alphabetical.
  • false: Disable the module.

Frequency sorting preserves the original attribute name casing (for example viewBox stays viewBox) and uses alphabetical ordering on lowercased attribute names to break ties. When attribute names collide by case, the first occurrence wins.

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 for alphabetical.
  • 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.

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>, and <template>.

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.
  • Template content is left untouched.
  • Elements carrying an inline white-space style that preserves whitespace (white-space: pre, pre-wrap, pre-line, or break-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 the style attribute value (no full CSS parsing), and pre-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 (&mdash;, &hellip;, &copy;©). Decimal (&#8212;) and hexadecimal (&#x2014;) numeric references are decoded as well.

Options

  • true — decode a conservative allowlist of common typographic/symbol named references plus safe numeric references (default in the safe preset).
  • { decodeAll: true } — additionally decode every named reference of the HTML standard, such as &hearts; or &Longleftrightarrow; (default in the max preset).

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.
  • &amp; becomes a bare & unless that would create an ambiguous ampersand, i.e. unless the following text would turn it back into a character reference. &amp;copy;, &amp;copy and &amp;#169; are therefore kept, while ?a=1&amp;b=2 becomes ?a=1&b=2. At the very end of a text node the following context is not known yet, so &amp; is kept there as well.
  • &lt; is never decoded in text nodes, since posthtml-render does not re-escape text and a literal < would open a tag. &gt; is decoded, and both are decoded in attribute values — values containing < or > are always rendered quoted.
  • &quot; and &#34; are kept in attribute values (posthtml-render would re-encode or break on a literal "), but decoded in text nodes. &apos;/&#39;/&#x27; are decoded in attribute values, except in the ones posthtml-render renders single-quoted (JSON-ish values, or every value when the quoteStyle render option asks for single quotes).
  • Content of <script>, <style>, and <textarea> is left untouched, since browsers do not entity-decode raw-text element content.
  • Invalid, unknown, or non-terminated references (for example &fake; or &mdash without a semicolon) are left untouched, and double-encoded input like &amp;mdash; never collapses into &mdash;.

Example

Source:

<p title="a &mdash; b">Copyright &copy; 2024 &#8212; the end&hellip;</p>
<a href="/search?q=1&amp;page=2">R &amp; 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) (default)
  • all — removes all HTML comments, including conditional/noindex/sse/excerpt comments
  • A RegExp — removes HTML comments that match the regexp (non-matching comments are kept)
  • A string — treated as a regexp pattern. Supports /pattern/flags or a plain pattern string (useful in JSON config files)
  • A Function that returns boolean — removes HTML comments for which the callback returns a truthy value

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, style and aria-hidden. Used by the max preset.
  • { 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 only whitespace/comments as content. 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, slot — they are painted or filled by something other than their own markup: scripts, a nested document, the resource of their src, or the light DOM projected into them.
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.

Notes

  • Empty conditional comments and standalone <!--<![endif]--> markers are left untouched.
  • 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:

<!--[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.

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 the max preset 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 is meta, link, script, style, or template.
  • colgroup — Can be omitted when the first child element is col, and the element is not immediately preceded by another colgroup whose end tag was omitted.
  • tbody — Can be omitted when the first child element is tr, and the element is not immediately preceded by a tbody, thead, or tfoot whose 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 another li, or last in its list.
  • dt — when immediately followed by a dt or dd.
  • dd — when immediately followed by a dd or dt, or last in its list.
  • p — when immediately followed by one of address, article, aside, blockquote, details, div, dl, fieldset, figcaption, figure, footer, form, h1h6, 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 an rt or rp, or last in the ruby.
  • optgroup — when immediately followed by another optgroup, or last in the select.
  • option — when immediately followed by an option or optgroup, or last in its parent.
  • thead — when immediately followed by a tbody or tfoot.
  • tbody — when immediately followed by a tbody or tfoot, or last in the table.
  • tfoot — when last in the table.
  • tr — when immediately followed by another tr, or last in its table section.
  • td, th — when immediately followed by a td or th, or last in the tr.

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 (li in a ul, td in a tr, …). In <span><p>x</p></span> the parser does not close the p on </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 open p.
  • End tags are never omitted inside <svg> and <math>, where the parser requires every element to be closed explicitly.

Example

Source:

<html><head><title>Title</title></head><body><ul><li>One</li><li>Two</li></ul></body></html>

Minified:

<title>Title</title><ul><li>One<li>Two</ul>

normalizeDoctype

Normalize a legacy doctype to the short HTML5 form <!doctype html>.

Legacy XHTML 1.0 / HTML 4.01 doctypes carry a PUBLIC/SYSTEM identifier and are typically 90–120 bytes; the short form is only 15 bytes.

This module is enabled only in the max preset (or by explicitly setting normalizeDoctype: true). It is not part of the safe preset.

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.
  • Quirks-mode caveat: rewriting an HTML 4.01 / XHTML 1.0 PUBLIC doctype to the short form can subtly change how a browser renders the page. Different legacy doctypes can trigger standards, almost-standards, or quirks mode, and the short form always selects full standards mode. This may alter, for example, inline image spacing inside table cells. Because that change is not guaranteed to be visually neutral, this module is max-only.
  • The XML declaration (<?xml ...?>) is never treated as a doctype.
  • An already-short doctype is normalized to lowercase (<!DOCTYPE html> becomes <!doctype html>); a document without a doctype is left unchanged.

Example

Source:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html></html>

Minified:

<!doctype html>
<html></html>

removeAttributeQuotes

Remove quotes around attributes when possible, see HTML Standard - 12.1.2.3 Attributes - Unquoted attribute value syntax.

Options

  • force — if true, forces quoteAllAttributes to false even when other PostHTML options or plugins set it to true.

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. removeAttributeQuotes sets this option to false only when it is not already defined, so other PostHTML plugins and configuration can override it.

For example:

posthtml([
htmlnano({
removeAttributeQuotes: true
})
]).process(html, {
quoteAllAttributes: true
})

removeAttributeQuotes will not work because PostHTML's quoteAllAttributes takes the 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:

  • Missing or empty type is treated as text/css (case-insensitive).
  • Missing or empty media is treated as all, and internal whitespace is collapsed.
  • Boolean attributes like amp-custom and disabled are treated as present when set.

Skipped styles:

  • <style scoped>...</style>
  • <style integrity>...</style>
  • AMP boilerplate styles (amp-boilerplate, amp4ads-boilerplate, amp4email-boilerplate)

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 other rel, such as preload, is not a stylesheet source and does not break a group);
  • a <style> with different group attributes (e.g. a different media or type), or a skipped scoped/integrity style.

Non-stylesheet content between styles (plain elements, text, comments, AMP boilerplate, 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 inline <script> tags 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/javascript and application/javascript (default is text/javascript). Other types (including type="module") are left untouched.
  • Scripts with src or integrity are never merged and they break a merge group, so code on each side stays separate.
  • Scripts inside <noscript> or <template> are left untouched: they don't run alongside the surrounding scripts (a <script> in <noscript> never executes, and one in <template> only executes once the template is cloned), so they neither merge nor break a group.
  • Boolean attributes (async, defer, nomodule) are normalized and treated as present, so defer and defer="defer" match.
  • Scripts are separated by nonce value, by nomodule, and by async/defer differences.
  • A missing trailing semicolon is added when concatenating. If a script ends with a line comment, the merger inserts \n; before the next script to avoid comment swallowing.

Side effects

Merging changes where the code physically lives in the document. This can break code when different attribute sets must remain isolated (for example async or nomodule), or when external tools rely on script boundaries.

Example

Source:

<script>const foo = 'A:1';</script>
<script class="test">foo = 'B:1';</script>
<script type="text/javascript">foo = 'A:2';</script>
<script defer>foo = 'C:1';</script>
<script>foo = 'A:3';</script>
<script defer="defer">foo = 'C:2';</script>
<script class="test" type="text/javascript">foo = 'B:2';</script>

Minified:

<script>const foo = 'A:1';foo = 'A:2';foo = 'A:3';</script>
<script defer="defer">foo = 'C:1';foo = 'C:2';</script>
<script class="test" type="text/javascript">foo = 'B:1';foo = 'B:2';</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 integrity attribute (covers both <style> tags and style attributes).
  • AMP boilerplate styles (amp-boilerplate, amp4ads-boilerplate, amp4email-boilerplate).

Notes:

  • style attributes are wrapped in a temporary selector (a{...}) before minification so cssnano can parse them, then the wrapper is removed.
  • For style attributes htmlnano disables the optimizations that a single declaration list can't give enough context for: mergeRules, minifySelectors, minifyParams, normalizeCharset, uniqueSelectors, normalizeUnicode, and (for the advanced preset) reduceIdents and zindex. This happens for every preset, including custom preset factories.
  • If you explicitly configure any of those plugins in preset: [..., { ... }], or pass a custom cssnano plugins list, 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:

  • discardUnused drops @font-face, @keyframes and @counter-style rules 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.
  • reduceIdents and mergeIdents rename those identifiers, which breaks the same cross-stylesheet and JavaScript references (element.style.animationName, grid-template-areas, …).
  • zindex rebases z-index values, which is only safe for a self-contained stylesheet.
  • autoprefixer (in add: false mode) 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 },
},
});

The module treats script types with parameters (for example text/javascript; charset=utf-8) as JavaScript. For type="module" scripts, it enables Terser's module option unless you explicitly set module yourself.

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 legacy text/ecmascript. Other types (for example application/json) are left untouched.
  • Any <script> with an integrity attribute is skipped to preserve SRI safety. Generate SRI after minification if you rely on it.
  • Inline event handler attributes (like onclick or onClick) are minified as JavaScript.
  • CDATA wrappers inside <script> in SVG are preserved; the inner JS is minified and re-wrapped.
  • 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 a type attribute are left untouched.
  • MIME parameters are allowed (application/json; charset=utf-8) and casing is ignored.
  • Tags with integrity are skipped to preserve SRI safety.
  • Invalid JSON is left unchanged.
  • JSON-like suffixes such as application/jsonp are 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 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 integrity attribute is skipped, and <script src="..."> is not processed.
  • 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 enables SVGO with its default configuration. htmlnano enables SVGO multipass by default; set multipass: false to disable it.

Notes

  • SVGs are rendered with quoted attributes before SVGO runs to keep the output stable.
  • 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: true to 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.

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:

  • content
  • css
  • extractors

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 the htmlroot option) 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 in form-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) and uuid (missing buffer bounds check).
  • postcss 7 (high: XSS via an unescaped </style> in the stringifier, and arbitrary .map file read through an attacker-controlled sourceMappingURL).

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:

  • stylesheets
  • ignoreSheets
  • raw

Notes

  • Only <style> tags with a CSS type (text/css or no type attribute) are processed. Other style types are left untouched.
  • CDATA wrappers in <style> tags are preserved after removing unused rules.
  • Empty styles are removed entirely when all rules are stripped.
  • AMP boilerplate styles (amp-boilerplate, amp4ads-boilerplate, amp4email-boilerplate) are not touched.
  • PurgeCSS keeps tag selectors based on the HTML (for example section {}), and class/id extraction treats whitespace and newlines as separators.

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.