Skip to content

Commit ebbb084

Browse files
authored
feat: unified gradient-header email template design (#53)
* feat(chat): ship-readiness polish — Tailwind, auto-scroll, markdown, a11y (#27) * docs(chat): add ship-readiness polish implementation plan Addresses all 19 audit issues: theme consolidation, Tailwind conversion, auto-scroll, textarea auto-expand, markdown rendering, empty state, responsive sidebar, SVG icons, ARIA, and API cleanup. * feat(chat): consolidate theme into shared TS module, add icons + markdown utils * feat(chat): convert ChatComponent to Tailwind, add auto-scroll + empty state + responsive sidebar - Replace 80+ lines of inlined CSS vars with CHAT_THEME_STYLES import - Add CHAT_MARKDOWN_STYLES + renderMarkdown for AI message rendering - Convert all inline style="" attributes to Tailwind utility classes - Add auto-scroll via viewChild + effect tracking message count - Add empty state when no messages and not loading - Make thread sidebar responsive with hidden md:flex + mobile toggle - Add ARIA attributes: role=log, aria-live=polite, role=navigation - Use ViewEncapsulation.None for markdown styles * feat(chat): convert primitives to Tailwind, add textarea auto-expand + focused signal * feat(chat): convert ChatDebug + sub-components to theme-aware Tailwind * feat(chat): convert remaining compositions to Tailwind with SVG icons + theme vars * feat(chat): clean up public API, add marked peer dep, verify build Remove legacy cp-chat/cp-chat-input/cp-chat-message components and migrate all cockpit examples to the new ChatComponent composition. Export shared styles, icons, and markdown utilities from public API. Update ChatConfig with renderRegistry, avatarLabel, assistantName. Add marked as optional peer dep and fix dynamic import for library build. Add @source directive to cockpit styles for Tailwind scanning. --------- * Rebrand to Angular Stream Resource (#28) * docs: add rebrand design spec for Angular Stream Resource * docs: add implementation plan for Angular Stream Resource rebrand * docs: rebrand StreamResource → Angular Stream Resource in README and COMMERCIAL Update product name in prose/marketing contexts: README alt text and license section, COMMERCIAL.md license header. * docs: rebrand StreamResource → Angular Stream Resource in website shared components Update product name in layout metadata title, Nav logo link text, Footer brand name, and Footer copyright line. * docs: rebrand StreamResource → Angular Stream Resource in landing and llms.txt Update product name in ProblemSection badge text, GenerativeUIFrame aria-label, and llms.txt route title and description line. * docs: rebrand StreamResource → Angular Stream Resource in documentation files Update product name in headings and prose across limitations.md and four design specs (titles, overview sentences, section headings). * docs: rebrand stream-resource → Angular Stream Resource in LLM context files Update product name in the H1 title of AGENTS.md and CLAUDE.md. * docs: rebrand remaining spec and plan references to Angular Stream Resource * docs: fix remaining brand references missed in initial pass * feat(website): add narrative sections, pilot-to-prod page, and rebrand integration (#29) * fix(website): add track shake animation to ProblemSection stall phase * fix(website): ProblemSection quality fixes — timer cleanup, unique SVG ID, aria-hidden, correct import - Store setTimeout IDs and clear them on unmount (prevents state updates on unmounted component) - Use useId() to generate unique hatchId per instance (prevents SVG pattern id collision) - Add role=progressbar + aria-valuenow to track container for screen readers - Add aria-hidden=true to decorative animated elements (pins, labels, badge, counter) - Fix import: use local lib/design-tokens instead of unresolved @cacheplane/design-tokens - Add invariant comment for done-timeout vs counter-duration coupling * feat: add FullStackSection with animated stack diagram and roadmap strip * feat: add ChatFeaturesSection with 4 interactive chat scenarios * feat: add FairComparisonSection comparison table * feat: wire ProblemSection, FullStackSection, ChatFeaturesSection, FairComparisonSection into landing page - Insert ProblemSection + FullStackSection + ChatFeaturesSection after StatsStrip - Insert FairComparisonSection after DeepAgentsShowcase - Add two ambient gradient blobs for extended page height - Task 5 (FeatureStrip copy): no-op — the problematic 'no established pattern' copy was not present in this branch * chore: add puppeteer devDependency and generate-whitepaper script * feat: add whitepaper signup API route with NDJSON persistence * feat: add whitepaper generation script * feat: add WhitePaperSection with free download and optional lead-gen form * feat: add WhitePaperSection to landing page; remove useStream parity copy from FeatureStrip * fix(whitepaper): add JetBrains Mono to Google Fonts URL and regenerate preview Fixes missing code font in whitepaper output. Regenerates whitepaper-preview.html with correct 'EB Garamond' and 'JetBrains Mono' font references throughout. * feat: add PilotHero component and /pilot-to-prod page skeleton * fix: PilotHero responsive padding, eyebrow style conflict, page metadata * feat: add WhatIsIncluded 3-column component for pilot-to-prod page * feat: add HowItWorks 3-phase timeline for pilot-to-prod page * feat: add PricingSignal pricing callout for pilot-to-prod page * feat: add WhitePaperGate 5-field lead gen form for pilot-to-prod page * fix: change role=alert to role=status to match aria-live=polite in WhitePaperGate * feat: add PilotFooterCTA and wire complete pilot-to-prod page * fix: use tokens.colors.accent in PilotFooterCTA, add aria-hidden to page blobs * feat: add Pilot to Prod nav link and restructure homepage (remove FeatureStrip/CockpitCTA/CodeBlock, add PilotProgram CTA) * fix: correct design-tokens import path in pilot-to-prod page (3 levels up) * fix: apply full review findings — messaging, mobile, UX, and RiskRemoval section - Remove useStream() parity messaging from HeroTwoCol, WhatIsIncluded, StatsStrip - Fix PricingSignal: remove ambiguous '/year', clarify as fixed fee + pilot included - Add PricingSignal mobile padding reduction via media query - Fix ProblemSection stat grid to collapse on mobile (auto-fit minmax) - Add RiskRemoval section to pilot-to-prod page (between PricingSignal and WhitePaperGate) - Fix Nav Examples link: external=true, target=_blank, rel=noopener noreferrer - Fix WhitePaperGate: role field sent in message body, not merged into company string - Fix PilotFooterCTA: replace broken whileHover borderColor with CSS class hover - Fix PilotHero: remove opacity from initial animations (prevents blank hero flash) - Increase PilotHero CTA padding to meet 44px touch target * fix: remove remaining useStream parity messaging from layout, Footer, and ValueProps * fix: second review pass — docs messaging, title, broken link, a11y labels - introduction.mdx: remove parity/useStream opening line, use Signal-native positioning - AGENTS.md.template + CLAUDE.md.template: update tagline to Signal-native - layout.tsx: update <title> from LangChain to LangGraph - Footer.tsx: fix /api-reference → /docs/api/stream-resource (was 404) - PilotHero.tsx: add aria-hidden to decorative gradient blobs - WhitePaperSection.tsx: add sr-only labels + aria-label to name/email inputs - LeadForm.tsx (pricing): add sr-only labels + aria-label to all four form inputs * feat: add whitepaper.pdf to public directory Generated from whitepaper-preview.html via Puppeteer. All 6 chapters present (Streaming State Management, Thread Persistence, Tool-Call Rendering, Human Approval Flows, Generative UI, Deterministic Testing). Fixes dead 'Download the Guide' CTAs on pilot-to-prod and homepage. * feat: citation badges on stats, pricing reframe to app deployment license Citation badges: - New CitationBadge component — click-to-open popover with source, stat, note, and link - 66% stat → Stack Overflow Developer Survey 2025 - 31% stat → ISG AI Adoption Reports - 75% stat → Stack Overflow Developer Survey 2025 - Keyboard (Escape) and outside-click dismissal, ARIA dialog role Pricing reframe (app deployment license): - Remove ALL refund/money-back/guarantee language site-wide - PilotHero: trust line → "App deployment license · $20,000 · 3-month co-pilot engagement" - PilotHero: subheadline removes "guaranteed outcome" - WhatIsIncluded: card 3 renamed from "Production Guarantee" → "App Deployment License" - HowItWorks: phase 3 removes "full refund" language, deliverable → "Production deployment" - PricingSignal: subtitle + features list updated to license/co-pilot framing - RiskRemoval: section reframed from guarantee → "What's included in the license" Replaces money-back card with "We work alongside your team" card - PilotFooterCTA: fine print updated - pilot-to-prod/page.tsx: meta description updated * feat: subtler citation badge + citations on all 77% claims CitationBadge: - Reduced to 13px, transparent background, faint border (rgba 0.2) - Text color rgba(0,64,144,0.35) at rest — nearly invisible until hovered - No fill on idle state, border-only approach New citation placements: - PilotHero subheadline: 77% → McKinsey State of AI 2024 - PilotFooterCTA body copy: 77% → McKinsey State of AI 2024 - HomePilotCTA (new component): extracts inline pilot CTA from page.tsx so it can be a client component with CitationBadge on the 77% claim - page.tsx: replaces inline section with <HomePilotCTA /> * docs: add FullStackSection redesign spec (EM/CTO layer narrative + Gen UI bug fix) * feat(website): redesign FullStackSection for EM/CTO audience * docs: apply Angular Stream Resource rebrand to narrative components * chore: sync package-lock.json after merge * fix(website): update e2e test for new landing page structure --------- * fix(website): replace unsourced stats with verified Gartner and Stack Overflow citations - Replace fabricated 77% McKinsey stat with Gartner "50% of GenAI projects abandoned after POC" (Jan 2026) across ProblemSection, PilotHero, HomePilotCTA, and PilotFooterCTA - Replace stale ISG 31% stat with same Gartner citation in ProblemSection - Update progress bar animation to stall at 50% instead of 77% - Update FairComparisonSection to reference @langchain/langgraph-sdk explicitly in column header and subtitle - All three stat cards now cite verified sources: Stack Overflow 2025 Developer Survey (66%, 75%) and Gartner GenAI Project Failure 2026 (50%) * docs: add website audit and lead generation specs - Spec 1: Mobile responsive fixes (ChatFeaturesSection overflow, FairComparisonSection mobile layout, touch targets, min font sizes) plus design improvements (social proof, footer newsletter, white paper soft gate, OG meta tags) - Spec 2: Resend integration for lead gen (email notifications on lead forms, white paper PDF delivery via email, newsletter signup with Resend audience, React Email templates) * docs: add implementation plans for lead gen and website audit - Plan 1: Lead generation with Resend (9 tasks) — install deps, shared module, 3 email templates, 3 API route upgrades, e2e verify - Plan 2: Website audit mobile + design (9 tasks) — ChatFeatures responsive, FairComparison mobile cards, touch targets, min font sizes, social proof, footer newsletter, white paper soft gate, OG tags * chore: install resend and react-email dependencies * feat: add shared resend module with audience helper * feat: add lead notification email template * feat: add whitepaper download email template * feat: add newsletter welcome email template * feat: wire /api/leads to Resend email + audience * feat: wire /api/whitepaper-signup to Resend email delivery * feat: add /api/newsletter route with Resend welcome email * fix: convert email templates to plain HTML to avoid React dual-instance crash Replace @react-email/components JSX templates with plain TypeScript string functions and switch Resend calls from react: to html:, eliminating the duplicate React instance that caused useInsertionEffect to crash in API routes. * fix: lazy-init Resend client to gracefully handle missing API key - Resend client is now created lazily on first use, not at module load - When RESEND_API_KEY is missing, all email operations no-op with a log - Forms still return { ok: true } and write NDJSON backup without keys - Extracted sendEmail() helper that encapsulates the null-client check * fix: make ChatFeaturesSection responsive on mobile * fix: stack FairComparisonSection rows vertically on mobile * fix: increase touch targets to meet WCAG 44px minimum * fix: enforce 12px minimum font size on progress bar labels * feat: add social proof badge strip below stats * feat: add newsletter signup form to footer * feat: restructure white paper section with soft gate * feat: add OpenGraph and Twitter Card meta tags * feat: rebrand from streamResource to agent() — @cacheplane/angular - Rename streamResource() → agent(), StreamResource → Agent - Rename @cacheplane/stream-resource → @cacheplane/angular - Rebrand 'Angular Stream Resource' → 'Angular Agent Framework' - Rename libs/stream-resource → libs/angular, e2e/stream-resource-e2e → e2e/angular-e2e - Update all imports, types, providers, docs, and configs - Preserve stream-resource.dev domain and GitHub repo URLs - Preserve STREAM_RESOURCE_CONFIG injection token name * fix: complete rebrand audit — nav bug, hero copy, templates, footer - Fix hamburger menu showing on desktop (inline display:flex was overriding Tailwind md:hidden — moved to className) - Fix hero headline: "Enterprise Streaming Resource" → "Enterprise Agent Framework" - Fix footer tagline and layout.tsx meta description - Fix hero.svg text - Update CLAUDE.md.template and AGENTS.md.template with new API names (agent(), provideAgent(), @cacheplane/angular) * fix: remove $20k from pilot program, include with app license, annual term * fix: resolve API docs slug mismatch after rename - docs-config.ts slugs now match MDX filenames (agent.mdx, provide-agent.mdx) - API_NAME_MAP updated with correct slug→name mappings - Nav API link updated to /docs/api/agent * refactor: rename stream-resource lib to agent in Nx project structure - Rename libs/angular/ → libs/agent/ - Rename e2e/angular-e2e/ → e2e/agent-e2e/ - Update Nx project names, configs, tsconfig paths, CI workflows - Package name (@cacheplane/angular) and source code unchanged * feat: add airplane emoji favicon and logo to header/footer * fix: prevent FullStackSection connector animations from overlapping cards * fix: add remark-gfm to enable markdown table rendering in docs * fix: update developer seat feature text to "12-month license" * feat: add dismissible whitepaper announcement toast - Shows after 30s on site, bottom-right with glassmorphism - Dismissible via X, 'Not now', or downloading the PDF - Persisted in localStorage keyed by announcement date - Bump ANNOUNCEMENT_DATE constant to re-show for all users * feat: add lead capture form to announcement toast - Clicking "Get the Guide" now shows an email form instead of downloading directly - Form submits to /api/whitepaper-signup (Resend integration) - Success state shows confirmation, auto-dismisses after 4s - "or download directly" skip link below the form for users who don't want to provide email * feat: replace social proof badges with animated logo scroll strip - 12 LangChain ecosystem company names in an infinite scrolling strip - Glassmorphism container with fade edges - Pauses on hover, 30s full scroll cycle - Eyebrow: "Built for teams shipping with LangChain" * feat: add Loops.so integration for drip email campaigns - Shared lib/loops.ts with upsertContact and sendEvent helpers - Graceful degradation when LOOPS_API_KEY is missing - Three events: lead_submitted, whitepaper_downloaded, newsletter_subscribed - Contacts created with source tracking (lead-form, whitepaper, newsletter) - Wired into all 3 API routes alongside existing Resend calls * feat: replace Loops with self-hosted drip via Resend scheduled_at - Remove Loops.so integration (lib/loops.ts deleted, all route imports removed) - Add 4-email whitepaper drip sequence (Day 2, 5, 10, 20) - Drip emails scheduled at signup via Resend's scheduledAt parameter - Add /api/unsubscribe route with NDJSON persistence - Unsubscribe link in every drip email footer - sendEmail() now supports optional scheduledAt parameter * feat: unified gradient-header email template design - New shared email-wrapper.ts with gradient header, logo, footer - All 7 templates (4 drip + 3 transactional) updated to use wrapper - Consistent design: gradient band, white body, accent CTA buttons - Lead notification uses red 'New Lead' eyebrow - Drip emails include unsubscribe link, transactional emails don't - Day 10 drip has styled timeline card for pilot program phases ---------
1 parent f50c683 commit ebbb084

6 files changed

Lines changed: 188 additions & 72 deletions

File tree

apps/website/emails/drip-whitepaper-followup.ts

Lines changed: 46 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,66 @@
1-
export function dripWhitepaperFollowupHtml(day: number): { subject: string; html: string } {
2-
const unsubUrl = 'https://stream-resource.dev/api/unsubscribe';
1+
import { wrapEmail } from './email-wrapper';
32

3+
export function dripWhitepaperFollowupHtml(day: number): { subject: string; html: string } {
44
if (day === 2) {
55
return {
66
subject: 'Did you get a chance to read Chapter 3?',
7-
html: wrapEmail(`
8-
<p style="font-size:14px;color:#3f3f46;line-height:1.6;margin:0 0 16px">Chapter 3 covers <strong>tool-call rendering</strong> — how to surface agent actions as real UI instead of raw JSON. It's the chapter most teams bookmark first.</p>
9-
<p style="font-size:14px;color:#3f3f46;line-height:1.6;margin:0 0 24px">If you haven't downloaded it yet, <a href="https://stream-resource.dev/whitepaper.pdf" style="color:#004090;text-decoration:underline">grab it here</a>.</p>
10-
`),
7+
html: wrapEmail({
8+
body: `
9+
<p style="font-size:11px;font-family:monospace;text-transform:uppercase;letter-spacing:0.08em;color:#004090;font-weight:700;margin:0 0 8px">Whitepaper Follow-up</p>
10+
<p style="font-size:20px;font-weight:700;color:#1a1a2e;margin:0 0 14px;line-height:1.3">Did you get a chance to read Chapter 3?</p>
11+
<p style="font-size:14px;color:#555770;line-height:1.7;margin:0 0 24px">Chapter 3 covers <strong>tool-call rendering</strong> — how to surface agent actions as real UI instead of raw JSON. It's the chapter most teams bookmark first.</p>
12+
<a href="https://stream-resource.dev/whitepaper.pdf" style="display:inline-block;background-color:#004090;color:#fff;padding:12px 28px;border-radius:8px;font-size:14px;font-weight:700;text-decoration:none">Read the Guide →</a>
13+
`,
14+
showUnsubscribe: true,
15+
}),
1116
};
1217
}
18+
1319
if (day === 5) {
1420
return {
1521
subject: 'The gap between demo and production',
16-
html: wrapEmail(`
17-
<p style="font-size:14px;color:#3f3f46;line-height:1.6;margin:0 0 16px">Half of GenAI projects die after proof of concept. The gap isn't the model — it's the frontend production path: streaming state, thread persistence, human approval flows, and deterministic testing.</p>
18-
<p style="font-size:14px;color:#3f3f46;line-height:1.6;margin:0 0 24px">That's exactly what the Angular Agent Framework solves. <a href="https://stream-resource.dev/docs" style="color:#004090;text-decoration:underline">See how it works →</a></p>
19-
`),
22+
html: wrapEmail({
23+
body: `
24+
<p style="font-size:11px;font-family:monospace;text-transform:uppercase;letter-spacing:0.08em;color:#004090;font-weight:700;margin:0 0 8px">Production Readiness</p>
25+
<p style="font-size:20px;font-weight:700;color:#1a1a2e;margin:0 0 14px;line-height:1.3">The gap between demo and production</p>
26+
<p style="font-size:14px;color:#555770;line-height:1.7;margin:0 0 24px">Half of GenAI projects die after proof of concept. The gap isn't the model — it's the frontend production path: streaming state, thread persistence, human approval flows, and deterministic testing.</p>
27+
<a href="https://stream-resource.dev/docs" style="display:inline-block;background-color:#004090;color:#fff;padding:12px 28px;border-radius:8px;font-size:14px;font-weight:700;text-decoration:none">See How It Works →</a>
28+
`,
29+
showUnsubscribe: true,
30+
}),
2031
};
2132
}
33+
2234
if (day === 10) {
2335
return {
2436
subject: 'The pilot program is included with every app license',
25-
html: wrapEmail(`
26-
<p style="font-size:14px;color:#3f3f46;line-height:1.6;margin:0 0 16px">Every app deployment license includes a <strong>3-month co-pilot engagement</strong> — we work alongside your Angular team to ship your first agent to production.</p>
27-
<p style="font-size:14px;color:#3f3f46;line-height:1.6;margin:0 0 8px">Week 1: Integration & first stream<br/>Month 1: First agent in staging<br/>Month 3: Production deployment</p>
28-
<p style="font-size:14px;color:#3f3f46;line-height:1.6;margin:16px 0 24px"><a href="https://stream-resource.dev/pilot-to-prod" style="color:#004090;text-decoration:underline">Learn about the pilot program →</a></p>
29-
`),
37+
html: wrapEmail({
38+
body: `
39+
<p style="font-size:11px;font-family:monospace;text-transform:uppercase;letter-spacing:0.08em;color:#004090;font-weight:700;margin:0 0 8px">Pilot Program</p>
40+
<p style="font-size:20px;font-weight:700;color:#1a1a2e;margin:0 0 14px;line-height:1.3">The pilot program is included with every app license</p>
41+
<p style="font-size:14px;color:#555770;line-height:1.7;margin:0 0 14px">Every app deployment license includes a <strong>3-month co-pilot engagement</strong> — we work alongside your Angular team to ship your first agent to production.</p>
42+
<div style="background:#f8f9fc;border-radius:8px;padding:16px 18px;margin:0 0 24px;border:1px solid rgba(0,64,144,0.08)">
43+
<p style="font-size:13px;color:#555770;margin:0 0 4px;line-height:1.6"><strong style="color:#004090">Week 1</strong> · Integration &amp; first stream</p>
44+
<p style="font-size:13px;color:#555770;margin:0 0 4px;line-height:1.6"><strong style="color:#004090">Month 1</strong> · First agent in staging</p>
45+
<p style="font-size:13px;color:#555770;margin:0;line-height:1.6"><strong style="color:#004090">Month 3</strong> · Production deployment</p>
46+
</div>
47+
<a href="https://stream-resource.dev/pilot-to-prod" style="display:inline-block;background-color:#004090;color:#fff;padding:12px 28px;border-radius:8px;font-size:14px;font-weight:700;text-decoration:none">Learn About the Pilot →</a>
48+
`,
49+
showUnsubscribe: true,
50+
}),
3051
};
3152
}
53+
3254
// day === 20
3355
return {
34-
subject: 'Ready to ship your agent? Let\'s talk.',
35-
html: wrapEmail(`
36-
<p style="font-size:14px;color:#3f3f46;line-height:1.6;margin:0 0 16px">If your team is evaluating how to take an Angular + LangGraph agent to production, I'd love to hear what you're building.</p>
37-
<p style="font-size:14px;color:#3f3f46;line-height:1.6;margin:0 0 24px">Reply to this email or <a href="mailto:hello@cacheplane.io" style="color:#004090;text-decoration:underline">schedule a conversation</a> — no pitch, just a technical discussion about your use case.</p>
38-
`),
56+
subject: "Ready to ship your agent? Let's talk.",
57+
html: wrapEmail({
58+
body: `
59+
<p style="font-size:11px;font-family:monospace;text-transform:uppercase;letter-spacing:0.08em;color:#004090;font-weight:700;margin:0 0 8px">Let's Connect</p>
60+
<p style="font-size:20px;font-weight:700;color:#1a1a2e;margin:0 0 14px;line-height:1.3">Ready to ship your agent? Let's talk.</p>
61+
<p style="font-size:14px;color:#555770;line-height:1.7;margin:0 0 24px">If your team is evaluating how to take an Angular + LangGraph agent to production, I'd love to hear what you're building. Reply to this email or <a href="mailto:hello@cacheplane.io" style="color:#004090;text-decoration:underline">schedule a conversation</a> — no pitch, just a technical discussion about your use case.</p>
62+
`,
63+
showUnsubscribe: true,
64+
}),
3965
};
4066
}
41-
42-
function wrapEmail(body: string): string {
43-
return `<!DOCTYPE html><html><head></head>
44-
<body style="font-family:Inter,Arial,sans-serif;background-color:#f4f4f5;padding:40px 0;margin:0">
45-
<div style="max-width:520px;margin:0 auto;background-color:#fff;border-radius:12px;padding:32px 40px;border:1px solid #e4e4e7">
46-
<p style="font-size:11px;font-family:monospace;text-transform:uppercase;letter-spacing:0.08em;color:#004090;font-weight:700;margin:0 0 16px">Angular Agent Framework</p>
47-
${body}
48-
<hr style="border:none;border-top:1px solid #e4e4e7;margin:24px 0 16px"/>
49-
<p style="font-size:11px;color:#a1a1aa;line-height:1.5;margin:0">Angular Agent Framework — Signal-native streaming for LangGraph.</p>
50-
<p style="font-size:10px;color:#d4d4d8;margin:8px 0 0"><a href="https://stream-resource.dev/api/unsubscribe?email=RECIPIENT" style="color:#d4d4d8;text-decoration:underline">Unsubscribe</a></p>
51-
</div></body></html>`;
52-
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/**
2+
* Shared HTML wrapper for all email templates.
3+
* Gradient header band with logo, white body, footer.
4+
*/
5+
export function wrapEmail(opts: {
6+
body: string;
7+
showUnsubscribe?: boolean;
8+
}): string {
9+
return `<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"></head>
10+
<body style="font-family:Inter,Arial,sans-serif;background-color:#e8eaf0;padding:40px 0;margin:0">
11+
<div style="max-width:520px;margin:0 auto;border-radius:12px;overflow:hidden;box-shadow:0 2px 12px rgba(0,0,0,0.08)">
12+
<div style="background:linear-gradient(135deg, #fef0f3 0%, #f4f0ff 45%, #eaf3ff 70%, #e6f4ff 100%);padding:28px 32px 20px;border-bottom:1px solid rgba(0,64,144,0.1)">
13+
<div style="font-size:13px;font-weight:700;color:#1a1a2e;letter-spacing:-0.01em">🛩️ Angular Agent Framework</div>
14+
</div>
15+
<div style="background:#fff;padding:28px 32px 32px">
16+
${opts.body}
17+
<div style="border-top:1px solid #e4e4e7;margin-top:28px;padding-top:16px">
18+
<p style="font-size:11px;color:#a1a1aa;line-height:1.5;margin:0">Angular Agent Framework — Signal-native streaming for LangGraph.</p>
19+
${opts.showUnsubscribe ? '<p style="font-size:10px;color:#d4d4d8;margin:6px 0 0"><a href="https://stream-resource.dev/api/unsubscribe?email=RECIPIENT" style="color:#d4d4d8;text-decoration:underline">Unsubscribe</a></p>' : ''}
20+
</div>
21+
</div>
22+
</div>
23+
</body></html>`;
24+
}
25+
26+
export function esc(s: string): string {
27+
return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
28+
}
Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { wrapEmail, esc } from './email-wrapper';
2+
13
interface LeadNotificationProps {
24
name: string;
35
email: string;
@@ -7,18 +9,15 @@ interface LeadNotificationProps {
79
}
810

911
export function leadNotificationHtml({ name, email, company, message, ts }: LeadNotificationProps): string {
10-
return `<!DOCTYPE html><html><head></head>
11-
<body style="font-family:Inter,Arial,sans-serif;background-color:#f4f4f5;padding:40px 0;margin:0">
12-
<div style="max-width:520px;margin:0 auto;background-color:#fff;border-radius:12px;padding:32px 40px;border:1px solid #e4e4e7">
13-
<p style="font-size:11px;font-family:monospace;text-transform:uppercase;letter-spacing:0.08em;color:#004090;font-weight:700;margin:0 0 8px">New Lead</p>
14-
<p style="font-size:20px;font-weight:700;color:#1a1a2e;margin:8px 0 4px">${esc(name)}</p>
15-
<p style="font-size:14px;color:#71717a;margin:0 0 16px">${esc(email)}${company ? ` — ${esc(company)}` : ''}</p>
16-
${message ? `<hr style="border:none;border-top:1px solid #e4e4e7;margin:16px 0"/><p style="font-size:14px;color:#3f3f46;line-height:1.6;margin:0">${esc(message)}</p>` : ''}
17-
<hr style="border:none;border-top:1px solid #e4e4e7;margin:16px 0"/>
18-
<p style="font-size:11px;color:#a1a1aa;margin:0">Received ${esc(ts)}</p>
19-
</div></body></html>`;
20-
}
21-
22-
function esc(s: string): string {
23-
return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
12+
return wrapEmail({
13+
body: `
14+
<p style="font-size:11px;font-family:monospace;text-transform:uppercase;letter-spacing:0.08em;color:#DD0031;font-weight:700;margin:0 0 8px">New Lead</p>
15+
<p style="font-size:20px;font-weight:700;color:#1a1a2e;margin:0 0 4px">${esc(name)}</p>
16+
<p style="font-size:14px;color:#8b8fa3;margin:0 0 16px">${esc(email)}${company ? ` — ${esc(company)}` : ''}</p>
17+
${message ? `<div style="border-top:1px solid #e4e4e7;padding-top:14px;margin-bottom:4px"><p style="font-size:14px;color:#555770;line-height:1.7;margin:0">${esc(message)}</p></div>` : ''}
18+
<div style="border-top:1px solid #e4e4e7;padding-top:14px;margin-top:14px">
19+
<p style="font-size:11px;color:#a1a1aa;margin:0">Received ${esc(ts)}</p>
20+
</div>
21+
`,
22+
});
2423
}
Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
1+
import { wrapEmail } from './email-wrapper';
2+
13
export function newsletterWelcomeHtml(): string {
2-
return `<!DOCTYPE html><html><head></head>
3-
<body style="font-family:Inter,Arial,sans-serif;background-color:#f4f4f5;padding:40px 0;margin:0">
4-
<div style="max-width:520px;margin:0 auto;background-color:#fff;border-radius:12px;padding:32px 40px;border:1px solid #e4e4e7">
5-
<p style="font-size:11px;font-family:monospace;text-transform:uppercase;letter-spacing:0.08em;color:#004090;font-weight:700;margin:0 0 12px">Angular Agent Framework</p>
6-
<p style="font-size:22px;font-weight:700;color:#1a1a2e;margin:0 0 8px">Welcome to Angular Agent Framework updates</p>
7-
<p style="font-size:14px;color:#3f3f46;line-height:1.6;margin:0 0 24px">You'll receive updates on new capabilities, production patterns, and Angular agent best practices. We keep it focused and infrequent — no spam.</p>
8-
<a href="https://stream-resource.dev/docs" style="background-color:#004090;color:#fff;padding:12px 28px;border-radius:10px;font-size:14px;font-weight:700;text-decoration:none;display:inline-block">Explore the Docs</a>
9-
<hr style="border:none;border-top:1px solid #e4e4e7;margin:24px 0 16px"/>
10-
<p style="font-size:12px;color:#a1a1aa;line-height:1.5;margin:0">Angular Agent Framework — Signal-native streaming for LangGraph.</p>
11-
</div></body></html>`;
4+
return wrapEmail({
5+
body: `
6+
<p style="font-size:20px;font-weight:700;color:#1a1a2e;margin:0 0 8px;line-height:1.3">Welcome to Angular Agent Framework updates</p>
7+
<p style="font-size:14px;color:#555770;line-height:1.7;margin:0 0 24px">You'll receive updates on new capabilities, production patterns, and Angular agent best practices. We keep it focused and infrequent — no spam.</p>
8+
<a href="https://stream-resource.dev/docs" style="display:inline-block;background-color:#004090;color:#fff;padding:12px 28px;border-radius:8px;font-size:14px;font-weight:700;text-decoration:none">Explore the Docs</a>
9+
`,
10+
});
1211
}
Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,15 @@
1+
import { wrapEmail, esc } from './email-wrapper';
2+
13
const DOWNLOAD_URL = 'https://stream-resource.dev/whitepaper.pdf';
24

35
export function whitepaperDownloadHtml(name?: string): string {
4-
return `<!DOCTYPE html><html><head></head>
5-
<body style="font-family:Inter,Arial,sans-serif;background-color:#f4f4f5;padding:40px 0;margin:0">
6-
<div style="max-width:520px;margin:0 auto;background-color:#fff;border-radius:12px;padding:32px 40px;border:1px solid #e4e4e7">
7-
<p style="font-size:11px;font-family:monospace;text-transform:uppercase;letter-spacing:0.08em;color:#004090;font-weight:700;margin:0 0 12px">Angular Agent Framework</p>
8-
<p style="font-size:22px;font-weight:700;color:#1a1a2e;margin:0 0 8px">Your Angular Agent Readiness Guide</p>
9-
<p style="font-size:14px;color:#3f3f46;line-height:1.6;margin:0 0 24px">${name ? `Hi ${esc(name)}, t` : 'T'}he guide covers six production-readiness dimensions: streaming state, thread persistence, tool-call rendering, human approval flows, generative UI, and deterministic testing.</p>
10-
<div style="text-align:center;margin:0 0 24px">
11-
<a href="${DOWNLOAD_URL}" style="background-color:#004090;color:#fff;padding:14px 32px;border-radius:10px;font-size:14px;font-weight:700;text-decoration:none;display:inline-block">Download the Guide</a>
12-
</div>
13-
<hr style="border:none;border-top:1px solid #e4e4e7;margin:16px 0"/>
14-
<p style="font-size:12px;color:#a1a1aa;line-height:1.5;margin:0">Angular Agent Framework — Signal-native streaming for LangGraph.</p>
15-
</div></body></html>`;
16-
}
17-
18-
function esc(s: string): string {
19-
return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
6+
return wrapEmail({
7+
body: `
8+
<p style="font-size:20px;font-weight:700;color:#1a1a2e;margin:0 0 8px;line-height:1.3">Your Angular Agent Readiness Guide</p>
9+
<p style="font-size:14px;color:#555770;line-height:1.7;margin:0 0 24px">${name ? `Hi ${esc(name)}, t` : 'T'}he guide covers six production-readiness dimensions: streaming state, thread persistence, tool-call rendering, human approval flows, generative UI, and deterministic testing.</p>
10+
<div style="text-align:center;margin:0 0 4px">
11+
<a href="${DOWNLOAD_URL}" style="display:inline-block;background-color:#004090;color:#fff;padding:12px 32px;border-radius:8px;font-size:14px;font-weight:700;text-decoration:none">Download the Guide</a>
12+
</div>
13+
`,
14+
});
2015
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/**
2+
* Dev-only email template preview route.
3+
* Visit /api/email-preview?template=whitepaper-download to preview a template.
4+
* Available templates: whitepaper-download, newsletter-welcome, lead-notification,
5+
* drip-day-2, drip-day-5, drip-day-10, drip-day-20
6+
*/
7+
import { NextRequest, NextResponse } from 'next/server';
8+
import { whitepaperDownloadHtml } from '../../../../emails/whitepaper-download';
9+
import { newsletterWelcomeHtml } from '../../../../emails/newsletter-welcome';
10+
import { leadNotificationHtml } from '../../../../emails/lead-notification';
11+
import { dripWhitepaperFollowupHtml } from '../../../../emails/drip-whitepaper-followup';
12+
13+
const TEMPLATES: Record<string, () => { subject: string; html: string }> = {
14+
'whitepaper-download': () => ({
15+
subject: 'Your Angular Agent Readiness Guide',
16+
html: whitepaperDownloadHtml('Brian'),
17+
}),
18+
'newsletter-welcome': () => ({
19+
subject: 'Welcome to Angular Agent Framework updates',
20+
html: newsletterWelcomeHtml(),
21+
}),
22+
'lead-notification': () => ({
23+
subject: 'New lead: Brian at Cacheplane',
24+
html: leadNotificationHtml({
25+
name: 'Brian Love',
26+
email: 'brian@cacheplane.io',
27+
company: 'Cacheplane',
28+
message: 'Interested in the pilot program for our Angular + LangGraph project.',
29+
ts: new Date().toISOString(),
30+
}),
31+
}),
32+
'drip-day-2': () => dripWhitepaperFollowupHtml(2),
33+
'drip-day-5': () => dripWhitepaperFollowupHtml(5),
34+
'drip-day-10': () => dripWhitepaperFollowupHtml(10),
35+
'drip-day-20': () => dripWhitepaperFollowupHtml(20),
36+
};
37+
38+
export async function GET(req: NextRequest) {
39+
const template = req.nextUrl.searchParams.get('template');
40+
41+
// Index page — show all templates
42+
if (!template) {
43+
const links = Object.keys(TEMPLATES).map(
44+
(t) => `<a href="/api/email-preview?template=${t}" style="display:block;padding:8px 0;color:#004090;font-size:14px">${t}</a>`
45+
).join('');
46+
47+
return new NextResponse(
48+
`<!DOCTYPE html><html><head><meta charset="utf-8"><title>Email Previews</title></head>
49+
<body style="font-family:Inter,Arial,sans-serif;max-width:600px;margin:40px auto;padding:0 20px">
50+
<h1 style="font-size:24px;font-weight:700;color:#1a1a2e;margin:0 0 8px">Email Template Previews</h1>
51+
<p style="font-size:14px;color:#71717a;margin:0 0 24px">Click a template to preview it as rendered HTML.</p>
52+
${links}
53+
</body></html>`,
54+
{ headers: { 'Content-Type': 'text/html' } }
55+
);
56+
}
57+
58+
const factory = TEMPLATES[template];
59+
if (!factory) {
60+
return new NextResponse(`Unknown template: ${template}`, { status: 404 });
61+
}
62+
63+
const { subject, html } = factory();
64+
65+
// Wrap in a preview frame showing subject line
66+
const preview = `<!DOCTYPE html><html><head><meta charset="utf-8"><title>Preview: ${subject}</title></head>
67+
<body style="margin:0;padding:0;background:#e4e4e7">
68+
<div style="background:#fff;padding:12px 24px;border-bottom:1px solid #e4e4e7;font-family:Inter,Arial,sans-serif;display:flex;align-items:center;justify-content:space-between">
69+
<div>
70+
<span style="font-size:11px;color:#71717a;text-transform:uppercase;letter-spacing:0.06em">Subject:</span>
71+
<span style="font-size:14px;font-weight:600;color:#1a1a2e;margin-left:8px">${subject}</span>
72+
</div>
73+
<a href="/api/email-preview" style="font-size:12px;color:#004090;text-decoration:none">← All templates</a>
74+
</div>
75+
<div style="padding:20px 0">
76+
${html}
77+
</div>
78+
</body></html>`;
79+
80+
return new NextResponse(preview, { headers: { 'Content-Type': 'text/html' } });
81+
}

0 commit comments

Comments
 (0)