Plain-JavaScript functions for generating production-ready HTML email templates — built for the Hackernoon newsletter pipeline and the broader
markdown-to-emailproject.
This section describes the current generator. Everything below it is preserved from earlier phases of the repo (Work/, module split, NX plans). Where those sections are outdated, they carry a short legacy callout. Source for the old Work/outerTemplate/local-packages stack lives under
archive/legacy/. For AI agents: seeAGENTS.md.
A small Node ESM CLI in scripts/ that renders the Hackernoon-style newsletter
as one HTML email file, using the published
@llazyemail/template-runtime-display
package (document() with XHTML_DOCTYPE, xmlns:v / xmlns:o for Outlook).
| Path | Role |
|---|---|
scripts/generate-template.js |
CLI + renderHn() (hn / hn-without-ads) |
scripts/hn-parts.js |
head / preheader / main / footer + HTML/BODY attributes |
scripts/hn-blocks.js |
ads + image grid (table-based) |
scripts/generate-template.test.js |
node:test + lintEmail |
content/ |
Input datasets (content2.js is the default) |
generated/ |
Output HTML |
Requires Node 24 and a GitHub token with read:packages
(@llazyemail/* is on GitHub Packages — see scripts/.npmrc).
cd scripts
export NODE_AUTH_TOKEN=<token with read:packages>
npm ci
npm test
npm run generate
# or:
node generate-template.js --data=../content/content2.js --out=../generated/hn.html
node generate-template.js --helpFlags: --template (hn default, or hn-without-ads — same chrome without the ads/sponsor block), --data, --out, --content (optional HTML body).
| Path | Status | Notes |
|---|---|---|
scripts/, content/, generated/, docs/ |
current | Generator, data, docs |
sub-modules/Typography, innerComponents, Miscellaneous |
maintained (CI) | Standalone component packages |
Work/ |
stub | generate:template → scripts/; source/tests in archive/legacy/Work/ |
archive/legacy/ |
archived reference | Work/src+tests, outerTemplate, local template-* packages — see archive/legacy/README.md |
archive/ (other), files/, src/ |
historical | Early experiments |
template-runtime-display— document shell +lintEmail(used byscripts/)markup-generator— write content into files (legacyWork/path)markdown-to-email— markdown → email pipeline this template was built foremail-template-workflows— reusable CI action for rendering email HTML
AGENTS.md— guide for AI agents (commands, conventions, gotchas)CONTRIBUTING.mddocs/architecture.mddocs/adr/0001-module-boundaries.mddocs/history/— earlier READMEs (README.old.md,read4.md,readme3.md) moved here verbatim
- Current state (start here) — current generator, quick start, layout, ecosystem
- Overview
- Features
- Architecture & How It Works
- Tech Stack
- Repository Structure
- Target Architecture
- Baseline Behavior
- Prerequisites
- Installation & Setup
- Configuration
- Usage
- Workflow
- Development
- Testing & Validation
- Building & Bundling
- Deployment & Publishing
- Troubleshooting / FAQ
- Security & Privacy Notes
- Contributing
- Roadmap
- Related Resources & Articles
- Recent Changes
- About the
outerTemplateModule - Architecture Decisions
- Legacy README
- License
- Acknowledgements
hn_email_template is a modular JavaScript library that assembles complete, production-grade HTML email templates from small, composable function-based components.
HTML email markup is notoriously fragile. It relies on HTML4-era table-based layouts, inline styles, and a maze of compatibility quirks across email clients. When the Hackernoon engineering team needed to automate newsletter generation in the markdown-to-email pipeline, maintaining monolithic template strings became unsustainable.
This project solves that by:
- Breaking a single large HTML template into small, independently-testable function components
- Separating outer structure (head, body wrapper, footer) from inner content (articles, sponsor blocks, etc.)
- Providing a clear display pipeline so each section can be rendered and previewed in isolation
- Gradually migrating toward a TypeScript / NX monorepo structure for long-term maintainability
| Feature | Details |
|---|---|
| Composable components | Each email section (head, body, footer, main, content) is a standalone JS function |
| Display pipeline | A runDisplayPipeline abstraction maps raw data → validated model → rendered HTML string |
| Input validation | Structured validation rules throw descriptive errors on missing required fields |
| Multiple output formats | Bundled as CJS, ES module, and IIFE via Rollup |
| Jest test suite | 13 unit tests + integration tests covering every component and display section |
| ESLint + Prettier | Enforced code style with pre-commit hooks via Husky and lint-staged |
| Renovate bot | Automated dependency update PRs |
| NX / TypeScript workspace | Active TypeScript migration in the hackernoon/ directory |
| Gitpod ready | One-click cloud development environment via .gitpod.yml |
Note: legacy — describes the Work/ display pipeline (mapper → model → display). Current flow:
scripts/generate-template.js→ runtimedocument(); see Current state.
The template is split into two conceptual layers:
┌─────────────────────────────────────────────────────────┐
│ Full Email Template │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌────────┐ │
│ │ Head │ │ Main │ │ Body │ │ Footer │ │
│ └──────────┘ └──────────┘ └──────────┘ └────────┘ │
│ Outer Template │
├─────────────────────────────────────────────────────────┤
│ Inner Content │
│ ┌─────────────┐ ┌──────────┐ ┌───────────────────┐ │
│ │ Articles │ │ Sponsors │ │ Preview Text │ │
│ └─────────────┘ └──────────┘ └───────────────────┘ │
└─────────────────────────────────────────────────────────┘
Each section follows the same three-step pipeline defined in Work/src/display/core/:
Raw Input Data
│
▼
mapper.js ← maps raw input to a normalised internal structure
│
▼
model.js ← validates the structure; throws on missing required fields
│
▼
display.js ← renders the HTML string from the validated model
│
▼
HTML String
createDisplaySection.js wraps this three-step pattern, and runDisplayPipeline.js orchestrates running it across all sections.
renderTemplate('hn', { string, data }) in sub-modules/outerTemplate/src/templates/hn.js is the primary public API. It accepts a content string (or a { string, data } object) and wraps it in the full Hackernoon outer template, producing a ready-to-send HTML email string.
Note: legacy — lists the Work/ Rollup/Babel/Jest toolchain. The current generator is plain Node ESM (
scripts/) with@llazyemail/template-runtime-display; see Current state.
| Tool / Library | Role |
|---|---|
| Node.js | Runtime |
| Rollup | Module bundler (CJS, ES, IIFE outputs) |
| Babel | ES2015+ transpilation |
| Jest | Unit and integration testing |
| ESLint | Static analysis |
| Prettier | Code formatting |
| Husky + lint-staged | Pre-commit hooks |
| Renovate | Automated dependency updates |
| NX | Monorepo tooling (TypeScript migration workspace) |
| Lodash | Utility helpers |
| atherdon-newsletter-js-layouts-* | Published npm packages for body, typography, misc layouts |
| email-template-object | Shared email template object model |
| html-typography-tags | HTML typography helpers |
Note: partially outdated — tree below mixes current and legacy paths. Prefer the layout table in Current state. Legacy dirs are being removed in separate PRs.
Note:
Work/is the current runtime root and is transitional/deprecated. New top-level directories (content/,scripts/,src/,tests/,generated/,docs/) have been introduced as part of the reorganization. See Target Architecture anddocs/architecture.md.
hn_email_template/
├── .deepsource.toml # DeepSource code quality config
├── .gitignore
├── .gitpod.yml # Gitpod cloud dev environment config
├── package-lock.json # Root lockfile
├── README.md # This file
├── README.old.md # Preserved legacy README (historical reference)
│
├── .github/
│ └── workflows/
│ ├── codeql-analysis.yml # CodeQL security scanning
│ ├── eslint.yml # ESLint CI check
│ ├── node.js.yml # Node.js CI (build & test)
│ └── npm-publish.yml # NPM publish workflow
│
├── content/ # All newsletter content datasets (new canonical location)
│ ├── content1.js # Canonical template dataset
│ ├── content2.js # HN JSON-authored variant
│ ├── content3.js # Markdown-derived variant
│ └── data-markdown.js # Body content blocks array
│
├── scripts/ # Generation scripts (new canonical location)
│ └── generate-template.js # Top-level template generator
│
├── src/ # Rendering/business logic (migration target from Work/src/)
│ └── README.md # See for migration status
│
├── tests/ # Test suites (migration target from Work/tests/)
│ └── README.md # See for migration status
│
├── generated/ # Generated HTML outputs (gitignored)
│
├── docs/ # Project documentation & ADRs
│ ├── README.md
│ └── architecture.md # Target architecture & baseline behavior
│
├── Work/ # ⚠️ DEPRECATED transitional runtime root
│ │ # Do not add new core logic here. Will be removed
│ │ # once the top-level migration is complete.
│ ├── package.json # Package metadata & scripts (v3.9.0)
│ ├── jest.config.js # Jest configuration
│ ├── rollup.config.js # Rollup bundler configuration
│ ├── renovate.json # Renovate bot config
│ │
│ ├── bash/
│ │ ├── lint-fix.sh # Run ESLint auto-fix
│ │ ├── tests.sh # Run test suite
│ │ └── update-packages.sh # Update npm packages
│ │
│ ├── scripts/
│ │
│ ├── src/
│ │ ├── index.js # Public API exports
│ │ ├── config.js # Shared constants (contact URL, mailing address)
│ │ ├── data-markdown.js # Body content from content-from-markdown.html (lines 30–225)
│ │ ├── factory.js # Display factory class
│ │ ├── methods.js # Top-level print* helper functions
│ │ │
│ │ ├── components/ # Low-level HTML component functions
│ │ │
│ │ ├── templates/ # Template rendering system
│ │ │
│ │ └── display/ # Display pipeline (mapper → model → display)
│ │
│ └── tests/
│ ├── unit/ # Unit test files (one per component/section)
│ └── integration/ # End-to-end and integration tests
│
├── files/ # ⚠️ DEPRECATED — re-exports to content/ (backward compat)
│ ├── data.js # → re-exports content/content1.js
│ ├── data-hn.js # → re-exports content/content2.js
│ └── data-from-markdown.js # → re-exports content/content3.js
│
├── sub-modules/ # Standalone reusable sub-packages
│ ├── Typography/ # Typography HTML rendering module
│ ├── innerComponents/ # Inner email components module
│ ├── Miscellaneous/ # Miscellaneous utilities
│ └── outerTemplate/ # Outer template module
│
├── packages/ # Published npm packages
│ ├── template-engine/
│ ├── template-presets-hn/
│ └── template-runtime-display/
│
└── archive/ # Archived legacy files (historical reference)
Note: legacy — reorganization plan from the module-split phase. For the live picture see
docs/architecture.md(Current section) and Current state.
See
docs/architecture.mdfor the full migration plan.
The project is being reorganized toward a clean top-level structure:
| Directory | Purpose |
|---|---|
content/ |
All newsletter content datasets (canonical location) |
src/ |
Rendering and business logic (migrating from Work/src/) |
scripts/ |
Generation and tooling scripts (migrated from Work/scripts/) |
tests/ |
All test suites (migrating from Work/tests/) |
generated/ |
Generated HTML outputs (gitignored) |
docs/ |
Project documentation and ADRs |
Work/ will remain as a transitional runtime root until the migration is complete,
then be removed. No new core logic should be added to Work/.
Note: legacy —
Work/generate/test commands. Current verification:cd scripts && npm testandnpm run generate(Node 24 +NODE_AUTH_TOKEN).
Before the reorganization, generation used files/data-hn.js as the default
content source and output to Work/generated/. These paths are still valid
(via backward-compat re-exports in files/), but the canonical commands are now:
cd Work
# Generate using the canonical HN dataset (content2):
npm run generate:template -- --data=../content/content2.js --out=generated/hn.html
# Generate using the markdown-derived dataset (content3):
npm run generate:template -- \
--data=../content/content3.js \
--content=src/content-from-markdown.html \
--out=generated/hn-markdown.htmlOr using the new top-level script from the project root:
node scripts/generate-template.js --data=content/content2.js --out=generated/hn.htmlRun tests to verify behavior is unchanged:
cd Work && npm testNote: partially outdated — for the current generator you need Node 24 and a GitHub Packages token (
read:packages). See Quick start (current).
- Node.js >= 18 (18.x or 20.x; these are the versions tested in CI)
- npm >= 7 (workspaces support)
- Git
The NX workspace under
hackernoon/additionally requires TypeScript >= 4.9 (installed as a dev dependency).
Note: legacy — steps below install
Work/. Current install: Quick start (current).
git clone https://github.com/LLazyEmail/hn_email_template.git
cd hn_email_templatecd Work
npm installcd hackernoon
npm installOpen the repo in gitpod.io:
https://gitpod.io/#https://github.com/LLazyEmail/hn_email_template
The .gitpod.yml is pre-configured to install dependencies and start the dev watcher automatically.
Note: legacy — Work/
src/config.jsand related. Current HN attributes live inscripts/hn-parts.js.
All shared constants live in Work/src/config.js:
export const config = {
contact: 'https://sponsor.hackernoon.com/newsletter?ref=noonifications.tech',
mailingAddress: 'PO Box 2206, Edwards CO, 81632, U.S.A.',
unsubscribe: '#', // Set to your real unsubscribe URL in production
};There are no required environment variables for local development. If you extend the project (e.g., to send emails or integrate with an API), consider adding a .env file and using a library like dotenv — but make sure to add it to .gitignore and never commit secrets.
Note: legacy — Work/ API (
renderTemplate, section printers). Current CLI:node scripts/generate-template.js --help(see Quick start).
import { renderTemplate } from 'atherdon-old-newsletter-js-outertemplate';
// or from local source:
// import { renderTemplate } from 'atherdon-newsletter-js-layouts-outertemplate';
// Render with a plain HTML string as body content
const html = renderTemplate('hn', '<p>Hello, world!</p>');
// Render with structured data
const html = renderTemplate('hn', {
string: '<p>Hello, world!</p>',
data: { title: 'My Newsletter Issue #1' },
});
console.log(html); // Full HTML email string ready to sendimport { printMain, printFooter, printBody } from 'atherdon-old-newsletter-js-outertemplate';
const mainHtml = printMain(); // Renders the main outer wrapper
const footerHtml = printFooter(); // Renders the footer section
const bodyHtml = printBody(); // Renders the body sectionThe display pipeline throws descriptive errors on invalid input, for example:
// Missing required `title`
// → Error: `title` is a required option for `renderTemplate`
// Missing required `bodyContent`
// → Error: `bodyContent` is a required option for `renderTemplate`content/data-markdown.js (previously Work/src/data-markdown.js) contains the
newsletter body content extracted from Work/src/content-from-markdown.html
(lines 30–225) as an ordered JavaScript array of typed blocks. Each block has a
type field ("heading", "image", or "text") plus type-specific fields
(html, src/link/alt). The array preserves the original document order
and can be used for rendering comparison, content inspection, or as a data source
for custom renderers.
To generate a full email template using the markdown-derived data and content:
cd Work
npm run generate:template -- \
--data=../content/content3.js \
--content=src/content-from-markdown.html \
--out=generated/hn-markdown.htmlNote: legacy — Work/-centric edit/build/test loop. Current loop is edit
scripts/→npm test→npm run generate.
The intended end-to-end workflow for the Hackernoon newsletter pipeline is:
Markdown article content
│
▼
markdown-to-email (sibling repository)
│ converts Markdown → HTML inner content string
▼
renderTemplate('hn', { string: innerContentHtml })
│ wraps inner content in the full outer template
▼
Full HTML email string
│
▼
Email service provider (Mailchimp, SendGrid, etc.)
The separation between inner content and outer template means that the two parts can evolve independently, and the outer template can be tested without any real article content.
Note: legacy — Rollup watcher, Work/ lint/format/husky. Sub-modules still have their own lint/build/test; the generator itself needs no Rollup build.
cd Work
npm run devThis starts Rollup in watch mode — any change to a source file triggers an incremental rebuild.
# Check for lint errors
npm run lint
# Auto-fix lint errors
npm run lint:fixOr use the convenience shell scripts:
bash bash/lint-fix.sh# Check formatting
npm run format:check
# Apply formatting
npm run formatHusky and lint-staged are configured to run ESLint + Prettier automatically on every git commit for files under src/**/*.js.
Note: legacy — Work/ Jest suites and fixtures. Current tests:
cd scripts && npm test(includeslintEmail). Work/ tests are being removed separately.
Tests live in Work/tests/ and are split into unit and integration suites.
cd Work
npm test
# or
bash bash/tests.shnpm run test:unitnpm run test:integrationnpm run test:template
# runs tests/integration/template.test.js| Directory | Contents |
|---|---|
tests/unit/ |
13 files — one per component / display section |
tests/integration/ |
End-to-end template rendering test |
Key unit test files:
mainComponent.unit.test.jsheadComponent.unit.test.jsbody.unit.test.jsfooter.unit.test.jsdisplayHead.unit.test.js,displayBody.unit.test.js,displayFooter.unit.test.js,displayMain.unit.test.js,displayContent.unit.test.jstemplates.unit.test.jsvalidation.unit.test.jscreateDisplaySection.unit.test.js
The repository ships a small set of content fixtures under Work/fixtures/ — JSON files that each represent a different newsletter payload. Running the fixture generator renders the same template for every fixture and writes one HTML file per fixture to Work/generated/fixtures/.
| Fixture | Description |
|---|---|
default.json |
Full payload matching the default src/data.js (title, preview, ads, images) |
minimal.json |
Bare-minimum payload — title and preview only, no ads or images |
no-images.json |
Ads included but no images — validates layout with a missing images array |
cd Work
npm run generate:fixtures
# outputs: generated/fixtures/default.html minimal.html no-images.htmlOpen any of the generated files in a browser and compare side-by-side.
- Create a new JSON file in
Work/fixtures/, e.g.Work/fixtures/my-variant.json:
{
"title": "My Variant Title",
"preview": "Short preview text for this variant.",
"ads": [],
"images": []
}-
Run
npm run generate:fixtures— the new fixture is picked up automatically (no code changes needed). -
Open
Work/generated/fixtures/my-variant.htmlin a browser to review.
The Node.js CI workflow runs npm run generate:fixtures on every push/PR (Node 20 only) and uploads all generated HTML files — including fixture renders — as a single artifact named generated-html-comparison. To download it:
- Open the workflow run in the GitHub Actions UI.
- Scroll to Artifacts at the bottom of the run summary.
- Click
generated-html-comparisonto download the ZIP. - Unzip and open the
.htmlfiles in a browser to compare renders across fixtures.
The CI job summary also lists every fixture that was rendered in that run.
Note: legacy — Work/ Rollup build. The current generator is run with Node; no bundle step. Sub-modules still
npm run buildin CI.
The project uses Rollup to produce three output formats from Work/src/index.js:
| Format | Output file | Use case |
|---|---|---|
| CommonJS | dist/index.cjs.js |
Node.js require() |
| ES module | dist/index.es.js |
Modern bundlers (webpack, Vite) |
| IIFE | dist/index.iife.js |
Direct <script> tag in browser |
cd Work
npm run build # clean + bundle all formats
npm run bundle # bundle only (no clean step)
npm run clean # remove dist/ and coverage/Note: legacy — npm publish of Work/ / sub-modules. The current generator is not published as a package; CI uploads
generated/hn.htmlas an artifact.
The package is published to npm as atherdon-old-newsletter-js-outertemplate.
Publishing is automated via the .github/workflows/npm-publish.yml workflow, which triggers on releases. To manually publish:
cd Work
npm publishThe
publishConfiginpackage.jsonsets"access": "public"so the package is published publicly.
Note: mostly legacy — Work/ / Babel / Husky issues. For the current generator, a 401/404 on
npm ciinscripts/usually means missingNODE_AUTH_TOKEN(seescripts/.npmrcand AGENTS.md).
Make sure you have installed dependencies inside the Work/ directory specifically:
cd Work && npm installThe root package-lock.json is separate from Work/package-lock.json.
If you cloned the repo in a CI environment or with --no-verify, Husky may not be initialized. Run:
cd Work && npm run prepareThe prepare script is CI-aware and will skip Husky when the CI environment variable is set.
ESLint plugins are resolved relative to the Work/ directory. Always run eslint from Work/, or use the provided npm scripts / shell scripts which set the correct --resolve-plugins-relative-to flag.
Ensure you are running tests from the Work/ directory where jest.config.js and the Babel preset (babel-preset-react-app) are installed:
cd Work && npm test- Create a folder under
Work/src/display/sections/<sectionName>/with four files:<sectionName>.display.js,<sectionName>.mapper.js,<sectionName>.model.js, andindex.js. - Follow the mapper → model → display pattern used by existing sections.
- Export the HTML string from
index.jsand import it inWork/src/methods.js. - Add a corresponding unit test in
Work/tests/unit/<sectionName>.unit.test.js.
- No secrets should ever be committed. The
.gitignoreexcludesnode_modules/but you should add.envfiles to it before storing any API keys or credentials. - The
config.unsubscribefield defaults to'#'— always replace this with a real unsubscribe URL in production emails. Sending emails without a working unsubscribe link may violate CAN-SPAM, GDPR, and similar regulations. - The
contactURL inconfig.jspoints to a Hackernoon-specific sponsorship page — update this if you fork the project for a different newsletter. - CodeQL security scanning runs automatically on every push via
.github/workflows/codeql-analysis.yml. - This project generates HTML strings — always sanitize any user-supplied content before passing it to the template to prevent XSS injection in rendered emails. See the validation & sanitization tools below for suitable libraries.
html-validatehtml-validator-clisanitizercommon-tagshtmljs-parserparse5@tehshrike/html-template-tag
Prefer small draft PRs. Current code lives under
scripts/(and maintained sub-modules). Do not add new logic toWork/or other legacy dirs unless the task is their removal. See AGENTS.md.
Contributions are welcome! See CONTRIBUTING.md for the full guide.
Quick reference:
- Fork the repository and create a feature branch.
- Add new code in the correct module — see the table in CONTRIBUTING.md § Where to Add New Code.
- Run tests and make sure they pass:
cd Work && npm test - Run the linter and fix any issues:
cd Work && npm run lint:fix - Open a Pull Request against
mainwith a clear description.
Note: legacy policy — still historically accurate for the Work/ phase;
Work/itself is being removed.
Work/is integration and orchestration only — no new core logic.
New template definitions, display sections, and component functions must live in the appropriate package (sub-modules/outerTemplate, packages/template-runtime-display, etc.), not in Work/src/. This rule is documented in ADR 0001 and enforced by the work-policy CI check.
See CONTRIBUTING.md for full details on what belongs where.
Note: partially outdated — several migration items are done or superseded by the runtime-display generator. Remaining cleanup is removal of legacy dirs in separate PRs.
These are ideas and known areas for improvement — not commitments.
- Complete the TypeScript / NX migration in
hackernoon/ - Add a live HTML preview mode for local development
- Document each sub-module in
sub-modules/individually - Add Storybook or similar component explorer for email components
- Expand integration test coverage with snapshot tests
- Add a CLI tool for quick template rendering from the command line
- Publish NX workspace packages to npm under a
@llazyemailscope
- markdown-to-email — sibling project that feeds content into this template
- LLazyEmail LinkedIn
- DeepSource Code Quality Dashboard
- 5 Reasons Why Newsletters Should Be Part of Your Business Strategy
- Organizing an Advanced Structure for HTML Email Template
- How I Started to Build React Components for Email Templates
- Introducing a Simple npm Module with Email Templates
- Glossary for Non-Techies
- Email Marketing and How to Curate an Effective Business Newsletter
- Exploring Substack for Building Your Newsletter
- Building a Design System for Email Templates (React)
- Together4Victory: List of Email Marketing Tools
- Cool Newsletters for Developers — Part 1
- Cool Resources for Sending Emails
Note: the March 2026 list below is historical. More recent work moved generation to
scripts/+@llazyemail/template-runtime-display(see Current state).
- Template Modularization: Refactored the codebase to modularize the template system. Introduced
outerTemplate,display runtime,HN preset definitions, andtemplate-engineworkspace packages to enhance code maintainability. - Template Definition Updates: Moved
hn-without-adsand HN template definitions, along with their assembly logic, to the newouterTemplateruntime, improving isolation between data, definitions, and generation. - Testing & Artifacts: Added integration tests using real data, generating and committing verified HTML outputs for visual and functional validation.
- Validation & CLI: Introduced robust template input schema validation and a new generator CLI for automation and error reduction.
- File & Package Structure: Created new folders and packages for
outerTemplate,display runtime, HN definitions, and utility modules. - Documentation: Updated README and documentation with new architectural details and a roadmap for further development.
- NPM Packaging: Updated
.npmignoreto properly exclude generated artifacts and ensure clean npm releases.
Note: legacy —
sub-modules/outerTemplateis being removed. Current outer shell is assembled inscripts/hn-parts.jsvia runtimedocument().
The outerTemplate module is responsible for encapsulating the structure, definitions, and static assembly logic for email templates (such as "HN" and "hn-without-ads"). This module organizes all static aspects of template construction, providing a clear separation from dynamic, data-driven, or rendering-specific logic. While outerTemplate holds most or all static components, actual HTML rendering and runtime processing may also involve related modules like display runtime and template-engine. This modular approach enhances maintainability and makes each concern explicit within the repository's structure.
ADR 0001 still applies as module-boundary guidance. The local
packages/*split it describes is being replaced by the published@llazyemail/template-runtime-displaypackage.
Architecture Decision Records (ADRs) capture significant design choices made during the evolution of this project. They are stored in docs/adr/.
| ADR | Title | Status |
|---|---|---|
| 0001 | Module Boundaries and Dependency Direction | Accepted |
The repository is structured around four primary areas of responsibility:
| Area | Owns |
|---|---|
sub-modules/outerTemplate |
Outer-shell layout components, template registry, and template definitions |
packages/template-runtime-display |
The three-step display pipeline (mapper → model → renderer) and all section implementations |
packages/template-engine |
Generic template factory utilities, shared types, and input validation helpers |
Work/ |
Integration tests, CLI scripts, build configuration, and sample fixture data — no core logic |
Dependency direction: Work/ may depend on any package or sub-module. Packages and sub-modules must never import from Work/. Lower-level packages (template-engine) must not import from higher-level ones (template-runtime-display, outerTemplate).
See docs/adr/0001-module-boundaries.md for the full rules, migration guidance, and acceptance criteria.
The original README is preserved at docs/history/README-original.md for historical reference. It contains the original project notes, architecture sketches, and early development narrative written during the initial build phase. No content has been changed.
This project is licensed under the MIT License. Refer to the "license": "MIT" field in Work/package.json.
- Arthur Tkachenko — creator and primary author
- The Hackernoon engineering team for the original email pipeline
- RollupJS, Jest, NX communities
- All contributors who submitted issues, PRs, or articles about the project