diff --git a/README.md b/README.md
index 008f8fb..f783185 100644
--- a/README.md
+++ b/README.md
@@ -1284,6 +1284,46 @@ const layout: LayoutFunction<{site: string}, VDOMNode, string> = ({ children })
}
```
+### Using `async-htm-to-string` for string-based rendering
+
+DomStack's default layout pattern uses `htm/preact` and `preact-render-to-string` for HTML generation. If you only need server-side rendering and have no client-side Preact components, [`async-htm-to-string`](https://github.com/nicferrier/async-htm-to-string) is a lighter alternative. It uses the same `htm` tagged template syntax but renders directly to a string without a virtual DOM layer.
+
+```console
+npm install async-htm-to-string
+```
+
+```js
+import { html, rawHtml } from 'async-htm-to-string'
+
+// Note: rawHtml(children) assumes children is a pre-rendered HTML string from a
+// trusted source such as await page.renderInnerPage({ pages }). Do not pass unsanitized user input.
+export default async function layout ({ children, vars }) {
+ return await html`
+
+
+
${vars.title}
+ ${rawHtml(children)}
+
+ `
+}
+```
+
+Key differences from `htm/preact`:
+
+- **Attribute names are standard HTML.** Use `class` and `for` rather than React aliases like `className` and `htmlFor`, which `async-htm-to-string` will output literally with no warning. For attributes like `tabindex`, `tabIndex` is only a casing preference in HTML, but using standard lowercase keeps templates consistent.
+- **Always `await` the `html` tag.** The tag returns an object that resolves to a string asynchronously. If you return it without `await` from a non-async function (or assign it where a string is expected), you will get `[object Object]` in the output with no error thrown. Use `async function` and `await` the result.
+- **`rawHtml()` bypasses escaping.** It is equivalent to setting `innerHTML` directly. Use it only for HTML you generated yourself (such as the output of `await page.renderInnerPage({ pages })` or a trusted markdown renderer). `children` passed to a layout can be any type returned by a page function, and may contain unsanitized content depending on the page. Always verify the source before passing it through `rawHtml()`.
+
+```js
+import { html, rawHtml } from 'async-htm-to-string'
+
+// safe: vars.title is escaped automatically
+// children here is the HTML string produced by await page.renderInnerPage({ pages }) — trusted
+export default async function layout ({ children, vars }) {
+ return await html`${vars.title}
${rawHtml(children)}`
+}
+```
+
## Design Goals
- Convention over configuration. All configuration should be optional, and at most it should be minimal.