Email Campaigns
Build modern, on-brand marketing emails and send them via Resend. This skill encodes a complete pattern: asset hosting, reusable HTML components, GIF tooling, and the Resend send pipeline.
1. Asset hosting — use your own site as a CDN
Setup once. In a Next.js or any static-hosted project, create a folder served at the site root:
<project>/public/email-assets/
<campaign-name>/
hero.png
demo.gif
Anything in public/ is served at https://yourdomain.com/email-assets/.... Commit + push = instant CDN. No Cloudinary, no S3.
Rules for email-safe assets:
- Naming: lowercase, dashes only (
hero-image.jpg✅,Hero Image.jpg❌) - Images: JPG/PNG, ≤ 1 MB, max 600px wide
- GIFs: ≤ 1.5 MB, max 720px wide, 12-15 fps, keep under 8 seconds
- Video: MP4 in same folder, but NEVER embed
<video>— always use GIF thumbnail linking to MP4 - Version filenames instead of overwriting (
hero-v2.jpg) — email clients cache aggressively
Before sending: swap all local paths to absolute URLs (https://yourdomain.com/email-assets/...). Relative paths work in browser preview but break in recipients' inboxes.
2. Design system — reusable blocks
Use this building-block inventory. Copy from a previous campaign, don't redesign from scratch.
Outer shell
<body style="margin:0; padding:0; background-color:#ffffff;
font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif;">
<div style="display:none; max-height:0; overflow:hidden; font-size:1px; line-height:1px; color:#fff;">
[PREHEADER — 1 sentence, shown in inbox preview]
</div>
<table width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color:#ffffff;">
<tr><td align="center" style="padding:32px 16px;">
<table width="560" cellpadding="0" cellspacing="0" border="0"
style="max-width:560px; width:100%;">
<!-- MAIN CARD -->
<tr><td>
<table width="100%" cellpadding="0" cellspacing="0" border="0"
style="background-color:#f7fbfe;
background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIj48ZmlsdGVyIGlkPSJuIj48ZmVUdXJidWxlbmNlIHR5cGU9ImZyYWN0YWxOb2lzZSIgYmFzZUZyZXF1ZW5jeT0iMC44NSIgbnVtT2N0YXZlcz0iMiIgc3RpdGNoVGlsZXM9InN0aXRjaCIvPjwvZmlsdGVyPjxyZWN0IHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbHRlcj0idXJsKCNuKSIgb3BhY2l0eT0iMC4wNiIvPjwvc3ZnPg=='),
linear-gradient(90deg, #edf6fd 0%, #f6fbfe 50%, #fcfdfe 100%);
border-radius:4px;
box-shadow: 0 2px 8px rgba(24,24,27,0.06), 0 8px 24px rgba(24,24,27,0.04);
overflow:hidden;">
<!-- sections go here -->
</table>
</td></tr>
</table>
</td></tr>
</table>
</body>
Key tokens:
- Card bg: light sky gradient + 6% SVG noise texture for depth (subtle grain). Gradient direction
90deg= left→right. - Card shadow: layered soft shadow, no border
- Card radius: 4px (tight corners feel modern)
Header image
Full-width image flush with card top. Crop the top 3/4 of a wide image (1376×576 cropped from 1376×768) for hero; use the bottom 1/3 (256px) as a footer image inside the card.
Logo cards (frost-glass)
Five logos distributed across full card width via width="20%" columns. Each card 72×72, light fill (#f4f4f7 gradient), soft shadow, 3px radius.
Hero title + body
<h1 style="font-size:26px; font-weight:700; color:#18181b; letter-spacing:-0.5px;">...</h1>
<p style="font-size:15px; line-height:1.65; color:#3f3f46;">...</p>
Feature list with black bullets (not green checks)
Keep bullets black • in a 20px cell, text 15px. Avoid green check pills — they look dated.
Video block (frost-glass with blurred bg)
Pre-blur your brand hero image with ffmpeg to use as a colorful bed behind videos:
ffmpeg -y -i header.png -vf "boxblur=luma_radius=30:luma_power=3" header_blur.png
Then:
<a href="LANDING-URL">
<p>Video title</p>
<table width="100%">
<tr><td style="background-image:url('https://yourdomain.com/email-assets/header_blur.png');
background-size:cover; border-radius:3px; padding:14px;
box-shadow:0 4px 14px rgba(24,24,27,0.1);">
<table width="100%">
<tr><td style="background-color:rgba(255,255,255,0.55);
backdrop-filter:blur(12px);
-webkit-backdrop-filter:blur(12px);
border:1px solid rgba(255,255,255,0.6);
border-radius:3px; padding:10px;
box-shadow:inset 0 1px 0 rgba(255,255,255,0.5);">
<img src="GIF-URL" width="475" style="display:block; width:100%;
max-width:475px; border-radius:3px;">
</td></tr>
</table>
</td></tr>
</table>
</a>
In Apple Mail/iOS: backdrop-filter renders real glass. In Gmail/Outlook: the pre-blurred image underneath sells the effect on its own.
CTA button
Black pill, 4px radius, 14px 24px padding, optional small logo inline:
<table width="100%"><tr><td align="center"
style="border-radius:4px; background-color:#18181b;">
<a href="LANDING-URL" style="display:block; padding:14px 24px;
font-size:14px; font-weight:600; color:#fff;
text-decoration:none;">
<img src="LOGO" width="20" height="20"
style="display:inline-block; vertical-align:middle;
margin-right:8px; border-radius:3px;" alt="">
<span style="vertical-align:middle;">Connect your accounts →</span>
</a>
</td></tr></table>
Place one CTA immediately after the first proof video (highest conversion moment) and one at the bottom.
Typography scale
- H1: 26px
- Body/features/video titles: 15px
- Section labels/footer: 13px
- CTA button: 14px
Stick to this. No more than 3 sizes.
3. GIF optimization workflow
For each screen recording:
# Convert MP4 → optimized GIF (720px, 15fps, 2x speed, trim to 8s)
ffmpeg -y -i source.mp4 -t 8 \
-vf "setpts=0.5*PTS,fps=15,scale=720:-1:flags=lanczos,split[s0][s1];\
[s0]palettegen=max_colors=256[p];\
[s1][p]paletteuse=dither=sierra2_4a" \
-loop 0 output.gif
# Check file size — should be under 1.5 MB
ls -lh output.gif
If too big:
- Drop to 12fps
- Reduce width to 600px
- Trim to 5-6 seconds
- Drop palette to 128 colors, use
dither=bayer:bayer_scale=5
If too low quality:
- Bump to 720-900px
- 256 colors with
sierra2_4adither - Accept 1.5-2 MB ceiling
4. Sending via Resend
Setup (once per project)
npm install resend
Env var: RESEND_API_KEY=re_... (get from resend.com dashboard)
From domain: verify your sending domain in Resend settings first (DNS records take ~15 min to propagate). Use hello@yourdomain.com, not a free provider.
Send a single test email
// send.mjs
import { Resend } from 'resend';
import fs from 'fs';
const resend = new Resend(process.env.RESEND_API_KEY);
const html = fs.readFileSync('./email.html', 'utf-8');
const { data, error } = await resend.emails.send({
from: 'Ira <hello@get-ryze.ai>',
to: 'you@example.com',
subject: 'Claude Connector for Google/Meta Ads is live',
html,
// Optional plain-text fallback
text: 'We released the Claude connector. Connect in 1 click: https://...',
});
console.log(error ?? data);
Run: node send.mjs. Always send to yourself first.
Send to a list (batches)
Resend rate-limits at 10 req/sec (free) or 100 req/sec (paid). For larger lists use their batch API:
await resend.batch.send(
recipients.map(email => ({
from: 'Ira <hello@get-ryze.ai>',
to: email,
subject: '...',
html,
}))
);
Up to 100 emails per batch call. Chunk your list accordingly.
Sequences / drip campaigns
For a 3-email sequence (day 0, day 3, day 7), use a cron + a sent-state table in Postgres/Supabase:
// Pseudo
const dueToday = await db.campaigns.findMany({
where: { scheduled_date: today, status: 'pending' }
});
for (const c of dueToday) {
await resend.emails.send({ ...c });
await db.campaigns.update({ where: { id: c.id }, data: { status: 'sent' } });
}
Resend has webhooks for email.delivered, email.opened, email.clicked — use them to trigger the next email in the sequence rather than fixed timers.
5. Pre-send checklist
Before you hit send on a real list:
- All asset URLs are absolute (
https://...), not relative (/email-assets/...) - Preheader text set (first hidden div) — this is the inbox preview line
- Subject line tested for deliverability (no ALL CAPS, no exclamation spam)
- Every image has meaningful
alttext - Every link has
target="_blank" - GIFs under 2 MB each, total email under 5 MB
- Tested in at least: Gmail web, Gmail mobile, Apple Mail, Outlook
- Unsubscribe link in footer (required by CAN-SPAM / GDPR)
- Sent one test to yourself — clicked every link
-
fromaddress is on a verified domain in Resend
6. Conversion best practices
- One primary CTA — repeated twice (mid-email + bottom). Don't dilute with secondary actions.
- Video first, text second — a 3-second GIF shows product faster than 3 paragraphs.
- Specificity wins — "Connect in 1 click" > "Get started". "30-second setup" > "Easy to install".
- Claim + proof — every claim near a visual (GIF, screenshot, or logo strip showing integrations).
- Low-friction reply CTA — "Reply 'Guide' and I'll send you X" is high-intent and cheap to fulfill.
- Send Tuesday-Thursday, 9-11am recipient-local. Avoid Monday (full inboxes) and weekends (low open).
7. Template starter
To bootstrap a new campaign, copy the most recent working HTML from your repo (e.g. public/email-assets/preview/claude-connector.html) and edit in place. Keep the preview file checked in alongside the campaign folder:
public/email-assets/2026-05-launch/
hero.png
demo.gif
preview.html
Preview locally at http://localhost:3000/email-assets/2026-05-launch/preview.html.
8. What NOT to do
- ❌ Don't use
<video>tags — blocked in Gmail/Outlook - ❌ Don't use CSS grid or flexbox — broken in Outlook. Use nested
<table>s. - ❌ Don't use web fonts — most clients strip them. Stick to system font stack.
- ❌ Don't use background images on the
<body>if you need them on mobile Gmail — it ignores body bg. Put color inside the main card. - ❌ Don't embed MP4 directly — always GIF preview → link to external MP4.
- ❌ Don't send without testing — render one in Litmus/Mailtrap or just send to yourself on 3 clients.