# Email

> Transactional email for a Next.js 16 site — Resend 6 (the { data, error } return; it does NOT throw on API errors, check error explicitly) with @react-email/components templates that carry the design system's palette and type into the inbox, the React component passed via the react property, a contact-form flow wired through a server action, the react-email dev preview server, and RESEND_API_KEY handling with a lazy client so builds pass without the key. Invoke during the backend phase whenever the brief needs outbound mail — contact-form notifications, double-opt-in newsletter confirmations, magic links, welcome or receipt emails — when a template looks default or off-brand, or when sends fail silently. Trigger phrases — "contact form email", "send an email", "transactional email", "email template", "Resend", "magic link email", "double opt-in confirmation", "newsletter Bestätigungs-Mail", "the form submits but no email arrives".

- Skill: `blyatiful1/email` (Agent Skill, multi-file: 3 files)
- Install (CLI): `npx skillmds@latest add blyatiful1/email`
- Raw SKILL.md: https://api.skillmd.com/api/skills/blyatiful1/email/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Design & Media
- Author: blyatiful1 (https://skillmd.com/u/blyatiful1)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/blyatiful1/email

---


# email — mail the brand, not defaults

**Stage:** Phase 7 — Backend - **Reads:** design/BRIEF.md, design/SYSTEM.md - **Writes:** emails/*.tsx, emails/theme.ts, lib/email.ts, send calls in app/actions/*

## Standard

- The inbox is a brand surface. Every template carries SYSTEM.md's palette, type hierarchy, and voice — a first-grade site sending a default-gray email breaks trust at the exact moment it's being decided.
- Resend 6 returns `{ data, error }` and does NOT throw on API errors. Every send checks `error` explicitly; an unchecked send is silent mail loss.
- The React Email component goes in the `react` property of `emails.send` — never a hand-rolled HTML string.
- Every template is previewed in the react-email dev server before it's wired to a send.
- Email CSS is 2009-grade: style props only, table-safe components from `@react-email/components`, ≤600px container, hex colors (no oklch), system-stack font fallbacks. The design must hold when Gmail strips the web font.
- Transactional only: contact notifications, magic links, receipts, welcomes. No marketing machinery unless BRIEF.md demands it.
- When the brief includes a newsletter, its signup is double opt-in wherever the audience is in the DACH region: single opt-in (subscribing on submit) is unlawful advertising under German UWG §7, so the address joins the list only after a confirmation link is clicked. A form that's legal in the US can be illegal here — check the jurisdiction, not the UX convention.

## Process

1. Read design/BRIEF.md: which flows send mail? Contact form → notification to the team (and optionally a confirmation to the sender). Newsletter → double-opt-in confirmation (the footer's newsletter row wires to this, never a fire-and-subscribe action — see below). Auth → magic-link/verification templates for ultraweb:auth to call. Commerce → receipt. No flows → skip this skill entirely.
2. `npm i resend @react-email/components` and `npm i -D react-email`. Create `emails/` at the project root.
3. Write `emails/theme.ts`: SYSTEM.md tokens translated to email-safe values (oklch → hex, display font + system fallback stack).
4. Build one template per flow. Preview: `npx react-email dev --dir emails --port 3001` (3000 belongs to `next dev`). Iterate until it reads as this site's brand, not React Email's starter.
5. Write the lazy send helper; wire it into the server action per ultraweb:server-actions — failures return as state, never throw at the user.
6. Env: `RESEND_API_KEY` in .env.local and listed in .env.example. Dev sends use `onboarding@resend.dev` (delivers only to the account owner's address until a domain is verified); production requires a verified sending domain — prefer a subdomain (`mail.acme.com`) to isolate reputation.

## The send call

```ts
// lib/email.ts
import { Resend } from 'resend'

let client: Resend | null = null
export function getResend(): Resend {
  if (!client) client = new Resend(process.env.RESEND_API_KEY)  // lazy: builds pass without the key
  return client
}
```

```ts
// inside app/actions/contact.ts, after zod validation (state shape per ultraweb:server-actions)
import { getResend } from '@/lib/email'
import ContactNotification from '@/emails/contact-notification'

const { data, error } = await getResend().emails.send({
  from: 'Acme <contact@mail.acme.com>',
  to: 'team@acme.com',
  replyTo: parsed.data.email,                 // team replies land with the sender
  subject: `${parsed.data.name} — new inquiry`,
  react: ContactNotification(parsed.data),    // component in the react property; called as a fn so the action stays .ts
})
if (error) return { ok: false, errors: { form: ['Message not sent — try again or email us directly.'] } }
```

## Templates

```tsx
// emails/contact-notification.tsx
import { Html, Head, Preview, Body, Container, Text, Hr, Link } from '@react-email/components'
import { t } from './theme'

export default function ContactNotification({ name, email, message }: { name: string; email: string; message: string }) {
  return (
    <Html>
      <Head />
      <Preview>{`${name}: ${message.slice(0, 80)}`}</Preview>
      <Body style={{ margin: 0, backgroundColor: t.bg, fontFamily: t.font, color: t.fg }}>
        <Container style={{ maxWidth: 560, padding: '40px 24px' }}>
          <Text style={{ fontSize: 20, fontWeight: 700, letterSpacing: '-0.02em', margin: '0 0 24px' }}>New inquiry</Text>
          <Text style={{ fontSize: 15, lineHeight: '24px', whiteSpace: 'pre-wrap', margin: 0 }}>{message}</Text>
          <Hr style={{ borderColor: t.border, margin: '32px 0' }} />
          <Text style={{ fontSize: 13, color: t.muted, margin: 0 }}>
            {name} · <Link href={`mailto:${email}`} style={{ color: t.accent }}>{email}</Link>
          </Text>
        </Container>
      </Body>
    </Html>
  )
}
```

```ts
// emails/theme.ts — SYSTEM.md translated for email clients: email CSS can't parse oklch
// and never sees globals.css. Read the hexes from lib/tokens.ts (ultraweb:tokens) — don't re-eyeball them.
export const t = {
  bg: '#faf9f7', fg: '#1c1917', muted: '#78716c', border: '#e7e5e4', accent: '#0d7a68',
  font: "'Söhne', -apple-system, 'Segoe UI', Helvetica, sans-serif",  // display face + system stack
}
```

Rules:

- Hierarchy in miniature: one heading, one body block, one accent use — the restraint taste demands of a page, at 560px.
- `<Preview>` is designed copy (the inbox's second line) — write it in the brief's voice, never let it default to the first template string.
- CTA buttons: `<Button>` from `@react-email/components`, solid accent background, ≥44px tall via padding — never a bare link for the primary action.
- Web fonts go through the `<Font>` component in `<Head>` with an explicit fallback family — Gmail and Outlook render the fallback, so check the preview in the system stack too. Exact props: verify against current docs first.
- Dark mode in email clients is forced and unreliable: keep bg/fg away from pure `#fff`/`#000` so auto-inversion doesn't produce mud.

## Can I Email — the hard floor

Check any technique against caniemail.com before shipping it. These four fail often enough to be standing rules rather than lookups — Outlook's Word rendering engine still carries a large share of DACH business inboxes.

- **Tables, not modern layout.** `display: flex`, `display: grid`, and `position` are ignored outright. Structure with `<Section>`/`<Row>`/`<Column>` from `@react-email/components`; a flex row that "looks fine in Gmail" is a collapsed email elsewhere.
- **Inline style props only.** `<style>` blocks and classes are stripped or ignored per client, so the style attribute on the element is the only reliable delivery. Design one 560–600px column that works unchanged; treat any `@media` rule as an enhancement, never the structure.
- **Resolved hex, never a custom property.** `var(--accent)` resolves to nothing in an inbox. `ultraweb:tokens` exports `lib/tokens.ts` with the palette already flattened to sRGB hex for exactly this case (and for OG images) — `emails/theme.ts` reads those values so a token change propagates instead of drifting into a stale hand-copy.
- **No SVG, no background-image, no web-font dependency.** Outlook drops all three: ship logos as PNG with explicit `width`/`height`, paint color with a table cell's `backgroundColor`, and let the `<Font>` fallback stack carry the type.

## Double opt-in — the newsletter confirmation

German UWG §7 (settled across OLG rulings) treats single opt-in — putting an address on the list the moment a form submits — as unlawful advertising: consent has to be *proven*, not assumed. So a DACH newsletter form never subscribes. Submit writes a **pending** (unconfirmed) record carrying a signed, single-use token that expires (~48h) — schema per ultraweb:database — and sends a Bestätigungs-Mail; the address becomes a real subscriber only when the confirmation link is clicked, which the confirm route verifies and flips per ultraweb:server-actions. The footer's newsletter row wires to this action, not a fire-and-subscribe one.

```ts
// app/actions/newsletter.ts — double opt-in: submit sends a confirmation link, it does NOT subscribe
import { getResend } from '@/lib/email'
import NewsletterConfirm from '@/emails/newsletter-confirm'
import { createPendingSubscriber } from '@/lib/newsletter'   // writes status:'pending' + signed token, per ultraweb:database

// inside the action, after zod validation:
const { token } = await createPendingSubscriber(parsed.data.email)
const confirmUrl = `${process.env.NEXT_PUBLIC_SITE_URL}/newsletter/confirm?token=${token}`  // absolute: an email has no origin
const { data, error } = await getResend().emails.send({
  from: 'Acme <mail@mail.acme.com>',
  to: parsed.data.email,
  subject: 'Bitte bestätige deine Anmeldung',
  react: NewsletterConfirm({ confirmUrl }),
})
if (error) return { ok: false, errors: { form: ['Bestätigung konnte nicht gesendet werden — bitte erneut versuchen.'] } }
// success state: "Check your inbox and click the link" — never "You're subscribed"
```

```tsx
// emails/newsletter-confirm.tsx — the Bestätigungs-Mail; the address joins the list only when this is clicked
import { Html, Head, Preview, Body, Container, Text, Button } from '@react-email/components'
import { t } from './theme'

export default function NewsletterConfirm({ confirmUrl }: { confirmUrl: string }) {
  return (
    <Html>
      <Head />
      <Preview>Ein Klick bestätigt deine Anmeldung — sonst passiert nichts.</Preview>
      <Body style={{ margin: 0, backgroundColor: t.bg, fontFamily: t.font, color: t.fg }}>
        <Container style={{ maxWidth: 560, padding: '40px 24px' }}>
          <Text style={{ fontSize: 20, fontWeight: 700, letterSpacing: '-0.02em', margin: '0 0 16px' }}>Fast geschafft</Text>
          <Text style={{ fontSize: 15, lineHeight: '24px', margin: '0 0 24px' }}>
            Bestätige mit einem Klick, dass du unseren Newsletter abonnieren möchtest. Ohne diese Bestätigung schicken wir dir nichts.
          </Text>
          <Button href={confirmUrl} style={{ backgroundColor: t.accent, color: '#fff', fontSize: 15, fontWeight: 600, padding: '14px 28px', borderRadius: 8, textDecoration: 'none' }}>
            Anmeldung bestätigen
          </Button>
          <Text style={{ fontSize: 13, color: t.muted, margin: '24px 0 0' }}>
            Nicht angefordert? Ignoriere diese Mail — ohne Klick landest du auf keiner Liste.
          </Text>
        </Container>
      </Body>
    </Html>
  )
}
```

## Anti-patterns

- `try {` around `emails.send` with no `error` check — Resend 6 does not throw on API errors; the catch never fires and mail vanishes silently.
- `html:` with a template-literal string when a React template exists — the `react` property renders the component; hand-rolled HTML drifts off-brand.
- `oklch(` anywhere under `emails/` — email clients can't parse it; theme.ts holds the hex translations.
- `className=` in a template without react-email's Tailwind wrapper — dead classes; and that wrapper takes a v3-style config that ignores the site's v4 `@theme` (verify against current docs first) — style props + theme.ts are the reliable path.
- `import '@/app/globals.css'` or `next/font` inside `emails/` — site CSS never reaches an inbox.
- `NEXT_PUBLIC_RESEND` — the key is server-only; a public prefix ships it in the client bundle.
- `new Resend(` at module scope of anything a page imports — build fails on machines without the key; lazy-init in the helper.
- `from: 'onboarding@resend.dev'` reaching production — dev-only sender; swap to the verified domain before ship.
- Adding the address to the list on submit (single opt-in) for a DACH audience — unlawful advertising under German UWG §7. On submit you write a *pending* record and send the Bestätigungs-Mail; the subscriber is real only after the confirmation link is clicked.
- An unsigned or reusable confirmation token — a guessable or shared link lets a scraper confirm addresses that aren't theirs. Sign it, scope it to the one address, and let the pending record expire (~48h) so it can't be confirmed weeks later.
- A newsletter success state that says "Subscribed!" when only the pending record exists — it isn't true yet; say "Check your inbox and click the link to confirm."
- ✨/🚀 in subject lines — taste bans emoji in production copy, and subject lines are production copy.

## Worked example — Kaffeewerk Ost, order confirmation after Stripe checkout

Moved to `references/example.md` — read only when this build's case is genuinely ambiguous; the sections above are the decision material.

## Composes with

Moved to `references/composes.md` — the handoff map; load it when orchestrating this skill against its neighbors.

