npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@stackoverflow/stacks-email

v1.0.0-beta.0

Published

Stack Overflow email components, templates, and compilation tools

Downloads

15

Readme

Stacks Email

Stack Overflow’s MJML powered email compile engine with a tokenized component library and template library

Component schema definitions

New components should use the schema helpers in src/lib/schema so rendering, defaults, validation, and generated docs stay in one place.

import {
    defineEmailComponent,
    defineOption,
    defineOptions,
    mjmlAlignOptions,
} from "../src/lib/schema";

const headline = defineEmailComponent({
    slug: "headline",
    variants: {
        highlight: {
            highlight: true,
        },
    },
    optionsSchema: defineOptions([
        defineOption({
            name: "textClass",
            type: "string",
            initialValue: "s-email-text-headline",
            description: "Text styling class for the headline node.",
        }),
        defineOption({
            name: "highlight",
            type: "boolean",
            initialValue: false,
            description: "Applies inline highlighted text styling.",
        }),
        defineOption({
            name: "textAlign",
            type: "enum",
            values: mjmlAlignOptions,
            initialValue: "left",
            description: "MJML text alignment.",
        }),
    ]),
    render: ({ options }) => {
        // Return a MJML JSON object
        // https://documentation.mjml.io/#using-mjml-in-json
        // options.textClass is string
        // options.highlight is boolean
    },
});

export const Headline = headline.component;
  • defineOption is the Sanity-style field declaration for component options.
  • initialValue is the single source of truth for runtime defaults and the generated options table default.
  • Use type: "enum" with a shared values tuple for constrained string values like MJML alignment. The generated schema validates the value and TypeScript infers the literal union.
  • optional: true should only be used when omitted is meaningfully different from the initial value.
  • The default variant is implied from schema defaults. Add entries in variants only for named variants that override option defaults. Variant values are plain option overrides.
  • defineOptions([...]) builds the underlying Zod schema, so API validation and render typing are derived from the option declarations.
  • Export the .component as a PascalCase callable alongside the default export, so templates can compose components directly e.g., Headline("highlight", { ... })

Template schema definitions

Templates use the same defineOption / defineOptions helpers as components, so defaults, types, and generated docs come from a single schema. Variants are plain prop overrides — the same shape used for component variants. The default variant’s values live in the schema’s initialValues; named variants list only what they change.

import {
    defineEmailTemplate,
    defineOption,
    defineOptions,
} from "../src/lib/schema";
import { Headline } from "../components/headline";

const transactional = defineEmailTemplate({
    slug: "transactional",
    defaultVariant: "short",
    variants: {
        long: {
            headlineText: "Privacy Policy Update",
        },
    },
    propsSchema: defineOptions([
        defineOption({
            name: "headlineText",
            type: "string",
            initialValue: "Reset your password",
            description: "Headline copy rendered near the top of the email.",
        }),
    ]),
    preview: ({ props }) => ({
        previewText: props.headlineText,
    }),
    renderDocument: ({ variant, props }) => ({
        tagName: "mjml",
        children: [
            // Compose components via their named PascalCase exports.
            Headline(variant === "long" ? "default" : "highlight", {
                textContent: props.headlineText,
            }),
        ],
    }),
});

export default transactional;

renderDocument returns the full MJML document tree; the compile pipeline handles MJML serialization, shared config injection, token transformation, and HTML output.

Run local sandbox

npm install
npm run dev -w @stackoverflow/stacks-email

API

The compile engine can be used two ways: as a deployed HTTP endpoint that any service can POST to, or as a library imported directly into another app. Both paths run the same pipeline — schema validation, shared config injection, token transformation, and HTML output — so a component or template behaves identically however you reach it.

Every compile takes a target, which selects the set of token substitutions applied to the output. Placeholders like [[FIRST_NAME]] or [[UNSUBSCRIBE_URL]] are rewritten per target:

  • preview — sample values for previewing in a browser or inbox test.
  • dotnet — Razor model bindings (@Model.FirstName), for the .NET mailer.
  • braze — Braze Liquid ({{${first_name}}}), for the Braze platform.

As a deployed endpoint

