> ## Documentation Index
> Fetch the complete documentation index at: https://bun-1dd33a4e-farm-de84d354-pm-sbom.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# HTMLRewriter

> Use Bun's HTMLRewriter to transform HTML documents with CSS selectors

HTMLRewriter transforms HTML documents with CSS selectors. It works with `Response`, `string`, and `ArrayBuffer` inputs. Bun's implementation is based on Cloudflare's [lol-html](https://github.com/cloudflare/lol-html).

***

## Usage

A common use case is rewriting URLs in HTML content:

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
// Replace all images with a rickroll
const rewriter = new HTMLRewriter().on("img", {
  element(img) {
    // Famous rickroll video thumbnail
    img.setAttribute("src", "https://img.youtube.com/vi/dQw4w9WgXcQ/maxresdefault.jpg");

    // Wrap the image in a link to the video
    img.before('<a href="https://www.youtube.com/watch?v=dQw4w9WgXcQ" target="_blank">', {
      html: true,
    });
    img.after("</a>", { html: true });

    // Add some fun alt text
    img.setAttribute("alt", "Definitely not a rickroll");
  },
});

// An example HTML document
const html = `
<html>
<body>
  <img src="/cat.jpg">
  <img src="dog.png">
  <img src="https://example.com/bird.webp">
</body>
</html>
`;

const result = rewriter.transform(html);
console.log(result);
```

The rewriter replaces every image with a thumbnail of Rick Astley and wraps each `<img>` in a link, producing a diff like this:

```html theme={"theme":{"light":"github-light","dark":"dracula"}}
<html>
  <body>
    <img src="/cat.jpg" /> <!-- [!code --] -->
    <img src="dog.png" /> <!-- [!code --] -->
    <img src="https://example.com/bird.webp" /> <!-- [!code --] -->
    <a href="https://www.youtube.com/watch?v=dQw4w9WgXcQ" target="_blank"> <!-- [!code ++] -->
      <img src="https://img.youtube.com/vi/dQw4w9WgXcQ/maxresdefault.jpg" alt="Definitely not a rickroll" /> <!-- [!code ++] -->
    </a> <!-- [!code ++] -->
    <a href="https://www.youtube.com/watch?v=dQw4w9WgXcQ" target="_blank"> <!-- [!code ++] -->
      <img src="https://img.youtube.com/vi/dQw4w9WgXcQ/maxresdefault.jpg" alt="Definitely not a rickroll" /> <!-- [!code ++] -->
    </a> <!-- [!code ++] -->
    <a href="https://www.youtube.com/watch?v=dQw4w9WgXcQ" target="_blank"> <!-- [!code ++] -->
      <img src="https://img.youtube.com/vi/dQw4w9WgXcQ/maxresdefault.jpg" alt="Definitely not a rickroll" /> <!-- [!code ++] -->
    </a> <!-- [!code ++] -->
  </body>
</html>
```

Clicking any image now leads to [a very famous video](https://www.youtube.com/watch?v=dQw4w9WgXcQ).

### Input types

HTMLRewriter can transform HTML from several input types:

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
// From Response
rewriter.transform(new Response("<div>content</div>"));

// From string
rewriter.transform("<div>content</div>");

// From ArrayBuffer
rewriter.transform(new TextEncoder().encode("<div>content</div>").buffer);

// From Blob (wrap in a Response)
rewriter.transform(new Response(new Blob(["<div>content</div>"])));

// From File (wrap in a Response)
rewriter.transform(new Response(Bun.file("index.html")));
```

The Cloudflare Workers implementation of HTMLRewriter only supports `Response` objects.

### Element Handlers

The `on(selector, handlers)` method registers handlers for HTML elements that match a CSS selector. The handlers run for each matching element during parsing:

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
rewriter.on("div.content", {
  // Handle elements
  element(element) {
    element.setAttribute("class", "new-content");
    element.append("<p>New content</p>", { html: true });
  },
  // Handle text nodes
  text(text) {
    text.replace("new text");
  },
  // Handle comments
  comments(comment) {
    comment.remove();
  },
});
```

Handlers can be asynchronous and return a Promise. The transformation pauses on
that element until the Promise settles, so handlers still run one at a time, in
document order:

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
rewriter.on("div", {
  async element(element) {
    const fragment = await fetch("https://example.com/fragment").then(r => r.text());
    element.setInnerContent(fragment, { html: true });
  },
});
```

`transform(response)` returns immediately; the rewrite continues in the
background and you read the result off the returned `Response`. Reading it
paces the rewrite: a streamed input (a file, a `fetch()` response, a
`ReadableStream`) is pulled through only as fast as the returned body is
consumed, so a slow reader does not accumulate the whole document in memory. If
nothing reads the body, the rewrite still runs every handler to the end of the
document and buffers the output until it is read. Because the rewrite outlives
`transform()`, an error thrown by an async handler (or a Promise it returns that
rejects) rejects the response body instead of throwing from `transform()`:

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
try {
  const output = await rewriter.transform(new Response(html)).text();
} catch (error) {
  console.error("a handler failed:", error);
}
```

`transform()` on a `string` or `ArrayBuffer` has to return its result
synchronously, so it cannot wait for a handler that needs the event loop to turn
(a timer, I/O, a `fetch`). Such a handler makes `transform()` throw a
`TypeError`, and the rewrite fails without running any further handlers:

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
new HTMLRewriter()
  .on("div", {
    async element(element) {
      await Bun.sleep(1000); // needs a timer
    },
  })
  .transform("<div></div>");
// TypeError: HTMLRewriter.transform() cannot synchronously return a string
// because a content handler returned a Promise that did not resolve within a
// microtask. Pass a Response instead and await its body
```

A handler whose Promise settles within a microtask checkpoint (anything that
does not need the event loop, including `process.nextTick` and already-resolved
Promises) still works with `transform(string)`. Pass a `Response` whenever a
handler might await real work.

### CSS Selector Support

The `on()` method supports a wide range of CSS selectors:

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
// Tag selectors
rewriter.on("p", handler);

// Class selectors
rewriter.on("p.red", handler);

// ID selectors
rewriter.on("h1#header", handler);

// Attribute selectors
rewriter.on("p[data-test]", handler); // Has attribute
rewriter.on('p[data-test="one"]', handler); // Exact match
rewriter.on('p[data-test="one" i]', handler); // Case-insensitive
rewriter.on('p[data-test="one" s]', handler); // Case-sensitive
rewriter.on('p[data-test~="two"]', handler); // Word match
rewriter.on('p[data-test^="a"]', handler); // Starts with
rewriter.on('p[data-test$="1"]', handler); // Ends with
rewriter.on('p[data-test*="b"]', handler); // Contains
rewriter.on('p[data-test|="a"]', handler); // Dash-separated

// Combinators
rewriter.on("div span", handler); // Descendant
rewriter.on("div > span", handler); // Direct child

// Pseudo-classes
rewriter.on("p:nth-child(2)", handler);
rewriter.on("p:first-child", handler);
rewriter.on("p:nth-of-type(2)", handler);
rewriter.on("p:first-of-type", handler);
rewriter.on("p:not(:first-child)", handler);

// Universal selector
rewriter.on("*", handler);
```

### Element Operations

All element modification methods return the element instance, so calls can be chained:

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
rewriter.on("div", {
  element(el) {
    // Attributes
    el.setAttribute("class", "new-class").setAttribute("data-id", "123");

    const classAttr = el.getAttribute("class"); // "new-class"
    const hasId = el.hasAttribute("id"); // boolean
    el.removeAttribute("class");

    // Content manipulation
    el.setInnerContent("New content"); // Escapes HTML by default
    el.setInnerContent("<p>HTML content</p>", { html: true }); // Parses HTML
    el.setInnerContent(""); // Clear content

    // Position manipulation
    el.before("Content before").after("Content after").prepend("First child").append("Last child");

    // HTML content insertion
    el.before("<span>before</span>", { html: true })
      .after("<span>after</span>", { html: true })
      .prepend("<span>first</span>", { html: true })
      .append("<span>last</span>", { html: true });

    // Removal
    el.remove(); // Remove element and contents
    el.removeAndKeepContent(); // Remove only the element tags

    // Properties
    console.log(el.tagName); // Lowercase tag name
    console.log(el.namespaceURI); // Element's namespace URI
    console.log(el.selfClosing); // Whether element is self-closing (e.g. <div />)
    console.log(el.canHaveContent); // Whether element can contain content (false for void elements like <br>)
    console.log(el.removed); // Whether element was removed

    // Attributes iteration
    for (const [name, value] of el.attributes) {
      console.log(name, value);
    }

    // End tag handling
    el.onEndTag(endTag => {
      endTag.before("Before end tag");
      endTag.after("After end tag");
      endTag.remove(); // Remove the end tag
      console.log(endTag.name); // Tag name in lowercase
    });
  },
});
```

### Text Operations

Text chunks represent portions of text content and report their position in the text node:

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
rewriter.on("p", {
  text(text) {
    // Content
    console.log(text.text); // Text content
    console.log(text.lastInTextNode); // Whether this is the last chunk
    console.log(text.removed); // Whether text was removed

    // Manipulation
    text.before("Before text").after("After text").replace("New text").remove();

    // HTML content insertion
    text
      .before("<span>before</span>", { html: true })
      .after("<span>after</span>", { html: true })
      .replace("<span>replace</span>", { html: true });
  },
});
```

### Comment Operations

Comments support similar methods to text nodes:

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
rewriter.on("*", {
  comments(comment) {
    // Content
    console.log(comment.text); // Comment text
    comment.text = "New comment text"; // Set comment text
    console.log(comment.removed); // Whether comment was removed

    // Manipulation
    comment.before("Before comment").after("After comment").replace("New comment").remove();

    // HTML content insertion
    comment
      .before("<span>before</span>", { html: true })
      .after("<span>after</span>", { html: true })
      .replace("<span>replace</span>", { html: true });
  },
});
```

### Document Handlers

The `onDocument(handlers)` method registers handlers for events at the document level rather than within specific elements:

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
rewriter.onDocument({
  // Handle doctype
  doctype(doctype) {
    console.log(doctype.name); // "html"
    console.log(doctype.publicId); // public identifier if present
    console.log(doctype.systemId); // system identifier if present
  },
  // Handle text nodes
  text(text) {
    console.log(text.text);
  },
  // Handle comments
  comments(comment) {
    console.log(comment.text);
  },
  // Handle document end
  end(end) {
    end.append("<!-- Footer -->", { html: true });
  },
});
```

### Response Handling

When transforming a Response:

* The status code, headers, and other response properties are preserved
* The body is transformed while maintaining streaming capabilities
* Content-encoding (like gzip) is handled automatically
* The original response body is marked as used after transformation
* Headers are cloned to the new response

## Error Handling

Which channel an error takes is decided by the overload you called, never by
timing. `transform()` itself throws for:

* Invalid selector syntax in the `on()` method
* Invalid input types (for example, passing a Symbol)
* Body already used errors, and input bodies that have already failed or aborted
* Anything a content handler raises on a `string` / `ArrayBuffer` input, since
  those have to produce their result before `transform()` returns — including a
  handler that needs the event loop (see [Element Handlers](#element-handlers))

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
try {
  const result = rewriter.transform("<div></div>");
} catch (error) {
  console.error("HTMLRewriter error:", error);
}
```

For a `Response` input, `transform()` returns before the rewrite finishes, so
everything the rewrite discovers surfaces on the output body instead:

* An error thrown by a content handler, or a rejected Promise one returned
* Malformed or truncated input
* Stream errors reading the input body
* Memory allocation failures

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
try {
  const output = await rewriter.transform(new Response(html)).text();
} catch (error) {
  console.error("the rewrite failed:", error);
}
```

A rejection from a Promise a handler creates but neither returns nor awaits
reaches neither channel: like any detached rejection, it goes to the
process-global `unhandledRejection` path. Earlier versions of Bun could surface
it from `transform()` itself.

***

## See also

You can also read the [Cloudflare documentation](https://developers.cloudflare.com/workers/runtime-apis/html-rewriter/), which this API is intended to be compatible with.
