Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1439,6 +1439,48 @@ const layout: LayoutFunction<{site: string}, VDOMNode, string> = ({ children })
}
```

### Using `async-htm-to-string` for HTM-style string rendering

DomStack's default layout uses [`fragtml`][fragtml] because the bundled template only needs safe string manipulation. If you prefer HTM syntax for server-rendered layouts or pages, [`async-htm-to-string`](https://github.com/nicferrier/async-htm-to-string) is another good option: it keeps the familiar `htm` tagged template style but renders directly to a string without a virtual DOM layer.

This can pair nicely with projects that also use Preact for browser-side components. Keep Preact configured for your client bundles, then use `async-htm-to-string` in Node-side layouts or pages where you want string output rather than Preact VNodes.

```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`
<!DOCTYPE html>
<html lang="${vars.lang}">
<head><title>${vars.title}</title></head>
<body>${rawHtml(children)}</body>
</html>
Comment thread
bcomnes marked this conversation as resolved.
`
}
```

Key differences from `htm/preact` and DOMStack's `fragtml` default:

- **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`<main class="content"><h1>${vars.title}</h1>${rawHtml(children)}</main>`
}
```

## Design Goals

- Convention over configuration. All configuration should be optional, and at most it should be minimal.
Expand Down