Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
cb242c7
feat(rendering): minify rendered HTML behind FEATURE_FLAG_MINIFY_HTML
fmontes Jul 31, 2026
b0b84e6
Merge branch 'main' into issue-36851-native-html-minification
zJaaal Aug 4, 2026
345b978
fix(rendering): stop HtmlMinifier from altering rendered content #36851
zJaaal Aug 5, 2026
3615147
Merge branch 'main' into issue-36851-native-html-minification
zJaaal Aug 5, 2026
12ec1ca
test(rendering): lock in HtmlMinifier comment-boundary and commented-…
zJaaal Aug 5, 2026
b6e2f15
test(rendering): assert HtmlMinifier integrity as an invariant, over …
zJaaal Aug 5, 2026
e4d90b5
test(rendering): scrub the minification corpus and document its handl…
zJaaal Aug 5, 2026
91279ca
Merge branch 'main' into issue-36851-native-html-minification
zJaaal Aug 5, 2026
5973036
ci(rendering): run the Postman Page collection again with HTML minifi…
zJaaal Aug 5, 2026
e84afc5
Merge branch 'main' into issue-36851-native-html-minification
zJaaal Aug 6, 2026
5b3def5
feat(rendering): declare FEATURE_FLAG_MINIFY_HTML in dotmarketing-con…
zJaaal Aug 6, 2026
8a60461
test(rendering): cover HTML minification with a Postman collection, n…
zJaaal Aug 7, 2026
d0b1450
test(rendering): guard the preserved-region lookup against the weak-b…
zJaaal Aug 7, 2026
0c9f74f
Merge branch 'main' into issue-36851-native-html-minification
zJaaal Aug 7, 2026
df6a7fd
fix(test): correct the visible-text comparison in the HTML minificati…
zJaaal Aug 7, 2026
41a6186
Merge branch 'main' into issue-36851-native-html-minification
zJaaal Aug 7, 2026
04e766c
fix(rendering): keep whitespace across a removed comment #36851
zJaaal Aug 7, 2026
7acf365
Merge branch 'main' into issue-36851-native-html-minification
zJaaal Aug 10, 2026
4f1cfb2
Merge branch 'main' into issue-36851-native-html-minification
zJaaal Aug 10, 2026
25a94ac
fix(rendering): keep whitespace beside invisible and replaced inline …
zJaaal Aug 10, 2026
7df4ab7
test(rendering): cover the gaps in the whitespace-adjacency coverage …
zJaaal Aug 10, 2026
90cd646
fix(rendering): keep whitespace beside conditional comments and non-H…
zJaaal Aug 10, 2026
451074b
fix(rendering): tighten the non-HTML guard and drop a copy from its f…
zJaaal Aug 10, 2026
0be584b
Merge branch 'main' into issue-36851-native-html-minification
zJaaal Aug 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,14 @@ public interface FeatureFlagName {
*/
String FEATURE_FLAG_EDIT_CONTENT_SIDE_PANEL = "FEATURE_FLAG_EDIT_CONTENT_SIDE_PANEL";

/**
* Minifies rendered page HTML (collapses insignificant whitespace and strips comments) before
* it is written to the response. Off by default.
*
* @see com.dotcms.rendering.util.HtmlMinifier
*/
String FEATURE_FLAG_MINIFY_HTML = "FEATURE_FLAG_MINIFY_HTML";

/**
* libvips image-engine toggle (off by default; the legacy Java2D engine is used
* otherwise). The new image editor reads this through the configuration endpoint
Expand Down
559 changes: 559 additions & 0 deletions dotCMS/src/main/java/com/dotcms/rendering/util/HtmlMinifier.java
Comment thread
zJaaal marked this conversation as resolved.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import com.dotcms.api.web.HttpServletRequestThreadLocal;
import com.dotcms.api.web.HttpServletResponseThreadLocal;
import com.dotcms.rendering.util.HtmlMinifier;
import com.dotcms.rendering.velocity.services.VelocityResourceKey;
import com.dotcms.rendering.velocity.util.VelocityUtil;
import com.dotcms.security.ContentSecurityPolicyUtil;
Expand Down Expand Up @@ -51,11 +52,18 @@
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.constraints.NotNull;
import org.apache.commons.io.output.StringBuilderWriter;
import org.apache.commons.io.output.TeeOutputStream;
import org.apache.velocity.context.Context;

public class VelocityLiveMode extends VelocityModeHandler {

/**
* Initial size of the in-memory buffer used when minification is on. Big enough that a typical
* page never triggers a grow-and-copy, and small enough to be irrelevant if a page is tiny.
*/
private static final int MERGE_BUFFER_INITIAL_CAPACITY = 32 * 1024;

final static ThreadLocal<ByteArrayOutputStream> byteArrayLocal = ThreadLocal.withInitial(
ByteArrayOutputStream::new);
final static long PAGE_CACHE_TIMEOUT_MILLIS = Config.getIntProperty("PAGE_CACHE_TIMEOUT_MILLIS", 2000);
Expand Down Expand Up @@ -267,7 +275,25 @@ PageCacheParameters buildCacheParameters(final long langId, final IHTMLPage html
*/
private void writePage(final Writer out, final IHTMLPage htmlPage) {
final Context context = VelocityUtil.getInstance().getContext(request, response);
this.getTemplate(htmlPage, mode).merge(context, out);

if (!HtmlMinifier.isEnabled()) {
this.getTemplate(htmlPage, mode).merge(context, out);
return;
}

// Merge into memory first so the markup can be minified as a whole. What is written here is
// also what gets stored in the page cache, so minification happens once per cache fill
// rather than on every cache hit.
//
// StringBuilderWriter rather than StringWriter: the latter is backed by a synchronized
// StringBuffer, and Velocity emits a page as many hundreds of small writes, so the lock is
// taken on every one of them. Sizing the buffer up front avoids the repeated grow-and-copy.
// minifyBestEffort, not minifyIfEnabled, because the flag was already read above and reading
// it twice per render buys nothing.
final StringBuilderWriter merged = new StringBuilderWriter(MERGE_BUFFER_INITIAL_CAPACITY);
this.getTemplate(htmlPage, mode).merge(context, merged);
Try.run(() -> out.write(HtmlMinifier.minifyBestEffort(merged.toString())))
.getOrElseThrow(DotRuntimeException::new);
}


Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.dotcms.rendering.velocity.servlet;

import com.dotcms.rendering.util.HtmlMinifier;
import com.dotcms.rendering.velocity.services.VelocityResourceKey;
import com.dotcms.rendering.velocity.services.VelocityType;
import com.dotcms.rendering.velocity.util.VelocityUtil;
Expand Down Expand Up @@ -105,12 +106,11 @@ public final String eval() {
try(ByteArrayOutputStream out = new ByteArrayOutputStream(4096)) {
serve(out);

if (ContentSecurityPolicyUtil.isConfig()) {
final String htmlCode = new String(out.toByteArray(), StandardCharsets.UTF_8);
return ContentSecurityPolicyUtil.apply(htmlCode);
} else {
return new String(out.toByteArray(), StandardCharsets.UTF_8);
}
final String htmlCode = new String(out.toByteArray(), StandardCharsets.UTF_8);

return ContentSecurityPolicyUtil.isConfig()
? HtmlMinifier.minifyIfEnabled(ContentSecurityPolicyUtil.apply(htmlCode))
: HtmlMinifier.minifyIfEnabled(htmlCode);
} catch (DotDataException | IOException | DotSecurityException e) {
Logger.debug(VelocityModeHandler.class, e.getMessage(), e);
throw new DotRuntimeException(e);
Expand Down
5 changes: 5 additions & 0 deletions dotCMS/src/main/resources/dotmarketing-config.properties
Original file line number Diff line number Diff line change
Expand Up @@ -883,6 +883,11 @@ FEATURE_FLAG_LOCALE_SELECTOR_V2=true
## of navigating full-screen (Content Drive) or a centered dialog (UVE).
FEATURE_FLAG_EDIT_CONTENT_SIDE_PANEL=true

## Native HTML minification of rendered pages. Off by default (rollback safety: pages are
## served exactly as the templates render them). Collapses insignificant whitespace and strips
## HTML comments; see com.dotcms.rendering.util.HtmlMinifier.
FEATURE_FLAG_MINIFY_HTML=false

## libvips image engine toggle. On by default; the engine still requires the native
## libvips library to be present, otherwise it falls back to the legacy Java2D engine
## at runtime. Set to false to force the legacy engine. The new image editor reads this
Expand Down
Loading
Loading