Every content page compiles to an email bundle alongside the HTML — same source, different channel. The bundle is three sibling artifacts you can hand straight to your send infrastructure:

File What
{slug}.email/index.html Inline-styled, table-based, cross-client email HTML
{slug}.email/index.txt Plaintext alternative for the multipart/alternative MIME part
{slug}.email/manifest.json { subject, preheader, from, replyTo, charset, messageRef }

We don't send the emails — that's your SES / SendGrid / Mailgun / Postmark. We produce the bytes; you POST them.

Why we don't ship SMTP ourselves

Deliverability, SPF/DKIM/DMARC, bounce + complaint handling, sender reputation, and CAN-SPAM/GDPR consent — that's multi-month infrastructure for one feature. Your existing email provider has solved it. We hand you ready-to-send bytes; you tell your provider to send them.

(Same rationale we use for form-submission notifications. Compiler outputs, not SMTP.)

What the HTML side does for you

What the plaintext side does for you

Multipart/alternative + a real text/plain part isn't optional for deliverability — every major provider scores you down for HTML-only mail because that's what spammers send. Shipping the .txt makes you a good citizen.

What the manifest does for you

{
  "subject": "Q3 launch announcement",
  "preheader": "Read about the new feature shipping today",
  "from": "StaticOwl <no-reply@staticowl.com>",
  "replyTo": "StaticOwl <no-reply@staticowl.com>",
  "charset": "utf-8",
  "messageRef": "1k3h8a2x"
}

messageRef is a stable id derived from (campaign, canonicalUrl, title) — re-sending the same campaign for the same page produces the same ref, which keeps inboxes that key on it from fragmenting the thread.

Recipes

AWS SES

import { SESv2Client, SendEmailCommand } from '@aws-sdk/client-sesv2';
import fs from 'node:fs/promises';

const html = await fs.readFile('dist/blog/post.email/index.html', 'utf8');
const text = await fs.readFile('dist/blog/post.email/index.txt', 'utf8');
const m    = JSON.parse(await fs.readFile('dist/blog/post.email/manifest.json', 'utf8'));

await new SESv2Client({ region: 'us-east-1' }).send(new SendEmailCommand({
  FromEmailAddress: m.from,
  ReplyToAddresses: [m.replyTo],
  Destination: { ToAddresses: ['someone@example.com'] },
  Content: {
    Simple: {
      Subject: { Data: m.subject, Charset: m.charset },
      Body: {
        Html: { Data: html, Charset: m.charset },
        Text: { Data: text, Charset: m.charset },
      },
    },
  },
}));

SendGrid

import sgMail from '@sendgrid/mail';
sgMail.setApiKey(process.env.SENDGRID_API_KEY);

await sgMail.send({
  to: 'someone@example.com',
  from: m.from, replyTo: m.replyTo,
  subject: m.subject,
  text, html,
  customArgs: { messageRef: m.messageRef },
});

Mailgun

import formData from 'form-data';
import Mailgun from 'mailgun.js';

const mg = new Mailgun(formData).client({ username: 'api', key: process.env.MAILGUN_API_KEY });
await mg.messages.create('mg.yoursite.com', {
  from: m.from, 'h:Reply-To': m.replyTo,
  to: 'someone@example.com',
  subject: m.subject,
  text, html,
  'v:messageRef': m.messageRef,
});

Postmark

import postmark from 'postmark';
const client = new postmark.ServerClient(process.env.POSTMARK_TOKEN);

await client.sendEmail({
  From: m.from, ReplyTo: m.replyTo,
  To: 'someone@example.com',
  Subject: m.subject,
  HtmlBody: html, TextBody: text,
  Headers: [{ Name: 'X-Entity-Ref-ID', Value: m.messageRef }],
});

Generic — anything that takes raw SMTP

Content-Type: multipart/alternative; boundary="boundary"
Subject: <m.subject>
From: <m.from>
Reply-To: <m.replyTo>

--boundary
Content-Type: text/plain; charset=utf-8

<text>

--boundary
Content-Type: text/html; charset=utf-8

<html>

--boundary--

Enabling email output per site

Email is one of the channels in site.config.outputChannels. Add it to enable per-page email artifact emission:

// site config
outputChannels: ['web', 'email', 'social_post', 'qr_code']

web is always required; the others are opt-in. Per-content-type opt-out is on the roadmap (some types like "internal-only documentation" probably don't need an email version).

What's NOT in scope

API reference

Function What
compileEmail(input)EmailBundle The pure compile. Inputs in the type definition.
htmlToPlainText(input)string The HTML → text derivation. Pure.
rewriteLinks(html, base, utm)string URL rewrite + UTM tagging. Pure.
messageRef(campaign, url, title)string Stable thread-ref id. Pure.
renderEmail(input)string The lower-level template wrapper. compileEmail calls it.

Related docs

Resolved at build time via {% similar %} — cosine similarity over embeddings, not tag overlap. Zero arguments.

Template language reference
59% match
StaticOwl documentation — StaticOwl Docs
58% match
Product positioning — StaticOwl Docs
58% match