The package is a SvelteKit app, so npm run build produces a deployable server (via @sveltejs/adapter-auto) that exposes POST /api/compile. Use this when a backend in another language or another service needs compiled email HTML without embedding the engine itself.

The endpoint supports two request shapes:

  • Compile any registered template by passing template, target, and optional props.
  • Compose a transactional email from an ordered list of blocks. Each block names a component type (headline, text, button, title, spacer), an optional variant/size, and optional props. The header, footer, and surrounding spacing are added automatically.
curl -X POST https://email.stackoverflow.design/api/compile \
  -H "Content-Type: application/json" \
  --data '{
    "template": "newsletter",
    "target": "braze",
    "props": {
      "previewText": "The Stack Overflow Newsletter"
    }
  }'
curl -X POST https://email.stackoverflow.design/api/compile \
  -H "Content-Type: application/json" \
  --data '{
    "template": "transactional",
    "target": "braze",
    "previewText": "Reset your password",
    "blocks": [
      { "type": "headline", "props": { "textContent": "Reset your password" } },
      { "type": "text", "props": { "textContent": "Click below to continue." } },
      { "type": "button", "props": { "text": "Reset", "href": "[[BUTTON_URL]]" } }
    ]
  }'

The response is JSON and includes compiled html, final mjml, renderedMjml, compile errors, and metadata such as template and target. Block composition responses also include blockCount. Invalid bodies return 400 with a human-readable error describing the failed field; compile failures return 500.

Auth (optional)

POST /api/compile supports an optional shared Bearer token.

  • If STACKS_EMAIL_AUTH_TOKEN is not set: auth is disabled.
  • If STACKS_EMAIL_AUTH_TOKEN is set: the request must include Authorization: Bearer <token> or it returns 401.
curl -X POST http://localhost:5173/api/compile \
  -H "Authorization: Bearer $STACKS_EMAIL_AUTH_TOKEN" \
  -H "Content-Type: application/json" \
  --data '{"template":"transactional","target":"preview","blocks":[{"type":"headline"}]}'

As a library in another app

The package’s main export (@stackoverflow/stacks-email) is a framework-agnostic API surface — no SvelteKit required. Import it directly when you want to compile in-process: a build script, a CMS, a queue worker, or your own HTTP server.

import {
    getEmailCatalog,
    compileEmailRenderable,
    compileEmailTemplate,
    compileEmailComponent,
} from "@stackoverflow/stacks-email";

// Discover what can be compiled — every component and template, with their
// variants, options, and the tokens each one exposes.
const { components, templates } = getEmailCatalog();

// Compile a full template for the Braze target.
const email = await compileEmailTemplate({
    slug: "transactional",
    target: "braze",
    props: { headlineText: "Reset your password" },
});

console.log(email.html); // ready-to-send HTML

// Compile a single component (e.g. to render docs or a preview tile).
const button = await compileEmailComponent({
    slug: "button",
    target: "preview",
});

console.log(button.componentHtml); // just the component’s markup

// Or dispatch by kind when the slug is dynamic.
const result = await compileEmailRenderable({
    kind: "template",
    slug: "transactional",
    target: "dotnet",
});

Inputs are validated with Zod, so an unknown slug or an invalid target throws with a descriptive message. getEmailRenderableMeta(kind, slug) returns the catalog entry (name, description, tokens, options) without compiling, and the lower-level compileMjml / transformTokens primitives are exported for callers that already have their own MJML source.

A second export, @stackoverflow/stacks-email/sveltekit, provides ready-made route handlers for serving the pre-compiled static email artifacts (used by @stackoverflow/stacks-docs).

Image assets

Image src values are prefixed with an absolute host so the compiled HTML is sendable. The host is resolved as: assetBaseUrl argument → STACKS_EMAIL_ASSET_BASE_URL environment variable → https://email.stackoverflow.design (default). Pass assetBaseUrl to any compile call (compileEmailTemplate, compileEmailComponent, compileEmailRenderable, or the POST /api/compile body) to override it:

const email = await compileEmailTemplate({
    slug: "transactional",
    target: "braze",
    // → <img src="https://cdn.example.com/email/...">
    assetBaseUrl: "https://cdn.example.com",
});

Pass assetBaseUrl: "" to force root-relative /email/... output. When provided, a non-empty value must be an absolute http(s) URL.