Skip to main content

rewrite-html

The rewrite-html directive streams a route's HTML response through lol-html, applying ordered mutations to elements matched by CSS selectors. Use it to inject tags, rewrite attributes, or strip nodes from a static page or a proxied upstream — without buffering the whole body.

route "/*" {
proxy {
upstream "http://127.0.0.1:9000"
}

rewrite-html {
select "head" {
append "<script src=\"/analytics.js\" async></script>"
}
select "a[target=_blank]" {
set-attribute "rel" "noopener noreferrer"
}
select "img" {
set-attribute "loading" "lazy"
}
select ".legacy-banner" {
remove
}
select "title" {
set-text "Proxied by trillium"
}
}
}

Only HTML is touched

The rewriter self-gates on the response Content-Type: only responses whose subtype is html are transformed. JSON, images, and other binary bodies stream through untouched. That makes rewrite-html safe to drop in front of a mixed proxy that serves both pages and an API.

Because it transforms the body produced by the preceding directive, place rewrite-html after the body-producing directive (proxy or files) in the route.

Selectors are validated at load time

Each select takes a CSS selector. lol-html supports a subset of CSS selectors, and they're checked when the config loads (or under --check) — not on the first matching request. An unsupported or malformed selector fails immediately with a miette span pointing at the offending string.

Element operations

Each select "css-selector" block holds an ordered list of mutations applied to every matching element. Markup-valued operations insert their argument as HTML; set-text inserts HTML-escaped text.

OperationEffect
before "<markup>"insert markup immediately before the element
after "<markup>"insert markup immediately after the element
prepend "<markup>"insert markup as the element's first child
append "<markup>"insert markup as the element's last child
set-inner "<markup>"replace the element's inner content with markup
replace "<markup>"replace the element and its content with markup
set-text "text"replace inner content with HTML-escaped text
set-attribute "name" "val"set (or replace) an attribute
remove-attribute "name"remove an attribute
set-tag "div"rename the element's tag
removedelete the element and its content
unwrapdrop the element's tags but keep its inner content

Examples

Inject a script into every page from a static site:

route "/*" {
files root="./public"
rewrite-html {
select "body" {
append "<script src=\"/live-reload.js\"></script>"
}
}
}

Harden outbound links and lazy-load images on a proxied upstream:

route "/*" {
proxy {
upstream "http://legacy-app:9000"
}
rewrite-html {
select "a[target=_blank]" {
set-attribute "rel" "noopener noreferrer"
}
select "img" {
set-attribute "loading" "lazy"
}
}
}

Strip a node and rewrite the title:

route "/*" {
files root="./public"
rewrite-html {
select ".tracking-pixel" {
remove
}
select "title" {
set-text "My Site"
}
}
}