Skip to content

Document async-htm-to-string as a first-class Preact-free templating option #239

@bcomnes

Description

@bcomnes

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:

  1. Explains when to use it (SSR-only sites without client Preact components)
  2. Shows the migration path from htm/preact + preact-render-to-string
  3. Documents the three gotchas above
  4. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions