This issue was originally identified in voxpelli/voxpelli.github.com#32
Problem
DomStack's default layout uses htm/preact combined with preact-render-to-string for server-side HTML generation. This works well for Preact users, but introduces a Preact dependency that is unnecessary for sites that only need server-side HTML rendering and have no client-side component model.
The async-htm-to-string package provides a lightweight alternative: same htm tagged template syntax but renders directly to HTML strings without a virtual DOM layer. Users wanting this approach must currently discover and wire up async-htm-to-string entirely on their own, with no guidance from DomStack documentation or tooling.
Key differences from the Preact approach
|
htm/preact |
async-htm-to-string |
| Attributes |
className, htmlFor, tabIndex |
class, for, tabindex |
| Return value |
Preact VNode |
Promise<string> |
| Extra step |
renderToString(html\...`)` |
await html\...`` |
| Dependencies |
preact, preact-render-to-string |
async-htm-to-string only |
Gotchas users need to know
1. Standard HTML attributes, not React aliases
Using className with async-htm-to-string produces className="foo" literally in the HTML output — an invalid attribute that browsers ignore silently. No warning or error.
2. rawHtml() XSS risk
async-htm-to-string provides rawHtml() for injecting pre-rendered HTML strings without escaping. This is a direct XSS vector if used with user-controlled content. It should be treated like innerHTML assignment — only use for content you control or have explicitly sanitized.
3. Silent [object Object] failure
The html function returns an HtmlMethodResult object, not a plain string. If returned without await, downstream code produces [object Object] literally in the HTML output — no error, no warning, just broken output:
// BUG: Missing await
export default function ({ vars }) {
return html`<h1>${vars.title}</h1>`; // Returns object, not string
}
// CORRECT
export default async function ({ vars }) {
return await html`<h1>${vars.title}</h1>`;
}
Proposed solution
Option A (minimum): Document as recommended Preact-free alternative
Add a section to DomStack documentation that:
- Explains when to use it (SSR-only sites without client Preact components)
- Shows the migration path from
htm/preact + preact-render-to-string
- Documents the three gotchas above
- Provides a helper pattern for composing partials with
rawHtml()
This requires no code changes to DomStack itself.
Option B (follow-on): Built-in page.htm.js page type
A new page builder that recognizes *.page.htm.js files and automatically wraps the return value — eliminating the [object Object] failure mode and reducing boilerplate to zero imports.
Recommendation: Option A immediately, Option B upstream once demand is validated.
Problem
DomStack's default layout uses
htm/preactcombined withpreact-render-to-stringfor server-side HTML generation. This works well for Preact users, but introduces a Preact dependency that is unnecessary for sites that only need server-side HTML rendering and have no client-side component model.The
async-htm-to-stringpackage provides a lightweight alternative: samehtmtagged template syntax but renders directly to HTML strings without a virtual DOM layer. Users wanting this approach must currently discover and wire upasync-htm-to-stringentirely on their own, with no guidance from DomStack documentation or tooling.Key differences from the Preact approach
htm/preactasync-htm-to-stringclassName,htmlFor,tabIndexclass,for,tabindexrenderToString(html\...`)`await html\...``preact,preact-render-to-stringasync-htm-to-stringonlyGotchas users need to know
1. Standard HTML attributes, not React aliases
Using
classNamewithasync-htm-to-stringproducesclassName="foo"literally in the HTML output — an invalid attribute that browsers ignore silently. No warning or error.2.
rawHtml()XSS riskasync-htm-to-stringprovidesrawHtml()for injecting pre-rendered HTML strings without escaping. This is a direct XSS vector if used with user-controlled content. It should be treated likeinnerHTMLassignment — only use for content you control or have explicitly sanitized.3. Silent
[object Object]failureThe
htmlfunction returns anHtmlMethodResultobject, not a plain string. If returned withoutawait, downstream code produces[object Object]literally in the HTML output — no error, no warning, just broken output:Proposed solution
Option A (minimum): Document as recommended Preact-free alternative
Add a section to DomStack documentation that:
htm/preact+preact-render-to-stringrawHtml()This requires no code changes to DomStack itself.
Option B (follow-on): Built-in
page.htm.jspage typeA new page builder that recognizes
*.page.htm.jsfiles and automatically wraps the return value — eliminating the[object Object]failure mode and reducing boilerplate to zero imports.Recommendation: Option A immediately, Option B upstream once demand is validated.