Boilerplate email theme for the gatewaze newsletters / events / calendars modules. Cloned per-newsletter on creation; rendered editions are written back to the cloned repo's publish branch.
This repo follows the react-email project layout, seeded with the Barebone demo templates. Three coexisting authoring paths:
- React-Email starter templates (
emails/*.tsx) — full-edition TSX templates ported from react-email's Barebone demo. Operators preview them locally withpnpm email:devand copy any of them as the starting point for a new edition. - React-email block registry (
manifest.json→enabled_blocks) — declarative manifest enabling the platform's react-email block components (Header / ContentSection / AiContentSection / Footer / Heading / Text / Button) for newsletters cloning this boilerplate. This is the path the editor's slash-command palette uses today. - Mustache (
source.html, legacy) — single file marked up with the templates parser's WRAPPER / BLOCK / BRICK comment grammar. Any block authored here ends up in the email block library asrender_kind='mustache'. Still fully supported for the original gatewaze authoring workflow.
Install once:
pnpm installRun the react-email dev server:
pnpm email:devOpen http://localhost:3000 to browse the eight starter templates with hot reload. Each template's PreviewProps block (at the bottom of its TSX file) supplies the data passed to the component during preview — adjust them as needed.
Static assets (logo, social icons, hero imagery) live under static/ and are served at /static/... by the dev server. See static/README.md for layout.
┌─────────────────────────────────────────────────────────────────┐
│ Operator creates a newsletter "Daily Digest" │
└────────────────────────────┬────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────┐
│ Platform clones gatewaze-template-email │
│ → bare repo at /var/gatewaze/git/newsletter/<id>.git │
│ → row in gatewaze_internal_repos with host_kind='newsletter' │
└────────────────────────────┬────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────┐
│ Reads manifest.json → `enabled_blocks` populates the editor │
│ slash-command palette (Header / Content / Footer / …). │
│ `starter_templates` exposes the Barebone TSX files as │
│ "start from template" presets in the new-edition flow. │
└────────────────────────────┬────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────┐
│ Editor edits "May 8" edition; clicks Publish │
└────────────────────────────┬────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────┐
│ Server renders the edition via `await render(<EditionEmail/>)` │
│ Commits to `publish` branch: │
│ editions/<edition_id>.html ← inlined-CSS email-safe HTML │
│ editions/<edition_id>.json ← raw block tree │
└─────────────────────────────────────────────────────────────────┘
gatewaze-template-email/
├── manifest.json ← which platform-registry blocks the editor exposes,
│ + `starter_templates` listing the TSX files in emails/
├── emails/ ← React-Email starter templates (Barebone-style)
│ ├── theme.ts ← shared Tailwind config (colors, fontScale, mobile variant)
│ ├── theme-fonts.tsx ← Inter @font-face + <Font> fallback registration
│ ├── welcome.tsx ← each TSX file is a complete edition: <Html><Body>…</Body></Html>
│ ├── activation.tsx
│ ├── password-reset.tsx
│ ├── feature-announcement.tsx
│ ├── product-update.tsx
│ ├── subscription-confirmation.tsx
│ ├── subscription-update.tsx
│ └── text-only.tsx
├── static/ ← logo, social icons, hero imagery served by `email dev`
├── content/ ← Mustache content fixtures (legacy path)
├── wrappers/default.html ← declarative page chrome (header + footer)
│ applied to every edition via EditionEmail's
│ <slot name="body" />
├── source.html ← Mustache wrapper + blocks (legacy path)
├── theme.json ← Mustache theme tokens (legacy path)
├── package.json
└── tsconfig.json
manifest.json is the source of truth for what the editor exposes:
enabled_blocks— IDs of platform-registry blocks that should appear in the slash-command palette. The platform supplies the implementation; this file just opts in.starter_templates— full-edition TSX files inemails/. The platform reads this list to populate the "start from template" picker when an operator creates a new edition.publish_branch— branch name where rendered editions are committed (defaultpublish).publish_layout— pathname template foreditions/<id>.htmlandeditions/<id>.json.
Per-tenant TSX components committed directly to emails/ are surfaced today as starter templates (full editions). Phase 2 will add runtime TSX compilation so individual block-level components committed by tenants can also flow into the slash-command palette.
Header and footer chrome for every edition is defined declaratively in
wrappers/default.html. The templates module ingests it into
templates_wrappers; EditionEmail renders the edition body inside the
wrapper's <slot name="body" />. Per-brand URLs and copy live in the markup
directly — there is no separate config layer.
<!-- wrappers/default.html (sketch) -->
<!-- SCHEMA: {
"edition": {
"date": { "type": "text", "label": "Edition date" },
"view_online_link": { "type": "text", "label": "View Online URL" }
}
} -->
<Section>
<Text if="edition.date">{{edition.date}}</Text>
</Section>
<slot name="body" />
<Section>
<Text>Footer copy goes here.</Text>
</Section>- Same declarative grammar as blocks. Allowlisted tags (
Section/Row/Column/Text/Heading/Link/Img/Button/Hr+ plain inert tags) compile to react-email components at render time. - Per-brand values are baked into the markup. Operators fork this file in
their newsletter's own repo and replace the placeholder text + URLs with
their brand. (Earlier versions kept those in
wrapper.json— removed; the template HTML is now the single source of truth.) - Body slot. Exactly one
<slot name="body" />per wrapper marks where the rendered edition blocks go. - Runtime values via mustache.
{{edition.date}},{{edition.view_online_link}},{{edition.title}},{{edition.preheader}}are substituted per edition byEditionEmail. Useif="edition.<field>"to hide chrome that depends on a missing runtime value (e.g. the published page suppresses the View Online link).
A newsletter cloned from this boilerplate gets wrappers/default.html out of
the box; the operator edits the file in their newsletter's repo to brand it.
- Edit
emails/theme.ts— swap thecolorsandfontScalevalues, or add new design tokens. The Tailwind plugin auto-generatesfont-{step}utility classes for every key infontScale. - Replace the Inter fallback URLs in
emails/theme-fonts.tsxwith your brand font. - Drop your logo + social icons into
static/shared/(seestatic/README.md). - Run
pnpm email:devto verify in the local preview before pushing.
- Create
emails/<name>.tsxfollowing the Barebone pattern: importBody / Container / Tailwind / …from@react-email/components, wrap the email body in<Tailwind config={barebonesBoxedTailwindConfig}>, and export both a named export andexport default. - Add a
<Component>.PreviewProps = {…} satisfies <Props>block at the bottom for local preview. - Append an entry to
manifest.json→starter_templates.templates. - Push. Newsletters cloned from this boilerplate will see it on their next manifest fetch.
The platform owns the implementation of header / content_section / ai_section / footer / heading / text / button. To enable a new one for newsletters cloning this boilerplate, add it to manifest.json → enabled_blocks. To author a new block that the platform doesn't ship yet, see the platform's gatewaze-modules/modules/newsletters/admin/components/puck/email-blocks/ registry — that's where the component lives until Phase 2 lifts arbitrary tenant blocks out of the platform monorepo.
source.html is parsed by the templates module (<!-- WRAPPER:key --> / <!-- BLOCK:key --> / <!-- BRICK:key --> markers + <!-- SCHEMA:{...} --> JSON Schema). See the Marker grammar reference below.
<!-- WRAPPER:key | name=... -->
<!-- META:slot_key -->...<!-- /META:slot_key -->
<!-- SCHEMA:{ JSON Schema } -->
...HTML...
<!-- /WRAPPER:key -->
<!-- BLOCK:key | name=... | description=... | has_bricks=... | sort_order=N -->
<!-- SCHEMA:{ JSON Schema } -->
<!-- DATA_SOURCE:{ ...optional adapter config... } -->
...HTML...
<!-- /BLOCK:key -->
<!-- BRICK:key | name=... | sort_order=N --> (nested inside a BLOCK with has_bricks=true)
<!-- SCHEMA:{ JSON Schema } -->
...HTML...
<!-- /BRICK:key -->
Field interpolation uses Mustache:
{{field}}— escaped string from the editor's content.{{{field}}}— unescaped HTML (use with care; only forformat: 'html'fields).{{>page_body}}— wrapper slot for the composed block list.{{#bricks}}{{> brick_key}}{{/bricks}}— repeating bricks inside a block.{{#field}}…{{/field}}— conditional rendering when field is truthy.
See gatewaze-modules/modules/templates/lib/parser/markers.ts for the parser implementation.
- The Barebone templates target Gmail, Outlook (modern), Apple Mail, Yahoo, ProtonMail. They use react-email's table-based primitives (
Section / Row / Column) so Outlook on Windows renders cleanly. - The Tailwind utilities are inlined to per-element styles by
@react-email/renderat publish time — no external stylesheet ships in the email body. - The
mobile:variant intheme.tsadds a@media (max-width: 600px)rule that all major email clients respect.
Starter templates adapted from react-email's Barebone demo (MIT). The Inter font fallback URLs and font-scale tokens are imported as-is.
Apache-2.0.