Engine for turning your named email templates into HTML. This package does not ship templates. You pass a catalog and render functions.
Requires Node.js >= 22.18. Disk writes go through markup-generator.
Working on this repo with an AI agent? Start at AGENTS.md.
Current version: 1.6.1. Published on GitHub Packages.
1.0.1 is the release Postmark depends on. 0.2.0 was an unpublished markup-generator branch, not a release.
npm install @llazyemail/generate-templateInstalls need a read:packages token. See .npmrc.example.
This is the generation layer. It owns catalog lookup, payload resolution, the run/writeAll contract, and disk writes.
@llazyemail/render-template— low-level deterministic part/compose renderer (string parts only).@llazyemail/template-runtime-display— email shell fragments + runtime that builds onrender-template.- Consumer projects (e.g.
_playing_with_lit) should inject their ownrenderfunctions (Lit SSR, React, etc.) intocreateGeneratorrather than reimplementing catalog, payload, or write logic locally.
Local BuildPipeline-style classes that reimplement validation + render + write are duplication. Prefer a catalog entry whose render calls your existing renderer, then generate.run({ templateId, payload }).
import {
createGenerator,
GenerateTemplateError,
type GenerateRequest,
type GenerateResult,
} from '@llazyemail/generate-template';
const generate = createGenerator({
catalog: [{ ids: ['welcome'], render: (p) => `<p>${(p as { name: string }).name}</p>` }],
samplePayloads: { welcome: { name: 'Alex' } },
});
const html = await generate.render('welcome');
const result = await generate.run({ templateId: 'welcome', payload: { name: 'Alex' } });run(request) is the contract a future HTTP handler wraps. Failures are GenerateTemplateError with code:
UNKNOWN_TEMPLATE | NO_PAYLOAD | RENDER_FAILED | WRITE_FAILED | INVALID_CONFIG | CONFIG_NOT_FOUND | CONFIG_IMPORT_FAILED | CONFIG_INVALID.
render / loadPayload are async. File catalog entries need allowFileTemplates: true (prefer injected render).
Date revival is off unless reviveDates: true.
The bin loads a project catalog. It does not ship one.
// generate-template.config.js
export { createProjectGenerator } from './scripts/create-project-generator.js';Or set package.json "generateTemplate": "./scripts/create-project-generator.js". That module must export createProjectGenerator(), generator, or a default function returning a generator. --config= overrides the search. A missing or invalid config throws GenerateTemplateError with CONFIG_NOT_FOUND, CONFIG_IMPORT_FAILED, or CONFIG_INVALID.
generate-template --help
generate-template --list--help prints usage. With no flags and no config, the bin prints usage and exits 1. --all (also the default when a config exists and --template is omitted) calls writeAll(outDir). A single --template calls run(), so --data= still loads through the same createGenerator factory as the library. run({ write }) renders once. generate-template-assert is the assert command without the assert subcommand.
Passing a generator to main(argv, generator) still skips the config file. That is what a project script can do. The bin itself always calls main() — it does not compare argv[1] to its own file, so an npm .bin symlink still runs.
sandbox/ is a local proof, not part of the package. It renders three package-owned fixtures through createGenerator + run() and writes sandbox/generated/.
npm run sandboxOpen sandbox/generated/index.html after it succeeds. package.json files and .npmignore keep sandbox/, src/, and tests/ out of the published tarball.
npm test
npm run test:coverage
npm run typecheck
npm run lint
npm run build
npm run sandbox
npx changesetVersions are bumped by Changesets. Add a changeset for user-facing changes. Merging the Version Packages pull request updates CHANGELOG.md; publishing still happens when a GitHub Release is published.