> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://akamai.ferndocs.com/edge-workers/htmlrewriter/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://akamai.ferndocs.com/_mcp/server.
# html-rewriter
This module is available to use in your EdgeWorkers functions to consume and rewrite HTML documents. The html-rewriter module includes a built-in parser that emulates standard HTML parsing and DOM-construction.
> 📘 To learn more, go to the [Dynamic Content Assembly using the html-rewriter](html-rewriter-dynamic-content-assembly.md) use case in this guide.
An EdgeWorkers function can register callbacks on CSS selectors. When the parser encounters an element matching the selector, it executes the callback. The callback can insert new content around the element, modify the tag attributes, or remove the element entirely.
> 🚧 The HtmlRewritingStream **does not** escape inserted text. This means that you need to validate and escape user-supplied text to prevent [cross-site scripting (XSS)](https://developer.mozilla.org/en-US/docs/Web/Security/Types_of_attacks#cross-site_scripting_xss) vulnerabilities.
# HtmlRewritingStream Object
There are three steps to using the `html-rewriter`.
1. Create a new `HtmlRewritingStream`.
You need to create a new rewriter for each stream, because it's a stateful HTML parser.
2. Add one or more handlers using the `onElement()` method.
The handlers call functions on their argument to modify the stream.
3. Pipe an HTML stream through the rewriter object.
Consider this EdgeWorkers function that inserts a beacon script into a webpage.
```javascript
import { HtmlRewritingStream } from 'html-rewriter';
import { httpRequest } from 'http-request';
import { createResponse } from 'create-response';
export async function responseProvider(request) {
// Setup: Fetch a stream containing HTML
let htmlResponse = await httpRequest("/html");
if (!htmlResponse.ok) {
return createResponse(500, {}, `Failed to fetch doc: ${htmlResponse.status}`);
}
// (1) Create a new rewriter instance
let rewriter = new HtmlRewritingStream();
// (2) Add a handler to the rewriter: this one adds a ');
});
// (3) Use `pipeThrough()` to modify the input HTML with the rewriter
return createResponse(200, {}, htmlResponse.body.pipeThrough(rewriter))
}
```
The handler passed to the `onElement()` method inserts a new script tag into the HTML document.
This example shows how to insert a script tag.
```javascript
Sample PageText
```
Here's the updated code snippet that includes the specified script tag. A handler runs when it encounters the open tag. The operations that run on the handler's parameter can either occur immediately, or when the element closes.
```javascript
Sample PageText
```
> 👍 You can associate multiple handlers with an `HtmlRewritingStream`.
## Streaming
You can use the `HtmlRewritingStream` in pipe chains. It acts as a transform stream that you can use with `ReadableStream.pipeThrough()` and `ReadableStream.pipeTo()`.
When reading, instances of `HtmlRewritingStream` expect ArrayBuffers, which are interpreted as containing UTF8 characters. When reading from HTTP responses, the `response.body` can be streamed directly to rewriter instances.
## CSS Selectors
The html-rewriter library supports type, class, attribute, and ID CSS selectors, as well as child and descendent combinators.
You can, for example, rewrite a page to lazy load images, but load a hero image normally. In this example the hero image includes the ID "hero" so you can use `:not pseudoclass` with the ID selector `img:not(#hero)` to identify all of the non-hero images.
Here's the JavaScript for your EdgeWorkers function.
```javascript
import { HtmlRewritingStream } from 'html-rewriter';
import { httpRequest } from 'http-request';
import { createResponse } from 'create-response';
export async function responseProvider(request) {
return httpRequest('/index.html').then(async response => {
let rewriter = new HtmlRewritingStream();
rewriter.onElement('img:not(#hero)', el => {
el.setAttribute('loading', 'lazy');
});
return createResponse(200, {},
response.body.pipeThrough(rewriter)
);
});
}
```
If the `index.html` file contains the following details the hero image remains unchanged. Lazy load only applies to the other images.
**`HTML`**
```javascript HTML
```
The EdgeWorkers output will contain the following images.
**`HTML`**
```Text HTML
```
# Methods
## onElement()
Registers a handler to run when a CSS selector matches. The handler takes an Element object as a parameter and provides functions to modify the document.
| Parameter | Type | Description |
| --- | --- | --- |
| handler | Function | The function that runs when the selector matches. When the `HtmlRewritingStream` calls the handler, it passes an Element object as an argument.
Async handlers are not currently supported. It is not possible to await an `httpRequest()` call in the handler. |
| selector | String | A CSS selector that specifies when the handler should run.
Evaluates the selector string on the incoming text. It does not match on text inserted with the [Element methods](/edge-workers/htmlrewriter#methods-1). |
## Handler Execution Ordering
When multiple handlers match an element, they run in the order that `onHandler()` was called.
The example below specifies the ordering.
```javascript
rewriter.onElement('div#example', tag => tag.append('A'));
rewriter.onElement('div#example', tag => tag.append('B'));
rewriter.onElement('div#example', tag => tag.append('C'));
```
The ordering occurs when the HTML runs.
```javascript
```
The output in this example shows that the handler added on line one runs first, followed by the subsequent handlers.
```javascript
ABC
```
# Element Object
The Element object is an argument to the handler registered with the `onElement()`method. The handler calls functions on the Element to modify the output stream.
> 📘 You should not store the Element object. It's reused when calling each handler. Using the object outside of the handler that it was passed into may have unexpected results.
## Properties
## selector
The CSS string passed to `onElement()`.
## tag
The lowercase name of the matched HTML tag.
## Methods
The Element object supports methods for adding text around existing elements. The example below shows the output of processing `
original
`.
```javascript
el.before('before') el.after('after')
| |
---+-- --+--
before
PREPENDoriginalAPPEND
after
---+--- --+--
| |
el.prepend('PREPEND') el.append('APPEND')
```
## after()
Inserts new content immediately after the end tag of the matched element. The argument is the new text to insert.
```javascript
rewriter.onElement('div', el => el.after('AFTER'))
```
When given the input ``, the rewriter transforms it to `AFTER`.
If the original document doesn't include a close tag for the element, you can use the `insert_implicit_close` optional argument to differentiate between the element child and the appended text. For example, you can use the logic in the code sample below if you want to expand certain links, but `` tags don't appear reliably in the source document.
```javascript
rewriter.onElement('a[external]', el => {
const external = el.removeAttribute('external');
el.after(` (Original)`, {insert_implicit_close: true});
});
```
This lets you to gracefully handle malformed HTML with missing end tags.
```javascript
```
| Parameter | Type | Description |
| :-------- | :----- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| newText | String | Text to add. |
| options | Object | An optional object that allows you to insert a trailing tag. If it includes a `insert_implicit_close` property set to Boolean `true`, then a close tag will be added to the document. |
## append()
Inserts content at the end of the element.
This example adds scripts at the end of the `` element.
```javascript
rewriter.onElement('head', el => {
el.append('\n');
el.append('\n');
});
```
`` is re-written to the following.
```javascript
```
| Parameter | Type | Description |
| --- | --- | --- |
| newText | String | Text to insert at the end of the element. Repeated calls act like a FIFO queue. It inserts the contents of the first call first, and inserts the contents of the last call next to the end tag. |
| option | Object | **(Optional)** An argument that controls the insertion of a missing end tag. The rewriter will add the appropriate end tag to an implicitly closed tag of an Object if `insert_implicit_close` is set to `true`. |
## before()
Inserts text immediately before the start tag of the matched element.
This example inserts a leading div before the title.
```javascript
rewriter.onElement('h2.product-title', el => {
el.before("
");
el.removeAttribute('class');
});
```
It transforms `
Cheese slicer
` to a micro friendly format.
```javascript
Cheese slicer
```
| Parameter | Type | Description |
| :-------- | :----- | :------------------------------------------- |
| newText | String | Text to add before the start of the element. |
## getAttribute()
Reads the value of an attribute name on the tag, returning `undefined` if the attribute does not exist.
This example changes the script path.
```javascript
rewriter.onElement('head script[type=module]', el => {
const src = el.getAttribute('src');
el.setAttribute('src', src.replace('v1', 'v2'));
});
```
It uses the following input.
```javascript
```
The rewriter changed the `v1` in the path to `v2`.
```javascript
```
| Parameter | Type | Description |
| :-------- | :----- | :----------------------------------------------------- |
| name | String | The case-insensitive name of the attribute to extract. |
## prepend()
Inserts content right after the start tag of the element.
This example adds an `onElement` element to preload directives to a `` element.
```javascript
rewriter.onElement('head', el => {
el.prepend("");
});
```
The rewriter changed `` to the following.
```javascript
```
| Parameter | Type | Description |
| --- | --- | --- |
| newText | String | New text to insert. Inserts the text immediately after the start tag, but before the content.
Calls insert the text immediately after the start tag. Repeated calls act like a LIFO queue. Inserts the contents of the last call first, and inserts the contents of the first call last. |
## removeAttribute()
Removes an attribute if it exists. Returns the value.
This example removes the `background` element of a body.
```javascript
rewriter.onElement('body', el => {
el.removeAttribute('background')
});
```
| Parameter | Type | Description |
| :-------- | :----- | :------------------------------- |
| name | String | Case-insensitive attribute name. |
## replaceChildren()
Removes the children of the current element and inserts content in place of them. Leaves the tags intact.
This example removes an inline script and instead loads a remote script.
```javascript
rewriter.onElement('script', el => {
el.setAttribute('src', 'cached.js');
el.replaceChildren('');
});
```
Running on an input of ``, produces the following output.
```javascript
```
| Parameter | Type | Description |
| --- | --- | --- |
| newText | String | The text to insert between the start and end tags. If `replaceChildren()` is called multiple times, the value passed in to the last invocation is inserted. |
| options | Object | An optional argument that controls the insertion of a missing end tag. The rewriter will add the appropriate end tag to an implicitly closed tag of an Object if `insert_implicit_close` is set to `true`. |
## replaceWith()
Removes the tags and element children. Inserts the passed content in its place.
| Parameter | Type | Description |
| --- | --- | --- |
| newText | String | The text inserted in place of the element and its children. Inserts the value passed in the last invocation if `replaceWith()` is called multiple times. |
This example registers a new callback.
```javascript
rewriter.onElement('div.loggedIn', el => el.replaceWith(''))
```
It uses the following input.
```javascript
Welcome to our site
Cart is empty
Products
```
The rewriter transforms the callback.
```javascript
Welcome to our site
Products
```
## setAttribute()
Sets the value of the named attribute. Creates the attribute if one does not exist.
The following example.
```javascript
rewriter.onElement('div', el => {
el.setAttribute('single', 'single', {quote: "'"});
el.setAttribute('double', 'double', {quote: '"'});
});
```
When run on `
` produces the results below.
```javascript
```
| Parameter | Type | Description |
| --- | --- | --- |
| name | String | The name is case insensitive. If the name contains an illegal character, the function will throw a TypeError. |
| value | String | The value of the attribute. If the string contains illegal characters, they will be escaped. |
| options | Object | **Optional ** Controls the application of quotes to the attribute value. It must include a property named `quote`, whose value is a string containing either a single or double quote. |
> 👍 A number of functions support an optional `TrailingOpt` argument. If the argument is present, the options object must include a property named `insert_implicit_close` with a boolean value. When the value is `true`, elements that are missing a close tag will have one inserted.
## Insertion Ordering
When inserting text, the insertion point remains the same, even after other insertions. Consider a handler that uses multiple append statements.
```javascript
1 rewriter.onElement('div', tag => {
2 tag.after('A'));
3 tag.after('B'));
4 tag.after('C'));
5 }
```
The `after()` insertion point is the end of the close tag. This means that an input of `` produces an output of `CBA`.
- The `after()` on line two inserts the A next to the close angle bracket.
- The `after()` on line three inserts the B between the close angle bracket and the previously inserted A.
- The `after()` on line four inserts the C between the close angle bracket and the previously inserted B.
## Replacement and Nested Handlers
Handler matching is disabled during replacement.
For example, if you provide the following input.
```javascript
internal
```
With the following handlers.
```javascript
rewriter.onElement('div#doomed', el => el.replaceWith(''));
rewriter.onElement('div#doomed i', el => el.append('APPENDED'));
```
The handler that matches on `` will not run.
That means your handlers must not rely on side-effects such as, modifying variables in the module scope, when replacement is occurring.
# Development Tips
When creating a new handler, it's helpful to iterate on a controlled input. Rather than using an external document for your input, it's possible to use a locally defined `ReadableStream`.
```javascript
import { HtmlRewritingStream } from 'html-rewriter';
import { ReadableStream } from 'streams';
import { createResponse } from 'create-response';
import { TextEncoderStream } from 'text-encode-transform';
export async function responseProvider(request) {
let source = new ReadableStream({
start(controller) {
controller.enqueue("");
controller.enqueue("
hi");
controller.close();
}
});
const rewriter = new HtmlRewritingStream();
rewriter.onElement('head', el => el.append(''));
return createResponse(200, {}, source.pipeThrough(new TextEncoderStream()).pipeThrough(rewriter));
}
```
Here's the expected output.
```javascript
hi
```
The `ReadableStreams` pattern is helpful when writing small integration tests.
> 📘 The `TextEncoderStream` is necessary because strings are written into the pipeline, rather than typed arrays.