feat(rendering): minify rendered HTML behind FEATURE_FLAG_MINIFY_HTML - #36852
feat(rendering): minify rendered HTML behind FEATURE_FLAG_MINIFY_HTML#36852fmontes wants to merge 1 commit into
Conversation
Adds an opt-in HTML minifier that strips insignificant whitespace, line breaks and indentation from rendered pages before they are written to the response. Wired at the two seams that together cover every render path: - VelocityLiveMode.writePage() for LIVE mode, before the page cache write so minification is paid once per cache fill rather than per request - VelocityModeHandler.eval() for preview/edit/admin modes and the REST and GraphQL getPageHtml callers The minifier is conservative by design: pre/textarea/script/style content is copied byte-for-byte, whitespace between inline elements is collapsed rather than removed so words are never joined, and any failure returns the original markup. Refs #36851 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Claude finished @fmontes's task in 1m 58s —— View job Code Review — native HTML minification
Nice work — the design (two seams, cache-fill-time minification, best-effort fallback, idempotency, preserved regions) is sound and well-documented. One real correctness bug stands out. New Issues
Notes (non-blocking)
Everything else — feature-flag gating, • branch |
There was a problem hiding this comment.
Pull request overview
Adds a feature-flagged, dependency-free HTML minification step to dotCMS’s rendering pipeline so rendered pages can be served (and, in LIVE mode, cached) without indentation/blank lines/comments introduced by Velocity templates—opt-in via FEATURE_FLAG_MINIFY_HTML.
Changes:
- Introduces
HtmlMinifierto conservatively collapse insignificant whitespace and strip HTML comments while preserving<pre>,<textarea>,<script>, and<style>bodies. - Hooks minification into
VelocityLiveMode.writePage()(before page cache write) and intoVelocityModeHandler.eval()(post-CSP processing path). - Adds
FEATURE_FLAG_MINIFY_HTMLand a new unit test suite for the minifier.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| dotCMS/src/main/java/com/dotcms/rendering/util/HtmlMinifier.java | New minifier implementation guarded by FEATURE_FLAG_MINIFY_HTML. |
| dotCMS/src/main/java/com/dotcms/rendering/velocity/servlet/VelocityLiveMode.java | Minifies LIVE mode output before writing/storing into the page cache when enabled. |
| dotCMS/src/main/java/com/dotcms/rendering/velocity/servlet/VelocityModeHandler.java | Minifies eval() output (after CSP application when configured). |
| dotCMS/src/main/java/com/dotcms/featureflag/FeatureFlagName.java | Adds the FEATURE_FLAG_MINIFY_HTML feature flag constant + javadoc. |
| dotCMS/src/test/java/com/dotcms/rendering/util/HtmlMinifierTest.java | Adds unit tests for whitespace significance, preserved regions, comments, and idempotence. |
| private static boolean isSignificantBefore(final StringBuilder out) { | ||
|
|
||
| final int lastChar = out.length() - 1; | ||
| if (out.charAt(lastChar) != '>') { | ||
| // Preceded by text content. | ||
| return true; | ||
| } | ||
|
|
||
| final int tagStart = out.lastIndexOf("<"); | ||
| if (tagStart < 0) { | ||
| return true; | ||
| } | ||
|
|
||
| return isInlineTag(out.substring(tagStart, out.length())); | ||
| } |
| /** | ||
| * Given: null or empty input. | ||
| * Expected: it is returned untouched rather than throwing. | ||
| */ | ||
| @Test | ||
| public void test_minifyIfEnabled_handles_null_and_empty() { | ||
| assertEquals(null, HtmlMinifier.minifyIfEnabled(null)); | ||
| assertEquals("", HtmlMinifier.minifyIfEnabled("")); | ||
| } | ||
| } |
Fixes #36851
Adds an opt-in HTML minifier so rendered pages are served without the indentation, blank lines, and line breaks that VTL templates, containers, and widgets carry for readability.
Off by default — enable with
FEATURE_FLAG_MINIFY_HTML=true.Proposed Changes
HtmlMinifier(new) — dependency-free minifier that strips insignificant whitespace and HTML comments. Deliberately conservative: it does not minify JS/CSS, rewrite attributes, or strip optional end tags.VelocityLiveMode.writePage()— minifies LIVE mode output before the page cache write, so the cost is paid once per cache fill rather than on every request.VelocityModeHandler.eval()— covers preview/edit/admin/navigate modes plus thegetPageHtmlcallers (PageResourceREST andPageRenderDataFetcherGraphQL). This method already post-processes for CSP, so minification follows the established pattern.FeatureFlagName— adds theFEATURE_FLAG_MINIFY_HTMLconstant.HtmlMinifierTest(new) — 11 test methods / 27 assertions covering the whitespace-significance and preserved-region edge cases.Why two seams instead of one filter
There is no single chokepoint for rendered HTML.
VelocityLiveMode.serve()streams directly toresponse.getOutputStream()and writes into the static page cache — it never returns throughgetPageHtml. A servlet filter or a hook inVelocityServletwould therefore have missed the highest-traffic path entirely. The two seams above are the minimum that covers every render path.Safety
The risky part of HTML minification is whitespace that looks removable but is actually rendered. This implementation:
<pre>,<textarea>,<script>, and<style>content byte-for-byte — protects rendered output and JavaScript automatic semicolon insertion.<span>a</span> <span>b</span>keeps its space and words are never joined. Whitespace bordering block elements is removed.<!--[if IE]>).<html>/<body>injection into fragments, no DOCTYPE case changes, no auto-closing of tags. This matters for partials, URL-mapped fragments, and non-HTML templates.eval().Checklist
Security notes: minification only removes whitespace and comments; it does not decode, re-encode, or re-escape content, so it cannot introduce XSS by unescaping. Comment stripping removes HTML comments from delivered pages, which slightly reduces incidental information disclosure. Ordering with CSP is preserved — in
eval(),ContentSecurityPolicyUtil.apply()still runs first, so nonce injection is unaffected.Additional Info
Library evaluation. Two candidates were assessed before writing custom code:
prettyPrint(false)preserves whitespace verbatim (no minification at all);prettyPrint(true)re-indents. It also normalizes markup — injecting<html><head></head><body>into every fragment and lowercasing<!DOCTYPE html>— which would break fragment and URL-mapped output.com.googlecode.htmlcompressoris abandoned (last release 2011). The maintained forkcom.github.hazendaz:htmlcompressor:2.0.2is safe and handles preserved regions correctly, but deliberately collapses inter-tag whitespace to a single space rather than removing it, so output still carries a space between every tag. It is also the same library the customer explicitly rejected running as a plugin (see Native, configurable HTML minification in the core rendering engine #36851).Neither delivers full whitespace removal without custom logic layered on top, so a small owned minifier — guarded by tests — was the path chosen. No new dependency, no BOM change.
Scope. HTML whitespace only. Inline JS/CSS minification is intentionally out of scope; it is substantially riskier and should be a separate discussion.
Rollout. Enabling the flag does not retroactively minify already-cached pages — they update as cache entries refill. Flush the page cache to make it immediate.
Testing note.
./mvnw test -pl :dotcms-corecurrently fails in my local environment before reaching any test, on an unresolved${net.bytebuddy:byte-buddy-agent:jar}surefire property. This is pre-existing and unrelated — an untouchedFileUtilTestfails identically. I verified the suite by compiling and running it directly against the module classpath:Worth confirming these run green in CI.
Screenshots
n/a — no UI changes.