feat: packages/email — Maizzle v6 + Tailwind CSS v4 HTML email templates
- New @sexy.pivoine.art/email package with @maizzle/framework@6.0.0-15 - Uses @maizzle/tailwindcss (TW v4 preset) with @theme brand tokens derived from the frontend's app.css oklch primary color - LightningCSS automatically lowers oklch/lab to hex for email clients - Real HTML template files (templates/layouts/main.html, verification.html, password-reset.html) — not JS template strings - PostCSS `from` override so @import "@maizzle/tailwindcss" resolves from the email package's own node_modules - Backend lib/email.ts now calls renderVerification/renderPasswordReset instead of inline HTML strings Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
25
packages/email/src/index.ts
Normal file
25
packages/email/src/index.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { renderTemplate } from "./render.js";
|
||||
|
||||
const BASE_URL = process.env.PUBLIC_URL ?? "https://sexy.pivoine.art";
|
||||
|
||||
export async function renderVerification(data: {
|
||||
token: string;
|
||||
}): Promise<{ subject: string; html: string }> {
|
||||
return {
|
||||
subject: "Verify your email address — sexy.pivoine.art",
|
||||
html: await renderTemplate("verification", {
|
||||
url: `${BASE_URL}/signup/verify?token=${data.token}`,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
export async function renderPasswordReset(data: {
|
||||
token: string;
|
||||
}): Promise<{ subject: string; html: string }> {
|
||||
return {
|
||||
subject: "Reset your password — sexy.pivoine.art",
|
||||
html: await renderTemplate("password-reset", {
|
||||
url: `${BASE_URL}/password/reset?token=${data.token}`,
|
||||
}),
|
||||
};
|
||||
}
|
||||
43
packages/email/src/render.ts
Normal file
43
packages/email/src/render.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { readFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
// Templates live at packages/email/templates/
|
||||
// At runtime (dist/render.js), __dirname is packages/email/dist/
|
||||
const PKG_ROOT = path.join(__dirname, "..");
|
||||
const TEMPLATES_ROOT = path.join(PKG_ROOT, "templates");
|
||||
const BASE_URL = process.env.PUBLIC_URL ?? "https://sexy.pivoine.art";
|
||||
|
||||
export interface RenderOptions {
|
||||
url: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export async function renderTemplate(
|
||||
name: string,
|
||||
locals: RenderOptions,
|
||||
): Promise<string> {
|
||||
// Dynamic import: @maizzle/framework v6 is ESM-only
|
||||
const { render } = await import("@maizzle/framework");
|
||||
|
||||
const html = await readFile(path.join(TEMPLATES_ROOT, `${name}.html`), "utf8");
|
||||
|
||||
const { html: rendered } = await render(html, {
|
||||
components: {
|
||||
root: TEMPLATES_ROOT,
|
||||
folders: ["layouts"],
|
||||
},
|
||||
// Override PostCSS `from` so @tailwindcss/postcss resolves @import "@maizzle/tailwindcss"
|
||||
// from this package's node_modules (defu gives our value priority over the cwd default).
|
||||
postcss: {
|
||||
options: {
|
||||
from: path.join(PKG_ROOT, "email.css"),
|
||||
},
|
||||
},
|
||||
locals: {
|
||||
baseUrl: BASE_URL,
|
||||
...locals,
|
||||
},
|
||||
});
|
||||
|
||||
return rendered;
|
||||
}
|
||||
Reference in New Issue
Block a user