-
Notifications
You must be signed in to change notification settings - Fork 4.2k
Description
Feature: Expose Maily theme configuration for email steps
Description
The Maily rendering library (libs/maily-render) already has full theme support — a ThemeOptions interface, a setTheme() method, and 34+ internal references applying theme values during rendering. The render() function signature already accepts a theme parameter in its config object:
// libs/maily-render/src/render.ts
export async function render(content: JSONContent, config?: MailyConfig & RenderOptions): Promise<string> {
const { theme, preview, ...rest } = config || {};
maily.setTheme(theme || {});
// ...
}However, there is currently no way for users to actually configure or pass a theme. The API renderer hardcodes an empty theme (falling back to DEFAULT_THEME):
// apps/api/src/app/environments-v1/usecases/output-renderers/email-output-renderer.usecase.ts:474
const renderedMaily = await mailyRender(parsedMaily, { noHtmlWrappingTags });
// ^ theme is never passedThis means all emails render with hardcoded default colors and font sizes, with no ability to customize branding.
What's already done
| Layer | Status |
|---|---|
libs/maily-render — ThemeOptions type, setTheme(), deep merge with defaults |
Done |
render() function accepts theme in config |
Done |
Test coverage for custom themes (render.test.ts) |
Done |
What's missing
| Layer | File(s) | Work needed |
|---|---|---|
Vendored renderer ThemeOptions |
libs/maily-render/src/maily.tsx |
Align with upstream RendererThemeOptions (see below) |
| Email control schema | libs/application-generic/src/schemas/control/email-control.schema.ts |
Add optional theme field to emailControlZodSchema |
| Email UI schema | Same file, emailUiSchema |
Decide on UI component for theme configuration |
| Control value sanitization | libs/application-generic/src/utils/sanitize-control-values.ts |
Handle theme in sanitizeEmail() |
| API email renderer | apps/api/.../email-output-renderer.usecase.ts |
Extract theme from control values and pass to mailyRender() |
| Dashboard editor | apps/dashboard/src/components/maily/maily.tsx |
Expose theme configuration UI |
| Editor preview | @novu/maily-core EditorProps |
Accept theme prop for real-time preview in the editor |
Update vendored renderer to match upstream theme options
Novu's vendored ThemeOptions in libs/maily-render/src/maily.tsx is a reduced subset of what upstream Maily supports. The upstream RendererThemeOptions (from @maily-to/shared/src/theme.ts) includes several additional categories that the vendored version has stripped:
Current Novu ThemeOptions — only supports:
colors(13 semantic color properties)fontSize(paragraph + footer font sizing)
Upstream RendererThemeOptions — additionally supports:
interface BaseThemeOptions {
container?: Partial<{
backgroundColor, maxWidth, minWidth,
paddingTop, paddingRight, paddingBottom, paddingLeft,
borderRadius, borderWidth, borderColor
}>;
body?: Partial<{
backgroundColor,
paddingTop, paddingRight, paddingBottom, paddingLeft
}>;
button?: Partial<{
paddingTop, paddingRight, paddingBottom, paddingLeft,
backgroundColor, color
}>;
link?: Partial<{ color }>;
font?: {
fontFamily: string;
fallbackFontFamily: FallbackFont;
webFont?: { url: string; format: FontFormat };
fontStyle?: string;
fontWeight?: string | number;
} | null;
}These upstream options enable essential branding capabilities:
- Container: control email width, background color, padding, and border styling
- Body: outer wrapper background and padding (important for "card-in-a-page" layouts)
- Button: default CTA button colors and padding
- Link: global link color
- Font: custom font family with web font support (critical for brand consistency)
The vendored renderer should be updated to support the full upstream RendererThemeOptions interface, including container, body, button, link, and font customization.
Proposed scope levels
Theme could be configured at different scopes with inheritance:
- Environment-level (brand defaults) — applied to all emails in a Novu environment
- Layout-level — override environment defaults for emails using a specific layout
- Step-level — override for a single email step
This mirrors how layoutId currently works as an optional override per step.
At minimum, step-level support would already be valuable — it would allow passing theme through the existing control values mechanism.
Minimal implementation path
The smallest useful change would be:
- Update
ThemeOptionsinlibs/maily-render/src/maily.tsxto match upstreamRendererThemeOptions - Add
themeas an optional field toemailControlZodSchema(structure field, code-controlled) - Pass
themefrom control values tomailyRender()in the API renderer - Dashboard UI can come later — theme would initially be configured through code/API
This would let framework users set email themes programmatically:
workflow('welcome-email', async ({ step }) => {
await step.email('send', async (controls) => ({
subject: controls.subject,
body: controls.body,
}), {
controlSchema: {
// ...
theme: {
colors: { heading: '#1a1a2e', paragraph: '#16213e' },
button: { backgroundColor: '#e94560', color: '#ffffff' },
font: { fontFamily: 'Inter', fallbackFontFamily: 'sans-serif' },
container: { maxWidth: '600px', backgroundColor: '#f5f5f5' },
},
},
});
});References
- Maily render
ThemeOptions:libs/maily-render/src/maily.tsx:87-116 - Maily
setTheme():libs/maily-render/src/maily.tsx— deep merges withDEFAULT_THEME - Maily
render()accepting theme:libs/maily-render/src/render.ts - Existing theme test:
libs/maily-render/src/render.test.ts:184-217 - API renderer (theme not passed):
apps/api/src/app/environments-v1/usecases/output-renderers/email-output-renderer.usecase.ts:474 - Email control schema (no theme field):
libs/application-generic/src/schemas/control/email-control.schema.ts:7-24 - Upstream theme definition:
packages/shared/src/theme.ts