This project is a modular, reusable HTML email system built with Maizzle.
It's designed to make creating and maintaining professional, responsive newsletters easy — especially those like Wirecutter or The New York Times Recommendation series — by separating content, layout, and style.
The goal is to author newsletters in Markdown (with YAML frontmatter), convert them into JSON data, and compile everything into bulletproof, table-based HTML emails ready for delivery via Sendy, Mailchimp, or any ESP.
Maizzle is the build system — it handles templating, inlining CSS, and making the final HTML compatible with Outlook, Gmail, and Apple Mail.
The workflow looks like this:
| Layer | Role | Example File |
|---|---|---|
| Authoring | Where I write copy and configure each issue. | content/2025-10-07.md |
| Conversion | Converts Markdown → JSON. | scripts/md_to_json.mjs |
| Schema | Defines the structure for validation/autocomplete. | newsletter.schema.json |
| Templating | Defines layout and components. | src/components/*.html, src/templates/newsletter.html |
| Build | Combines templates + data → HTML. | maizzle build production --data data/newsletter.json --inline |
The system allows quick iteration:
- Write Markdown.
- Convert it to JSON.
- Build and preview HTML.
- Publish or paste it into your ESP.
Authoring (Markdown)
Each issue starts as a Markdown file with YAML frontmatter, e.g.:
---
title: The Recommendation — Patio Edition
hero:
title: The best patio furniture
url: https://example.com/patio
image:
src: https://cdn.com/hero.jpg
alt: Patio set
deck: Turn your patio into a comfortable, good-looking space.
cta:
href: https://example.com/patio
label: Deck out your patio →
feature:
title: Keep mosquitoes away
url: https://example.com/mosquito
image:
src: https://cdn.com/mosquito.jpg
alt: Thermacell repeller
---Conversion Script
A Node script (using gray-matter) reads the Markdown frontmatter, converts it into structured JSON, and saves it as data/newsletter.json.
Schema (optional)
A JSON Schema (newsletter.schema.json) defines the structure so VS Code provides IntelliSense and validation when editing JSON.
Templating Maizzle templates (using Nunjucks-like syntax) define reusable components:
HeaderLogo.htmlHero.htmlFeature.htmlTwoUp.htmlFooter.html
Each section maps directly to keys in the JSON data.
Templates may include a section-styles.json file (located at templates/<template>/section-styles.json) that provides sane defaults for every section type the template supports. This file allows templates to declare:
- containerStyles: backgroundColor, padding, borderRadius (applied to the outer table/container)
- contentStyles: paragraph/font/color styles used when injecting HTML fragments
- linkStyles: inline styles applied to links inside injected HTML
- headingStyles: heading font/size/color defaults
Implementation note: Some keys in section-styles.json were added as forward-looking options and are not fully wired into every template yet. The current build pipeline and templates primarily consume containerStyles (backgroundColor, padding, borderRadius) and use contentStyles/linkStyles when preprocessing HTML fragments for inline styling. Other fields (for example, richer headingStyles variants or experimental properties) may be present in section-styles.json for future use; if you need a value applied now, provide it in the newsletter's frontmatter (per-section override) or update the template/build script to read that key.
Actively applied keys
The following keys from section-styles.json are actively applied by the build pipeline and templates today:
containerStyles.backgroundColor— used by templates to set section/table background; if null, templates fall back tothemeColors.<section>or a hardcoded default.containerStyles.borderRadius— used by templates to set border-radius (templates also include VML fallbacks for Outlook when non-zero).contentStyles(properties consumed):fontFamilyfontSizelineHeightcolortextAlignThese are applied during preprocessing to injected HTML (item descriptions) and inserted into<p>tags as inline styles.
linkStyles(properties consumed):fontFamilyfontSizefontWeighttextDecorationcolor(supports the sentinel value'inherit', which maps to the newsletter theme'slinkAccent) These are applied to<a>tags in injected HTML; if absent the build falls back totheme.linkAccent.
Notes:
containerStyles.paddingis normalized and written intosection.containerStylesby the build script, but templates in this project currently hard-code padding in their TDs (sopaddingis prepared but not yet consumed by dense templates).headingStylesis defined insection-styles.jsonand the build contains a CSS generator for it, but that generator is not invoked in the current pipeline — so heading-specific keys are not applied today.
How styles are applied (merge & precedence)
- Template defaults —
templates/<template>/section-styles.jsonprovide the base values for each section type. - Color theme — the build process then applies colors from
data/color-themes.json(the selectedcolorThemefor the newsletter) wherebackgroundColoror other color values are intentionally leftnullin the template defaults. - Issue-level frontmatter / JSON — values supplied in the newsletter's frontmatter (or already-converted
data/newsletter.json) can override template defaults. You can provide globalsectionStylesor per-section overrides in YAML/frontmatter. - Per-section / per-item overrides — explicit fields on a section or item in the frontmatter take highest precedence and will be applied on top of the merged defaults.
After merging, the build script (scripts/build-newsletter.mjs) writes the resolved style object into each section as section.containerStyles (and other merged fields) inside data/newsletter.json. Maizzle templates then read section.containerStyles directly when rendering, so templates can safely output inline styles and conditional MSO VML fallbacks for Outlook.
Note about Outlook (MSO) fallbacks
Because some email clients (Outlook) don't support CSS border-radius consistently, the templates include conditional VML fallbacks (e.g. v:roundrect) when a borderRadius is requested. The VML wrapper is emitted only when rounding is non-zero.
Quick-build template discovery
The scripts/quick-build.mjs helper now discovers available templates automatically by enumerating directories under templates/. Use the directory name as the template argument:
node scripts/quick-build.mjs dense-discovery content/my-issue.md
If a template directory is missing or you want to debug discovery set DEBUG_QUICK_BUILD=1 to print what the script found.
Build Process The command:
npx maizzle build production --data data/newsletter.json --inlinecombines templates + data, inlines CSS, and outputs:
build_production/newsletter.html
That HTML file is the final, production-ready email.
- Reusability: one design system, many issues.
- Ease of authoring: write in Markdown, not HTML tables.
- Robust output: inline CSS, responsive tables, Outlook-safe.
- AI-assisted editing: Copilot or other LLMs can help draft content safely within the schema.
- Extensibility: support add-ons later (UTM tagging, dark mode, image optimization).
-
Install dependencies:
npm install
-
Test the complete workflow:
npm run testThis converts the example
content/2025-10-07.md→ JSON → final HTML -
Check your output:
open build_production/newsletter.html
# Duplicate the example file
cp content/2025-10-07.md content/2025-10-14.md
# Edit in VS Code (IntelliSense will help with YAML structure)
code content/2025-10-14.mdEdit the YAML frontmatter in your new .md file:
---
title: "Your Newsletter Title"
hero:
title: "Main headline"
url: "https://your-link.com"
image:
src: "https://your-image-url.com/hero.jpg"
alt: "Alt text for image"
deck: "Subheading description"
cta:
href: "https://your-cta-link.com"
label: "Your CTA Text →"
feature:
title: "Featured article title"
url: "https://feature-link.com"
image:
src: "https://feature-image.com/image.jpg"
alt: "Feature image alt text"
html: |
<p>Your feature content with <strong>HTML formatting</strong>.</p>
<p>Multiple paragraphs supported.</p>
# ... more sections
---Run the new linter before building a template to catch missing sections, malformed items, or schema violations up front:
npm run lint:content content/2025-10-14.md
npm run lint:content data/newsletter.jsonThe script parses your Markdown/JSON frontmatter, validates section/item structure, checks any referenced section-styles file, and runs the template-specific schema (when available). It prints the problematic path for each issue and exits with a non-zero status when the file cannot be safely rendered.
Option A: Use the quick workflow script
# Usage: ./workflow.sh [--content <path>] [--template <name>] [--output <path>] [--send-test|send-test]
./workflow.sh content/2025-10-14.md dense-discovery
# Short-form flags are also available:
./workflow.sh --content content/2025-10-14.md --template dense-discovery --output build_production/custom-output.html
./workflow.sh content/2025-10-14.md dense-discovery --send-testOption B: Run individual commands
# Convert Markdown → JSON
node scripts/md_to_json.mjs content/2025-10-14.md
# Build HTML from JSON
npm run build:dataOption C: Use npm scripts
# This is basically what you want to do
node ./scripts/quick-build.mjs dense-discovery ./data/2025/w43-y25.md
# Test with example content
npm run test
# Preview (builds and opens in browser)
npm run previewYour production-ready HTML email is now at:
build_production/newsletter.html
Copy this entire HTML file and paste it into:
- Sendy
- Mailchimp
- Campaign Monitor
- Any other email service provider
If you already use AWS SES (for example with Sendy), you can fire off a device-ready test directly from this repo and skip the copy/paste step until you are ready for the final blast.
- Verify and authorize
- In the SES console, verify your sender identity (domain or explicit email). While still in the sandbox, also verify the one or two inboxes you use for tests.
- Create an IAM user with programmatic access. Grant it
ses:SendEmailandses:SendRawEmail(the managedAmazonSESFullAccesspolicy is fine for testing) and capture the access key + secret.
- Configure environment variables (inside
.envor your shell):
AWS_REGION=us-west-2
AWS_ACCESS_KEY_ID=AKIA...
AWS_SECRET_ACCESS_KEY=supersecret
SES_FROM=you@yourdomain.com
SES_TO=test1@example.com,test2@example.com
SES_SUBJECT=Newsletter rendering testThe script also respects the default AWS credential chain (~/.aws/credentials, AWS_PROFILE, etc.), so the AWS_* lines are optional if you already have a profile configured.
3. Send the compiled HTML (defaults to workflow-test.html when no path is supplied):
npm run send:test -- build_production/w48-y25.htmlor run the script directly:
node scripts/send-ses-test.mjs build_production/w48-y25.htmlChain it to the workflow for one-touch previews:
./workflow.sh content/2025-10-14.md dense-discovery workflow-test.html \
&& npm run send:test -- workflow-test.htmlnfl-maizzle-mail/
├── content/ # Your newsletter content (Markdown)
│ └── 2025-10-07.md # Example newsletter
├── data/ # Generated JSON data
│ └── newsletter.json # Auto-generated from Markdown
├── build_production/ # Final HTML output
│ └── newsletter.html # Copy this into your ESP!
├── src/
│ ├── components/ # Reusable email components
│ ├── templates/ # Main newsletter template
│ └── layouts/ # Base HTML layout
├── scripts/
│ └── md_to_json.mjs # Markdown → JSON converter
└── newsletter.schema.json # VS Code IntelliSense schema
| Command | Purpose |
|---|---|
npm run test |
Convert example content → build HTML |
npm run build:data |
Build HTML from existing JSON |
npm run convert <file.md> |
Convert specific Markdown file to JSON |
npm run lint:content <file> |
Validate Markdown/JSON frontmatter before building (sections, section-styles, schema) |
npm run preview |
Build and open in browser |
./workflow.sh <file.md> [template] [output-file] |
Complete workflow for specific file, template, and (optionally) output path |
This repository includes a small utility to generate a canonical Markdown sample for a template. It is useful when you want a quick FPO (for-position-only) newsletter that exercises a template's sections and layout.
- Script path:
scripts/generate_md_from_template.mjs - Purpose: inspect a template (or its
section-styles.json) and emit a Markdown file with YAML frontmatter containingintro,header,sections, andfooterpopulated with FPO content and placeholder images.
Usage:
# Generate a sample for one template (defaults to 1 item per section)
node scripts/generate_md_from_template.mjs dense-discovery
# Generate 2 items per section
node scripts/generate_md_from_template.mjs dense-discovery --items 2
# Generate samples for every template in the `templates/` folder (batch)
node scripts/generate_md_from_template.mjs --batch --items 2
# Specify an output path
node scripts/generate_md_from_template.mjs dense-discovery --output generated/my-sample.mdWhat it does
- Detects section types by scanning
templates/<template>/newsletter.htmlforsection.typechecks, falling back totemplates/<template>/section-styles.jsonkeys or sensible defaults. - Emits semantically consistent English FPO copy (no Latin) tailored to each section type; useful for layout testing. You can control how many items a section contains with
--items. - Alternates supplied placeholder images (4x5 and 1x1) to help evaluate different image aspect ratios in the template layout.
- Emits YAML frontmatter using
js-yamlso HTML block scalars are formatted correctly. The generated file default location isgenerated/<template>-sample.mdunless--outputis provided.
Footer and extras
- The generator injects a
footerblock into the frontmatter (email share link, subscribe link, social links, gratitude, address, colophon). If you want theaddressfield emitted as an explicit YAML block scalar with|, say so and the generator can be adjusted to force block-style output.
Dependencies
- The generator uses
js-yamlfor robust YAML serialization. If you haven't already installed dependencies after pulling changes, run:
npm installNotes
- This tool is intentionally heuristic and designed to provide a fast, usable sample. If a template contains a
schema.json, the generator can be extended to prefer that authoritative schema (PR welcome).
✅ Use VS Code - The JSON schema provides autocomplete and validation
✅ Test early, test often - Run npm run test frequently to catch issues
✅ Preview in browser - Use npm run preview to see your email before sending
✅ Keep images optimized - Use appropriately sized images for email
✅ Test with placeholders - Use placeholder images during development
Q: My title shows "undefined"
A: Make sure your Markdown file has proper YAML frontmatter with a title: field
Q: Images aren't showing
A: Check that your image URLs are publicly accessible and use HTTPS
Q: Components aren't rendering
A: Run npm run build:data after making changes to ensure fresh build
Q: JSON schema validation errors
A: Check that all required fields are present in your YAML frontmatter
This system supports multiple email templates that you can switch between or experiment with.
templates/
├── wirecutter/ # Current "recommendation" style template
│ ├── components/ # Template-specific components
│ │ ├── HeaderLogo.html
│ │ ├── Hero.html
│ │ ├── Feature.html
│ │ ├── TwoUp.html
│ │ └── Footer.html
│ ├── layouts/
│ │ └── main.html
│ ├── newsletter.html # Main template file
│ └── schema.json # Template-specific schema
├── newsletter/ # Traditional newsletter template
│ ├── components/
│ ├── layouts/
│ ├── newsletter.html
│ └── schema.json
└── digest/ # News digest template
├── components/
├── layouts/
├── newsletter.html
└── schema.json
# Build with Wirecutter template (current default)
npm run build:wirecutter
# Build with Newsletter template
npm run build:newsletter
# Build with Digest template
npm run build:digest# Convert content with specific template
node scripts/md_to_json.mjs content/my-issue.md --template=newsletter
# Build with specific template
npm run build -- --template=newsletter---
template: "newsletter" # Override default template
title: "My Newsletter"
# ... rest of content
---# Create new template structure
node scripts/create_template.mjs mynewtemplate
# This creates:
# templates/mynewtemplate/
# ├── components/
# ├── layouts/
# ├── newsletter.html
# └── schema.json# Decompose existing email into template
node scripts/decompose_email.mjs existing-email.html mynewtemplate
# This analyzes the HTML and creates:
# 1. Component files for reusable sections
# 2. Schema definition based on content structure
# 3. Sample content file showing expected dataA consistently reliable workflow for decomposing HTML emails into reusable components using multi-strategy analysis:
# Reliable workflow (enhanced heuristics + validation)
npm run decompose:reliable input.html template-name
# Automatic AI analysis (requires OPENAI_API_KEY)
npm run decompose:auto input.html template-name
# Comprehensive analysis (all methods)
npm run decompose:comprehensive input.html template-name# List available workflows
npm run decompose:workflows
# Quick heuristic analysis (basic)
npm run decompose:quick input.html template-name
# Smart AI-powered analysis (manual GPT)
npm run decompose:smart input.html template-name
# Interactive step-by-step analysis
npm run decompose:interactive input.html template-name
# Compare multiple analysis methods
npm run decompose:compare input.html template-name- Multi-Strategy Analysis: Combines enhanced heuristics + semantic analysis + newsletter-specific patterns
- Email Type Detection: Automatically detects newsletter, marketing, transactional, or ecommerce emails
- Confidence Scoring: Provides reliability scores and validation metrics
- Automatic API Integration: Uses GPT-4o when
OPENAI_API_KEYis set - Validation System: Quality checks and recommendations for each decomposition
- Fallback Support: Works without API keys, gracefully handles failures
| Workflow | Speed | Accuracy | Reliability | AI Required |
|---|---|---|---|---|
| Reliable | ⚡⚡ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | Optional |
| Auto | ⚡ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | Yes (GPT-4o) |
| Quick | ⚡⚡⚡ | ⭐⭐⭐ | ⭐⭐ | No |
| Smart | ⚡⚡ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | Manual GPT |
Set up OpenAI API for automatic GPT-4o analysis:
# Set your API key (get from https://platform.openai.com/api-keys)
export OPENAI_API_KEY="your-api-key-here"
# Run automatic analysis
npm run decompose:auto emails/newsletter.html my-template
# Or comprehensive analysis (all methods)
npm run decompose:comprehensive emails/newsletter.html my-templateThe system automatically:
- Detects email complexity and chooses appropriate analysis
- Falls back to heuristics if API fails
- Combines multiple analysis methods for maximum accuracy
- Provides confidence and reliability scores
# Real-world examples:
npm run decompose:quick emails/morning-brew.html morningbrew
npm run decompose:smart emails/hacker-news.html hackernews
npm run decompose:compare emails/product-hunt.html producthuntAll workflows produce:
- Component files (Header, Hero, Content, Footer)
- Template structure (main template + layout)
- JSON schema (data validation)
- Sample data (example content)
- Analysis report (confidence scores)
| Command | Purpose |
|---|---|
npm run templates:list |
Show all available templates |
npm run templates:create <name> |
Create new blank template |
npm run templates:copy <from> <to> |
Duplicate existing template |
npm run templates:set-default <name> |
Set default template |
node scripts/decompose_email.mjs <html> <name> |
Create template from existing email |
Your Markdown files can specify which template to use:
---
template: "newsletter" # Use newsletter template
title: "Weekly Update"
layout: "sidebar" # Optional: layout variant
theme: "dark" # Optional: theme variant
# Newsletter-specific structure
sections:
- type: "header"
logo: "..."
- type: "article-list"
articles: [...]
- type: "footer"
---# In template config
extends: "wirecutter" # Inherit from wirecutter template
overrides:
hero: "custom-hero.html" # Override specific components
colors:
primary: "#ff6b35" # Override theme colors# Mix components from different templates
npm run build -- --template=newsletter --hero=wirecutter --footer=digesttemplates/wirecutter/
├── variants/
│ ├── dark-mode/
│ ├── holiday/
│ └── minimal/
When assisting in VS Code, GitHub Copilot should:
- Understand this is a static email generation pipeline (not a web app).
- Generate or refactor Maizzle components using email-safe table markup.
- Suggest copywriting (headlines, blurbs, CTAs) in Markdown/YAML.
- Help write or maintain the Markdown → JSON converter script.
- Help define or extend the JSON Schema for content validation.
- Maintain Maizzle syntax (
{% include %},{{ variable }}) without breaking the pipeline. - Keep output HTML simple, light, and robust for all email clients.
-
Create or duplicate a Markdown file in
content/:cp content/2025-10-07.md content/2025-10-14.md
-
Edit text, images, and links inside the YAML frontmatter.
-
Run the converter:
node scripts/md_to_json.mjs content/2025-10-14.md data/newsletter.json
-
Build the email:
npx maizzle build production --data data/newsletter.json --inline
-
Preview or paste
build_production/newsletter.htmlinto your ESP.
- Maizzle — email templating, build pipeline, inline CSS.
- gray-matter — parse Markdown frontmatter.
- Node.js — conversion and build scripts.
- JSON Schema + VS Code — validation and autocomplete.
- GitHub Copilot — writing assistant for copy, scripts, and component creation.
Here are a few examples you can paste directly into Copilot Chat:
"I'm building a Markdown-authored, Maizzle-rendered HTML email system. Help me write a Node script that converts Markdown frontmatter into JSON for Maizzle."
"Draft a JSON Schema for my newsletter data model based on this YAML frontmatter structure."
"Write a new Maizzle component for a testimonial quote block that fits into my email's table-based system."
"Suggest alternate headline and subhead copy for my
herosection in a friendly, conversational tone."
"Ensure my Maizzle build command also appends UTM tracking parameters to all URLs."
Copilot (and I) should avoid:
- Using CSS grid, flexbox, or modern web layout in emails.
- Generating JavaScript for the email client.
- Inserting external fonts that aren't email-safe (unless properly embedded).
- Breaking the JSON structure that feeds Maizzle.
- Dark-mode image swap pattern.
- Automated UTM tagging.
- RSS-to-Markdown ingestion (for newsletters that mirror blog content).
- "Issue generator" script that bootstraps a new Markdown file with placeholder content.