feat(rendering): minify rendered HTML behind FEATURE_FLAG_MINIFY_HTML - #36852
Open
fmontes wants to merge 2 commits into
Open
feat(rendering): minify rendered HTML behind FEATURE_FLAG_MINIFY_HTML#36852fmontes wants to merge 2 commits into
fmontes wants to merge 2 commits 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>
12 tasks
Contributor
jcastro-dotcms
approved these changes
Jul 31, 2026
Contributor
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. |
Comment on lines
+150
to
+164
| 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())); | ||
| } |
Comment on lines
+144
to
+153
| /** | ||
| * 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("")); | ||
| } | ||
| } |
Contributor
🐳 PR Docker test imageLatest build for commit docker pull dotcms/dotcms-test:pr-36852-issue-36851-native-html-minification
docker pull dotcms/dotcms-test:pr-36852-issue-36851-native-html-minification_b0b84e6 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.