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.