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
40 changes: 40 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Comment thread
bcomnes marked this conversation as resolved.

// 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`:

- **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