Skip to content

Commit fe4a603

Browse files
feat(email): stx-first templates + drop @ts-ignore (A1)
Converts the four bundled email templates from .html (plain {{var}} interpolation + layout wrapping) to .stx (full self-contained documents with <script server> for props, same render pipeline as the existing subscription-confirmation.stx). - resources/emails/welcome.stx - resources/emails/password-reset.stx - resources/emails/password-changed.stx - resources/emails/email-verification.stx Removes the duplicate layout directory at resources/layouts/emails/main.html — the only remaining email layout lives at resources/emails/layouts/base.html, used by the legacy HTML template path. New stx templates are self-contained (the stx render pipeline skips the layout-wrap step), keeping the chrome + footer copy + Outlook conditionals inline. Drops the @ts-ignore on the dynamic renderEmail import in email/template.ts — renderEmail is publicly exported from @stacksjs/stx via build-views.d.ts:31, properly typed. Visual / content parity with the original HTML: same primary colour theming, same `class="button"`-with-hover scaffold, same copy. Apps with a project-level template at resources/emails/<name>.html still resolve via the .html branch in template.ts:175 — backward compat preserved. Closes #1898. Refs umbrella #1904. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 0ae511f commit fe4a603

10 files changed

Lines changed: 346 additions & 183 deletions

File tree

storage/framework/core/email/src/template.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -277,13 +277,15 @@ export async function template(
277277
return { html: '', text: '' }
278278
}
279279

