Email Template Editor
Technical reference for the Certivant email template editor — a standalone Vite + React app that embeds GrapesJS and Monaco Editor inside the Next.js admin console via iframe and postMessage.
The email template editor lives in apps/email-editor/ as a standalone
Vite + React app. It is embedded inside the Next.js admin console via
an <iframe> and communicates with the parent page through postMessage.
Why a separate Vite app?
GrapesJS and Monaco Editor are browser-only tools designed for Vite-style build pipelines. Running them inside Next.js/Turbopack caused multiple unresolvable conflicts:
- GrapesJS CSS contains
::btn— an invalid pseudo-element that Turbopack's CSS linter rejects (no suppression API exists in Turbopack) - Monaco Editor spawns Web Workers that Turbopack cannot bundle correctly
- Both tools use browser-only APIs that conflict with SSR
Moving them to a dedicated Vite app eliminates all of these issues.
File structure
apps/email-editor/
├── package.json @repo/email-editor
├── tsconfig.json browser target, no Next.js deps
├── vite.config.ts React plugin + vite-plugin-monaco-editor
├── index.html full-viewport, no chrome
└── src/
├── main.tsx React createRoot entry
├── App.tsx GrapesJS init + Monaco modal + postMessage bridge
└── types/
└── postMessage.ts INIT / READY / CHANGE / SAVE protocol
postMessage protocol
All communication between the Next.js parent and the iframe uses a typed
postMessage protocol defined in src/types/postMessage.ts.
Parent → iframe
// Sent once when the iframe signals READY
{ type: "INIT"; html: string; variables: string[]; disabled: boolean }
iframe → parent
{ type: "READY" } // GrapesJS fully initialised
{ type: "CHANGE"; html: string } // canvas mutated (every edit)
{ type: "SAVE"; html: string } // user clicked Apply in code modal
Sequence
1. iframe loads → sends READY (page JS ready)
2. parent → sends INIT (passes current HTML + variables)
3. iframe → inits GrapesJS with the received HTML
4. iframe → sends READY again (GrapesJS fully up)
5. user edits → iframe sends CHANGE on every canvas mutation
6. user clicks Apply in Monaco modal → iframe sends SAVE
Next.js side — GrapesJsEmailEditor.tsx
The Next.js component is a thin iframe wrapper
(apps/web/modules/saas/admin/components/communications/GrapesJsEmailEditor.tsx).
It has zero GrapesJS or Monaco dependencies.
const EDITOR_URL =
process.env.NEXT_PUBLIC_EMAIL_EDITOR_URL ?? "http://localhost:5173";
// Sends INIT when iframe signals READY
// Calls onChange() on CHANGE messages
// Calls onSave() on SAVE messages
Environment variables
| Variable | Where | Purpose |
|---|---|---|
NEXT_PUBLIC_EMAIL_EDITOR_URL | apps/web/.env.local | URL of the Vite app |
VITE_PARENT_ORIGIN | apps/email-editor/.env | Tightens postMessage origin in prod |
Development (.env.local in apps/web/)
NEXT_PUBLIC_EMAIL_EDITOR_URL=http://localhost:5173
Production
NEXT_PUBLIC_EMAIL_EDITOR_URL=https://email-editor.certivant.com
Running locally
# From repo root — starts both servers via Turborepo
pnpm dev
# Or individually
pnpm --filter @repo/email-editor dev # Vite → http://localhost:5173
pnpm --filter @repo/web dev # Next.js → http://localhost:3005
Building for production
pnpm build
# Turborepo builds @repo/email-editor first (turbo.json: @repo/web#build
# dependsOn @repo/email-editor#build), then @repo/web.
Deploy apps/email-editor/dist/ to any static host (Cloudflare Pages,
S3, Netlify). Set NEXT_PUBLIC_EMAIL_EDITOR_URL in your Next.js
production environment to the deployed URL.
Security
- The iframe runs with
sandbox="allow-scripts allow-downloads allow-popups".allow-same-originis intentionally omitted — this prevents the iframe from accessing the parent's cookies or localStorage. - The
postMessagehandler inGrapesJsEmailEditor.tsxvalidatesevent.originagainstEDITOR_ORIGINbefore processing any message. - The
postToParentfunction inApp.tsxusesVITE_PARENT_ORIGIN(defaults to"*"in dev, should be set to the exact Next.js origin in production).
Dependencies
| Package | Version | Purpose |
|---|---|---|
grapesjs | ^0.21.13 | Drag-and-drop email canvas |
grapesjs-preset-newsletter | ^1.0.2 | Newsletter blocks + toolbar layout |
monaco-editor | ^0.55.1 | HTML code editor in the code modal |
vite-plugin-monaco-editor | ^1.1.0 | Bundles Monaco Web Workers correctly |
@vitejs/plugin-react | ^4.3.4 | React JSX transform + HMR |