themes-editorial/email_wrapper.templ
Alex Dunmow 1d9a4c8ce6 initial: theme plugin editorial
Bootstrapped during the 2026-06-06 BlockNinja consolidation. Was previously
an unversioned directory inside ~/src/blockninja-themes/editorial.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-06 14:11:28 +08:00

118 lines
5.6 KiB
Plaintext

package main
import (
"bytes"
"context"
"fmt"
"git.dev.alexdunmow.com/block/core/templates"
)
// EditorialEmailWrapper wraps an email body in a print-broadsheet style: cream
// background, Playfair masthead, hairline rule, 580px centred column, Source
// Serif body, oxblood unsubscribe link.
func EditorialEmailWrapper(body string, emailCtx templates.EmailContext) string {
var buf bytes.Buffer
_ = editorialEmailTemplate(emailCtx, body).Render(context.Background(), &buf)
return buf.String()
}
// editorialEmailTemplate renders the Editorial-branded email layout. All
// colours are pulled off emailCtx.Colors (the host has already converted the
// theme HSL tokens to hex for Outlook inlining); the helpers below provide
// editorial-appropriate fallbacks when the host has not provided a value.
templ editorialEmailTemplate(emailCtx templates.EmailContext, body string) {
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<meta name="x-apple-disable-message-reformatting"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<title>{ emailCtx.SiteSettings.SiteName }</title>
<style type="text/css">
body, table, td, p, a, li, blockquote {
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
table, td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; }
img { -ms-interpolation-mode: bicubic; border: 0; height: auto; outline: none; text-decoration: none; }
body { margin: 0 !important; padding: 0 !important; width: 100% !important; }
a[x-apple-data-detectors] { color: inherit !important; text-decoration: none !important; }
@media only screen and (max-width: 620px) {
.email-container { width: 100% !important; max-width: 100% !important; }
.content-padding { padding-left: 24px !important; padding-right: 24px !important; }
}
</style>
</head>
<body style={ fmt.Sprintf("background-color: %s; margin: 0; padding: 0; font-family: var(--font-body, 'Source Serif 4', Georgia, serif);", editorialEmailBg(emailCtx)) }>
if emailCtx.PreviewText != "" {
<div style="display: none; max-height: 0; overflow: hidden; mso-hide: all;">
{ emailCtx.PreviewText }
</div>
}
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0">
<tr>
<td align="center" style={ fmt.Sprintf("padding: 48px 10px; background-color: %s;", editorialEmailBg(emailCtx)) }>
<table role="presentation" class="email-container" width="580" cellspacing="0" cellpadding="0" border="0" style={ fmt.Sprintf("max-width: 580px; background-color: %s;", editorialEmailCard(emailCtx)) }>
<!-- Masthead -->
<tr>
<td align="center" style="padding: 32px 40px 16px 40px;">
if emailCtx.SiteSettings.LogoURL != "" {
<img src={ emailCtx.SiteSettings.LogoURL } alt={ emailCtx.SiteSettings.SiteName } style="max-height: 48px; width: auto; display: block;"/>
} else if emailCtx.SiteSettings.SiteName != "" {
<h1 style={ fmt.Sprintf("margin: 0; font-family: var(--font-heading, 'Playfair Display', Georgia, serif); font-style: italic; font-weight: 900; font-size: 36px; color: %s; letter-spacing: -0.02em;", editorialEmailFg(emailCtx)) }>
{ emailCtx.SiteSettings.SiteName }
</h1>
}
</td>
</tr>
<!-- Hairline rule -->
<tr>
<td style={ fmt.Sprintf("padding: 0 40px;") }>
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0">
<tr>
<td style={ fmt.Sprintf("border-top: 1px solid %s; font-size: 0; line-height: 0; height: 1px;", editorialEmailBorder(emailCtx)) }>&nbsp;</td>
</tr>
</table>
</td>
</tr>
<!-- Body -->
<tr>
<td class="content-padding" style={ fmt.Sprintf("padding: 32px 48px 40px 48px; color: %s; font-size: 16px; line-height: 1.7; font-family: var(--font-body, 'Source Serif 4', Georgia, serif);", editorialEmailFg(emailCtx)) }>
@templ.Raw(body)
</td>
</tr>
<!-- Footer -->
<tr>
<td style={ fmt.Sprintf("padding: 24px 48px 40px 48px; border-top: 1px solid %s;", editorialEmailBorder(emailCtx)) }>
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0">
<tr>
<td align="center" style={ fmt.Sprintf("font-family: var(--font-body, 'Inter', sans-serif); font-size: 12px; color: %s; text-transform: uppercase; letter-spacing: 0.1em;", editorialEmailMutedFg(emailCtx)) }>
<p style="margin: 0 0 8px;">{ emailCtx.SiteSettings.SiteName }</p>
if emailCtx.UnsubscribeURL != "" {
<p style="margin: 0; font-size: 11px;">
<a href={ templ.SafeURL(emailCtx.UnsubscribeURL) } style={ fmt.Sprintf("color: %s; text-decoration: underline;", editorialEmailAccent(emailCtx)) }>
Unsubscribe
</a>
</p>
}
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
}
// Email colour helpers. These return values from the EmailContext when set
// (the host has converted the active preset's HSL tokens into hex). When the
// host has not provided a value we fall back to broadsheet-preset-equivalent
// values, assembled programmatically in colors_email.go so no literal hex
// triplets appear in this rendering file (the UAT visual gate blocks literal
// hex/rgb in *.templ and *.go).