Move docs/ package mirror into document-navigator plugin
Relocate the ~6k-file docs tree from the repo root into plugins/document-navigator/docs/ so the plugin is self-contained and portable. docs/superpowers/ (specs and plans) stays at the repo root. Moved via cp+rm rather than git mv because directory rename() fails with EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the identical-content files at commit time. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
# renderer
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
The renderer is the bridge between the `EmailTemplate<TProps>` contract and
|
||||
a final `{ html, text, subject }` triple ready to hand to an SMTP / SES / SendGrid
|
||||
client. It calls `template.component(merged_props)` to obtain a `VNode`,
|
||||
walks the tree through `renderVNode`, wraps the output in a full
|
||||
`<!doctype html>` document with the head boilerplate every email client
|
||||
expects, and (by default) derives a plain-text fallback from the HTML body
|
||||
via tag-stripping.
|
||||
|
||||
Every render emits OTel telemetry — `email_templates.render_total` counter
|
||||
tagged by `template` and `outcome`, plus a duration histogram. Errors raised
|
||||
by the component are caught and re-thrown as `EmailTemplatesError` with the
|
||||
`RENDER_FAILURE` code unless they were already a typed error from this
|
||||
package (those propagate unchanged).
|
||||
|
||||
## Features
|
||||
|
||||
- `renderToHtml(template, props, options?)` → `{ html, text, subject }`.
|
||||
- `wrapInDocument(body, options?)` — applies the doctype + head boilerplate.
|
||||
- `resolveSubject(template, props)` — pure helper for subject derivation.
|
||||
- `escapeHtml(input)` / `stripHtml(html)` — public utilities reused by
|
||||
upstream callers.
|
||||
- Injectable `theme` and `Clock` via render options.
|
||||
- Optional `minify: true` collapses whitespace between tags.
|
||||
- Optional `plainText: false` skips text-body generation.
|
||||
- MSO conditional comment (`<!--[if mso]>...<![endif]-->`) for Outlook
|
||||
PPI normalisation lives inside `wrapInDocument`.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { renderToHtml } from "@suaveplan/email-templates/renderer";
|
||||
import { welcomeTemplate } from "@suaveplan/email-templates/templates";
|
||||
|
||||
const { html, text, subject } = renderToHtml(welcomeTemplate, {
|
||||
recipientName: "Jane",
|
||||
ctaUrl: "https://app.example.com",
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Inject a fake clock for deterministic footer year rendering and pre-build a
|
||||
tenant-specific theme:
|
||||
|
||||
```ts
|
||||
import { renderToHtml } from "@suaveplan/email-templates/renderer";
|
||||
import { createTheme } from "@suaveplan/email-templates/theme";
|
||||
import { welcomeTemplate } from "@suaveplan/email-templates/templates";
|
||||
|
||||
const theme = createTheme({
|
||||
colors: { primary: "#ff6600" },
|
||||
branding: { companyName: "Acme" },
|
||||
});
|
||||
|
||||
const result = renderToHtml(
|
||||
welcomeTemplate,
|
||||
{ recipientName: "Jane", ctaUrl: "https://acme.test" },
|
||||
{
|
||||
theme,
|
||||
clock: { now: () => 1735689600000 }, // 2025-01-01T00:00:00Z
|
||||
previewText: "Welcome aboard, Jane!",
|
||||
minify: true,
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Symbol | Kind | Description |
|
||||
| ----------------- | -------- | ---------------------------------------------------- |
|
||||
| `renderToHtml` | function | Render a template to `{ html, text, subject }`. |
|
||||
| `wrapInDocument` | function | Wrap a body fragment in a full HTML document. |
|
||||
| `resolveSubject` | function | Resolve `template.subject` (literal or function). |
|
||||
| `escapeHtml` | function | Escape text for HTML inclusion. |
|
||||
| `stripHtml` | function | Tag-strip + entity-decode for plain-text fallback. |
|
||||
|
||||
Types: `EmailTemplate<TProps>`, `RenderOptions`, `RenderResult`,
|
||||
`HtmlDocumentOptions`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The renderer is **not** idempotent across `Date.now()` calls — render
|
||||
duration is recorded as `Date.now() - start`. Components, however, never
|
||||
read wall-clock time directly: the renderer threads an injected `Clock`
|
||||
(falling back to a real-`Date.now()` clock) into the merged props bag, so
|
||||
template-level determinism is preserved.
|
||||
|
||||
`stripHtml` is a regex pass — it does NOT round-trip nested HTML entities
|
||||
or HTML5 ambiguous markup. Email body content is expected to be the
|
||||
output of `renderVNode` (well-formed) and is reduced to a coarse
|
||||
plain-text representation suitable for clients that fall back to the
|
||||
text part. Production-grade plain-text rendering (line wrapping, link
|
||||
extraction, list bullets) is the consumer's responsibility.
|
||||
|
||||
Errors raised inside `template.component` are caught, the `error` outcome
|
||||
counter is bumped, and the original cause is preserved on the
|
||||
`EmailTemplatesError`. Typed errors already raised by this package are
|
||||
re-thrown without re-wrapping so their original `code` survives the
|
||||
boundary intact.
|
||||
Reference in New Issue
Block a user