@kleb/templating (0.3.0)

Published 2026-07-14 11:00:18 +02:00 by kleb

Installation

@kleb:registry=
npm install @kleb/templating@0.3.0
"@kleb/templating": "0.3.0"

About this package

@kleb/templating

Email-focused React template rendering.

import { Body, Head, Html, Tailwind, Text } from "@react-email/components";
import { createEmailTailwindConfig, renderEmailTemplate } from "@kleb/templating";

const theme = createEmailTailwindConfig({
  colors: {
    background: "#ffffff",
    foreground: "#111827",
    primary: "#2563eb",
    primaryForeground: "#ffffff",
  },
});

const WelcomeEmail = ({ name }: { name: string }) => (
  <Html>
    <Tailwind config={theme}>
      <Head />
      <Body className="bg-background text-foreground">
        <Text>Hello {name}</Text>
      </Body>
    </Tailwind>
  </Html>
);

const rendered = await renderEmailTemplate(WelcomeEmail, { name: "Ada" });

Theme values should be resolved colors. Do not pass CSS variables directly to email templates. React Email's pixel-based Tailwind preset is included by default; pass { pixelBasedPreset: false } to disable it.

Use placeholder rendering for subjects, text bodies, or small snippets:

import { renderHtmlTemplateString, renderTemplateString } from "@kleb/templating";

const subject = renderTemplateString("Welcome, {{user.name}}", {
  user: { name: "Ada" },
});

const html = renderHtmlTemplateString("<p>Hello {{user.name}}</p>", {
  user: { name: "<Ada>" },
});

Missing placeholders throw by default. Pass { missing: "keep" } or { missing: "empty" } when that behavior is explicitly desired.

Use the static registry for a known set of reusable email templates. It preserves literal IDs and derives the props accepted by render and returned by get from the selected ID:

import { createEmailTemplateRegistry, defineEmailTemplate } from "@kleb/templating";

const registry = createEmailTemplateRegistry([
  defineEmailTemplate({
    id: "welcome",
    component: WelcomeEmail,
    subject: "Welcome {{name}}",
    preview: ({ name }) => `Hello ${name}`,
    sampleProps: { name: "Ada" },
  }),
]);

const rendered = await registry.render("welcome", { name: "Ada" });
const sample = await registry.renderSample("welcome");

Duplicate IDs are rejected when a registry is created. sampleProps is optional; renderSample throws if the selected definition does not provide it. Static validateProps remains available for additional runtime checks. The old caller-selected registry.render<Props>(...) and registry.get<Props>(...) overloads are deprecated but retained for one compatibility release. New code should omit those type arguments so the ID controls the props type. Let defineEmailTemplate infer props from the component, as above, to preserve the literal ID. Existing defineEmailTemplate<Props>(...) calls remain accepted but have a broad string ID; specify both generic arguments or remove the explicit one when migrating to strict ID checks.

Use the separately named dynamic registry when IDs or payloads cross a dynamic boundary. Its render method accepts unknown, and every definition must parse or validate that value before rendering:

import { createDynamicEmailTemplateRegistry, defineDynamicEmailTemplate } from "@kleb/templating";

const dynamicRegistry = createDynamicEmailTemplateRegistry([
  defineDynamicEmailTemplate({
    id: "welcome",
    component: WelcomeEmail,
    subject: "Welcome {{name}}",
    parseProps: (value: unknown): { name: string } => {
      if (
        typeof value !== "object" ||
        value === null ||
        !("name" in value) ||
        typeof value.name !== "string"
      ) {
        throw new TypeError("name must be a string");
      }
      return { name: value.name };
    },
  }),
]);

await dynamicRegistry.render(templateId, untrustedPayload);

Schema libraries fit this boundary without a package dependency: parseProps: (value) => schema.parse(value). Parse errors are propagated to the caller, and the parsed value is used for the subject, preview, and component.

createPreviewText truncates by Unicode code point so it never splits a surrogate pair. Its maxLength must be a non-negative safe integer and defaults to 90.

Markdown content can be wrapped as an email template with createMarkdownEmailTemplate.

Use the config integration when email theme tokens should be loaded through @kleb/config:

import { kConfig } from "@kleb/config/server";
import { emailThemeConfig } from "@kleb/templating/config";

const loaded = kConfig("app.json").section("emailTheme", emailThemeConfig()).load();

Dependencies

Dependencies

ID Version
@react-email/components ^1.0.12
@react-email/render ^2.0.10

Development dependencies

ID Version
@types/react 19.2.17
@types/react-dom 19.2.3
react 19.2.7
react-dom 19.2.7

Peer dependencies

ID Version
@kleb/config ^0.6.0
react ^18.0 || ^19.0 || ^19.0.0-rc
react-dom ^18.0 || ^19.0 || ^19.0.0-rc
Details
npm
2026-07-14 11:00:18 +02:00
11
UNLICENSED
6.9 KiB
Assets (1)
Versions (4) View all
0.4.0 2026-07-20
0.3.0 2026-07-14
0.2.0 2026-07-07
0.1.1 2026-07-04