280-
// Use STX engine for .stx templates
280+
// Use STX engine for .stx templates. `renderEmail` is the public
281+
// entry point from `@stacksjs/stx` (re-exported through the package
282+
// root) that handles the full <script server> + props + layout
283+
// chain. Dynamic import so test runs / CLI scripts that never load
284+
// an email template pay zero startup cost for the stx graph.
281285
if (resolved.type === 'stx') {
282286
try {
283-
// @ts-ignore - renderEmail may not be exported yet from stx
284287
const { renderEmail } = await import('@stacksjs/stx')
285-
const result = await renderEmail(resolved.path, allVariables)
286-
return result
288+
return await renderEmail(resolved.path, allVariables)
287289
}
288290
catch (error: unknown) {
289291
log.warn(`[email] STX template rendering failed for ${templateName}: ${error instanceof Error ? error.message : String(error)}`)

storage/framework/defaults/resources/emails/email-verification.html

Lines changed: 0 additions & 43 deletions
This file was deleted.
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
<script server>
2+
// Email-verification message — sent after sign-up so the user confirms ownership of the address.
3+
const appName = props.appName || 'Stacks'
4+
const verificationUrl = props.verificationUrl || '#'
5+
const expiryMinutes = props.expiryMinutes || 60
6+
const userName = props.userName || 'there'
7+
const primaryColor = props.primaryColor || '#3451b2'
8+
const primaryColorDark = props.primaryColorDark || '#2a4198'
9+
const subject = props.subject || `Verify your email for ${appName}`
10+
const year = props.year || new Date().getFullYear()
11+
</script>
12+
13+
<!DOCTYPE html>
14+
<html lang="en">
15+
<head>
16+
<meta charset="UTF-8">
17+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
18+
<meta http-equiv="X-UA-Compatible" content="IE=edge">
19+
<title>{{ subject }}</title>
20+
<!--[if mso]>
21+
<noscript><xml><o:OfficeDocumentSettings><o:PixelsPerInch>96</o:PixelsPerInch></o:OfficeDocumentSettings></xml></noscript>
22+
<![endif]-->
23+
<style>
24+
body, table, td, p, a, li, blockquote { -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; }
25+
table, td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; }
26+
img { -ms-interpolation-mode: bicubic; border: 0; height: auto; line-height: 100%; outline: none; text-decoration: none; }
27+
body { height: 100% !important; margin: 0 !important; padding: 0 !important; width: 100% !important; }
28+
.button { display: inline-block; padding: 14px 32px; background-color: {{ primaryColor }}; color: #ffffff !important; text-decoration: none; border-radius: 6px; font-weight: 600; font-size: 16px; text-align: center; }
29+
.button:hover { background-color: {{ primaryColorDark }}; }
30+
@media only screen and (max-width: 600px) {
31+
.container { width: 100% !important; padding: 20px !important; }
32+
.content { padding: 24px !important; }
33+
.button { display: block !important; width: 100% !important; }
34+
}
35+
</style>
36+
</head>
37+
<body style="margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background-color: #f4f4f5; line-height: 1.6;">
38+
<table role="presentation" cellpadding="0" cellspacing="0" style="width: 100%; border-collapse: collapse;">
39+
<tr>
40+
<td style="padding: 40px 20px;">
41+
<table role="presentation" cellpadding="0" cellspacing="0" class="container" style="max-width: 600px; margin: 0 auto; background-color: #ffffff; border-radius: 8px; overflow: hidden; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);">
42+
<tr>
43+
<td style="background-color: {{ primaryColor }}; padding: 30px 40px; text-align: center;">
44+
<h1 style="margin: 0; color: #ffffff; font-size: 24px; font-weight: 600;">{{ appName }}</h1>
45+
</td>
46+
</tr>
47+
<tr>
48+
<td class="content" style="padding: 40px;">
49+
<h2 style="margin: 0 0 20px; color: #111827; font-size: 20px; font-weight: 600;">Verify Your Email Address</h2>
50+
51+
<p style="margin: 0 0 20px; color: #4b5563; font-size: 16px;">Hi {{ userName }}, please verify your email address to complete your <strong>{{ appName }}</strong> account setup. Click the button below to verify.</p>
52+
53+
<table role="presentation" cellpadding="0" cellspacing="0" style="margin: 30px 0;">
54+
<tr>
55+
<td>
56+
<a href="{{ verificationUrl }}" class="button">Verify Email Address</a>
57+
</td>
58+
</tr>
59+
</table>
60+
61+
<p style="margin: 0 0 20px; color: #6b7280; font-size: 14px;">This verification link will expire in <strong>{{ expiryMinutes }} minutes</strong>.</p>
62+
63+
<p style="margin: 0 0 20px; color: #6b7280; font-size: 14px;">If you did not create an account, please ignore this email.</p>
64+
65+
<hr style="border: none; border-top: 1px solid #e5e7eb; margin: 30px 0;">
66+
67+
<p style="margin: 0; color: #9ca3af; font-size: 12px;">If the button above doesn't work, copy and paste the following URL into your browser:</p>
68+
<p style="margin: 10px 0 0; word-break: break-all;">
69+
<a href="{{ verificationUrl }}" style="color: {{ primaryColor }}; font-size: 12px;">{{ verificationUrl }}</a>
70+
</p>
71+
</td>
72+
</tr>
73+
<tr>
74+
<td style="padding: 20px 40px; background-color: #f9fafb; border-top: 1px solid #e5e7eb;">
75+
<p style="margin: 0; color: #6b7280; font-size: 12px; text-align: center;">This is an automated message from {{ appName }}. Please do not reply to this email.</p>
76+
<p style="margin: 10px 0 0; color: #9ca3af; font-size: 11px; text-align: center;">&copy; {{ year }} {{ appName }}. All rights reserved.</p>
77+
</td>
78+
</tr>
79+
</table>
80+
</td>
81+
</tr>
82+
</table>
83+
</body>
84+
</html>

storage/framework/defaults/resources/emails/password-changed.html

Lines changed: 0 additions & 41 deletions
This file was deleted.
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
<script server>
2+
// Password-changed notification — sent after the user's password is updated.
3+
// Includes a security warning panel for the "wasn't me" case.
4+
const appName = props.appName || 'Stacks'
5+
const changedAt = props.changedAt || new Date().toISOString()
6+
const supportEmail = props.supportEmail || ''
7+
const primaryColor = props.primaryColor || '#3451b2'
8+
const primaryColorDark = props.primaryColorDark || '#2a4198'
9+
const subject = props.subject || `Your ${appName} password was changed`
10+
const year = props.year || new Date().getFullYear()
11+
</script>
12+
13+
<!DOCTYPE html>
14+
<html lang="en">
15+
<head>
16+
<meta charset="UTF-8">
17+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
18+
<meta http-equiv="X-UA-Compatible" content="IE=edge">
19+
<title>{{ subject }}</title>
20+
<!--[if mso]>
21+
<noscript><xml><o:OfficeDocumentSettings><o:PixelsPerInch>96</o:PixelsPerInch></o:OfficeDocumentSettings></xml></noscript>
22+
<![endif]-->
23+
<style>
24+
body, table, td, p, a, li, blockquote { -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; }
25+
table, td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; }
26+
img { -ms-interpolation-mode: bicubic; border: 0; height: auto; line-height: 100%; outline: none; text-decoration: none; }
27+
body { height: 100% !important; margin: 0 !important; padding: 0 !important; width: 100% !important; }
28+
.button { display: inline-block; padding: 14px 32px; background-color: {{ primaryColor }}; color: #ffffff !important; text-decoration: none; border-radius: 6px; font-weight: 600; font-size: 16px; text-align: center; }
29+
.button:hover { background-color: {{ primaryColorDark }}; }
30+
@media only screen and (max-width: 600px) {
31+
.container { width: 100% !important; padding: 20px !important; }
32+
.content { padding: 24px !important; }
33+
.button { display: block !important; width: 100% !important; }
34+
}
35+
</style>
36+
</head>
37+
<body style="margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background-color: #f4f4f5; line-height: 1.6;">
38+
<table role="presentation" cellpadding="0" cellspacing="0" style="width: 100%; border-collapse: collapse;">
39+
<tr>
40+
<td style="padding: 40px 20px;">
41+
<table role="presentation" cellpadding="0" cellspacing="0" class="container" style="max-width: 600px; margin: 0 auto; background-color: #ffffff; border-radius: 8px; overflow: hidden; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);">
42+
<tr>
43+
<td style="background-color: {{ primaryColor }}; padding: 30px 40px; text-align: center;">
44+
<h1 style="margin: 0; color: #ffffff; font-size: 24px; font-weight: 600;">{{ appName }}</h1>
45+
</td>
46+
</tr>
47+
<tr>
48+
<td class="content" style="padding: 40px;">
49+
<h2 style="margin: 0 0 20px; color: #111827; font-size: 20px; font-weight: 600;">Password Changed Successfully</h2>
50+
51+
<p style="margin: 0 0 20px; color: #4b5563; font-size: 16px;">Your <strong>{{ appName }}</strong> account password was successfully changed on:</p>
52+
53+
<p style="margin: 0 0 20px; padding: 15px; background-color: #f3f4f6; border-radius: 6px; color: #111827; font-size: 16px; font-weight: 500; text-align: center;">
54+
{{ changedAt }}
55+
</p>
56+
57+
<p style="margin: 0 0 20px; color: #4b5563; font-size: 16px;">If you made this change, you can safely ignore this email.</p>
58+
59+
<div style="padding: 20px; background-color: #fef2f2; border: 1px solid #fecaca; border-radius: 6px; margin: 20px 0;">
60+
<p style="margin: 0; color: #991b1b; font-size: 14px; font-weight: 600;">Didn't make this change?</p>
61+
<p style="margin: 10px 0 0; color: #7f1d1d; font-size: 14px;">
62+
If you did not change your password, your account may have been compromised. Please contact support immediately@if(supportEmail) at <a href="mailto:{{ supportEmail }}" style="color: #991b1b;">{{ supportEmail }}</a>@endif.
63+
</p>
64+
</div>
65+
66+
<h3 style="margin: 30px 0 15px; color: #111827; font-size: 16px; font-weight: 600;">Security Recommendations</h3>
67+
68+
<ul style="margin: 0; padding: 0 0 0 20px; color: #4b5563; font-size: 14px;">
69+
<li style="margin-bottom: 8px;">Use a strong, unique password</li>
70+
<li style="margin-bottom: 8px;">Enable two-factor authentication if available</li>
71+
<li style="margin-bottom: 8px;">Review your recent account activity</li>
72+
<li style="margin-bottom: 8px;">Never share your password with anyone</li>
73+
</ul>
74+
</td>
75+
</tr>
76+
<tr>
77+
<td style="padding: 20px 40px; background-color: #f9fafb; border-top: 1px solid #e5e7eb;">
78+
<p style="margin: 0; color: #6b7280; font-size: 12px; text-align: center;">This is an automated message from {{ appName }}. Please do not reply to this email.</p>
79+
<p style="margin: 10px 0 0; color: #9ca3af; font-size: 11px; text-align: center;">&copy; {{ year }} {{ appName }}. All rights reserved.</p>
80+
</td>
81+
</tr>
82+
</table>
83+
</td>
84+
</tr>
85+
</table>
86+
</body>
87+
</html>

storage/framework/defaults/resources/emails/password-reset.html

Lines changed: 0 additions & 42 deletions
This file was deleted.

0 commit comments

Comments
 (0)