Postboi deliberately doesn’t ship a templating language. body is just HTML — a string, or
a promise resolving to one — so any tool that renders HTML works, and swapping
templating stacks never touches your sending code.
For anything fancier than FormData tables, our favourite pairing is Maizzle — enough so that Postboi ships an optional postboi/maizzle helper for it. Think of it as Postboi’s answer to Resend + react-email — the same “design in components, send the result” workflow, except it works
across all supported providers, not just one.
We’re not affiliated with Maizzle in any way. It’s just neat.
Why Maizzle
- Tailwind CSS for email — write utilities, get inlined, email-safe CSS out.
- Email-grade output — table layouts, Outlook fallbacks, and battle-tested
transformers, so you’re not hand-rolling
<td>soup. - Components included —
Button,Container,Heading, and friends, plus your own. - A real dev loop —
maizzle servegives you live-reloading previews in the browser.
Since Maizzle 6, templates are authored as Vue single-file components. That’s just the authoring format — rendering happens on the server and the output is plain HTML, so it slots into any app, SvelteKit included. No Vue ships anywhere near your frontend.
Set up
Add Maizzle to your app:
Or scaffold a standalone project with npx maizzle new — handy if emails live in their
own repo, or you want the Maizzle CLI workflow
with live previews.
Write a template
Templates live wherever you like — an emails/ directory keeps things tidy. Declare the
dynamic bits with defineProps:
Send it
Postboi ships a helper for exactly this: postboi/maizzle wraps Maizzle’s render() and resolves to the transformed,
inlined HTML. Because body accepts a promise, it drops straight in — Resend-style
ergonomics, no intermediate await:
@maizzle/framework is an optional peer dependency — the helper needs it installed
(see Set up), but nothing else in Postboi does.
The signature is maizzle(template, props?, config?):
template— a path to a template file, or a raw SFC source string.props— passed to the template’s root component, mapping 1:1 to itsdefineProps. Type them with a generic:maizzle<WelcomeProps>('./emails/welcome.vue', { name: 'Ava' }).config— Maizzle config overrides forwarded torender(), e.g.{ minify: true }.
Prefer to call Maizzle yourself? The helper is a thin convenience — render() works just
as well:
Pair it with auto_text in your global config to derive a plain-text alternative from the rendered HTML
automatically.
Edge runtimes
Maizzle’s render() — and therefore postboi/maizzle — spins up a lightweight
Vite-based renderer under the hood, so it needs Node or Bun — fine in SvelteKit, Next.js, or Express server routes, but not on edge runtimes like Cloudflare Workers.
There, pre-build instead: put literal tokens like %name% in your template copy, compile
to static HTML at deploy time with maizzle build (or the build() API), then import the HTML as a string
and .replace() the tokens at send time.
Prefer React Email or MJML?
Same pattern, different renderer — anything that produces an HTML string (or a promise of
one) drops straight into body. React Email’s render() resolves to an HTML string, so you
can pass it through unawaited too; with MJML it’s mjml2html(template).html.