From e100d4ea6c370996d9bef9b27394f1777c7f04e1 Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Tue, 28 Apr 2026 23:26:06 -0400 Subject: [PATCH] email template fixes --- actions/{emailAuth.ts => emailAuth.tsx} | 43 ++--- .../inngest/functions/send_post_broadcast.ts | 27 ++- emails/leafletConfirmEmail.tsx | 8 +- emails/post.tsx | 161 +++++++++++++++--- 4 files changed, 185 insertions(+), 54 deletions(-) rename actions/{emailAuth.ts => emailAuth.tsx} (79%) diff --git a/actions/emailAuth.ts b/actions/emailAuth.tsx similarity index 79% rename from actions/emailAuth.ts rename to actions/emailAuth.tsx index eb9d11a7..da48b984 100644 --- a/actions/emailAuth.ts +++ b/actions/emailAuth.tsx @@ -9,35 +9,24 @@ import { cookies } from "next/headers"; import { setAuthToken } from "src/auth"; import { pool } from "supabase/pool"; import { supabaseServerClient } from "supabase/serverClient"; +import { LeafletConfirmEmail } from "emails/leafletConfirmEmail"; +import { sendConfirmationEmail } from "src/utils/confirmationEmail"; async function sendAuthCode(email: string, code: string) { - if (process.env.NODE_ENV === "development") { - console.log("Auth code:", code); - return; - } - - let res = await fetch("https://api.postmarkapp.com/email", { - method: "POST", - headers: { - "Content-Type": "application/json", - "X-Postmark-Server-Token": process.env.POSTMARK_API_KEY!, - }, - body: JSON.stringify({ - From: "Leaflet ", - Subject: `Your authentication code for Leaflet is ${code}`, - To: email, - TextBody: `Paste this code to login to Leaflet: - -${code} - `, - HtmlBody: ` - - -

Paste this code to login to Leaflet: ${code}

- - - `, - }), + await sendConfirmationEmail({ + to: email, + subject: `Your authentication code for Leaflet is ${code}`, + template: ( + + ), + text: `Paste this code to login to Leaflet:\n\n${code}\n`, + devLogTag: "auth code", + code, }); } diff --git a/app/api/inngest/functions/send_post_broadcast.ts b/app/api/inngest/functions/send_post_broadcast.ts index 4bef3841..ab3fa0a9 100644 --- a/app/api/inngest/functions/send_post_broadcast.ts +++ b/app/api/inngest/functions/send_post_broadcast.ts @@ -45,8 +45,10 @@ export const send_post_broadcast = inngest.createFunction( async ({ event, step }) => { const { publication_uri, document_uri } = event.data; + const authorDid = new AtUri(document_uri).host; + const loaded = await step.run("load-pub-and-doc", async () => { - const [pubRes, docRes] = await Promise.all([ + const [pubRes, docRes, profileRes] = await Promise.all([ supabaseServerClient .from("publications") .select( @@ -59,8 +61,17 @@ export const send_post_broadcast = inngest.createFunction( .select("data") .eq("uri", document_uri) .maybeSingle(), + supabaseServerClient + .from("bsky_profiles") + .select("handle") + .eq("did", authorDid) + .maybeSingle(), ]); - return { pub: pubRes.data, doc: docRes.data }; + return { + pub: pubRes.data, + doc: docRes.data, + profile: profileRes.data, + }; }); const settings = loaded.pub?.publication_newsletter_settings; @@ -108,7 +119,15 @@ export const send_post_broadcast = inngest.createFunction( } const fromHeader = buildFromHeader(pubRecord?.name, fromDomain); const replyToEmail = resolveReplyToEmail(settings); - const did = new AtUri(document_uri).host; + const did = authorDid; + const authorName = loaded.profile?.handle ?? undefined; + const publishedAtLabel = docRecord?.publishedAt + ? new Date(docRecord.publishedAt).toLocaleDateString("en-US", { + month: "short", + day: "numeric", + year: "numeric", + }) + : undefined; // The first page is the document body. Canvas pages don't map to a linear // email body — the email renders an empty postContent section and falls @@ -160,6 +179,8 @@ export const send_post_broadcast = inngest.createFunction( postTitle, postDescription, postUrl, + authorName, + publishedAtLabel, blocks, did, assetsBaseUrl: `${assetsBaseUrl}/`, diff --git a/emails/leafletConfirmEmail.tsx b/emails/leafletConfirmEmail.tsx index 3333abd3..08eba255 100644 --- a/emails/leafletConfirmEmail.tsx +++ b/emails/leafletConfirmEmail.tsx @@ -17,10 +17,14 @@ import { export const LeafletConfirmEmail = (props: { code?: string; assetsBaseUrl?: string; + title?: string; + message?: string; }) => { const leafletSrc = makeStaticUrl( props.assetsBaseUrl ?? "https://leaflet.pub", )("leaflet.png"); + const title = props.title ?? "Welcome to Leaflet!"; + const message = props.message ?? "Verify your email with this code"; return ( @@ -42,12 +46,12 @@ export const LeafletConfirmEmail = (props: { - Welcome to Leaflet!{" "} + {title}{" "} - Verify your email with this code + {message} diff --git a/emails/post.tsx b/emails/post.tsx index 03fe938f..79d0de5b 100644 --- a/emails/post.tsx +++ b/emails/post.tsx @@ -16,6 +16,7 @@ import type { PrismLanguage } from "@react-email/code-block"; import React, { type CSSProperties } from "react"; import { PubLeafletBlocksBlockquote, + PubLeafletBlocksButton, PubLeafletBlocksCode, PubLeafletBlocksHeader, PubLeafletBlocksHorizontalRule, @@ -159,6 +160,32 @@ const defaultProps: PostEmailProps = { description: "Description on the link", }, }, + { + $type: "pub.leaflet.pages.linearDocument#block", + block: { + $type: "pub.leaflet.blocks.button", + text: "Click me", + url: "https://leaflet.pub", + }, + }, + { + $type: "pub.leaflet.pages.linearDocument#block", + alignment: "lex:pub.leaflet.pages.linearDocument#textAlignLeft", + block: { + $type: "pub.leaflet.blocks.button", + text: "Aligned left", + url: "https://leaflet.pub", + }, + }, + { + $type: "pub.leaflet.pages.linearDocument#block", + alignment: "lex:pub.leaflet.pages.linearDocument#textAlignRight", + block: { + $type: "pub.leaflet.blocks.button", + text: "Aligned right", + url: "https://leaflet.pub", + }, + }, { $type: "pub.leaflet.pages.linearDocument#block", block: { $type: "pub.leaflet.blocks.horizontalRule" }, @@ -301,7 +328,16 @@ export const PostEmail = (props: Partial = {}) => { margin: "8px 0 0", }} > - {p.postTitle} + + {p.postTitle} + {p.postDescription ? ( @@ -365,17 +401,6 @@ export const PostEmail = (props: Partial = {}) => { /> - - - - Open post - - ) : null} @@ -384,6 +409,7 @@ export const PostEmail = (props: Partial = {}) => { = {}) => { lineHeight: "20px", }} > - See Full Post + Read in Browser @@ -450,27 +476,23 @@ export const PostEmail = (props: Partial = {}) => { )} - - - - {/* Spacer */}   - {/* Horizontal rule between card and watermark.
- margins are flaky in Gmail, so we use a 1px-tall - with border-top instead. */} + {/* Horizontal rule above watermark.
margins are + flaky in Gmail, so we use a 1px-tall with + border-top instead. */} = {}) => { + + + + @@ -516,14 +542,35 @@ export const PostEmail = (props: Partial = {}) => { }; export default PostEmail; +// Map the lexicon's alignment token to the simple left/center/right values +// usable as HTML `align` attributes. `justify` falls through to `left` to +// match published web behavior (`justify-start` flex). For buttons we +// default to `center` when alignment is unset, matching PostContent.tsx. +const resolveButtonAlignment = ( + alignment: string | undefined, +): "left" | "center" | "right" => { + switch (alignment) { + case "lex:pub.leaflet.pages.linearDocument#textAlignRight": + return "right"; + case "lex:pub.leaflet.pages.linearDocument#textAlignLeft": + case "lex:pub.leaflet.pages.linearDocument#textAlignJustify": + return "left"; + case "lex:pub.leaflet.pages.linearDocument#textAlignCenter": + default: + return "center"; + } +}; + const BlockRenderer = ({ block, + alignment, did, assetsBaseUrl, theme, colors, }: { block: PubLeafletPagesLinearDocument.Block["block"]; + alignment?: string; did: string; assetsBaseUrl: string; theme: EmailTheme; @@ -633,6 +680,16 @@ const BlockRenderer = ({ /> ); } + if (PubLeafletBlocksButton.isMain(block)) { + return ( + + ); + } if (PubLeafletBlocksHorizontalRule.isMain(block)) { return (
{ + // Bulletproof button: table-based so Outlook (which ignores padding on + // ) renders a real clickable button. The `` carries the bgcolor + // attribute and padding; the `` is `display: block` so the entire + // padded area is clickable. Alignment via the table's `align` HTML + // attribute — Gmail won't reliably cascade `text-align` from a wrapping + //
, so we anchor on the table itself. + return ( +
+ + + + + + +
+ + {text} + +
+
+ ); +}; + export const CodeBlock = ({ code, language, -- 2.51.2