Esposter

Email Personalization

The email editor joins the data flow: merge-field blocks bound to a DatasetReference, survey invite blocks, and per-row personalized HTML export — applying the datasets standard to the email product. Actually sending email is deferred (email sending).

How it works

An email optionally binds one dataset (EmailEditor.datasetReference, stored inside the content blob — every GrapesJS save carries it over since project data doesn't know about it). The bound dataset's columns appear in the block manager as drag-in merge-field blocks ({{columnName}}), and the owner's published surveys appear as styled invite-button blocks (survey invite blocks, shared with the webpage editor). Export compiles the current MJML to HTML once (mjml-get-code), then writes one personalized .html per dataset row with merge fields substituted.

flowchart LR
  BIND["Dataset/ReferencePicker<br/>EmailEditor.datasetReference"] -->|columns| BLOCKS["block manager<br/>merge-field blocks {{col}}"]
  SURV["owner's published surveys"] -->|invite-button blocks| BLOCKS
  BLOCKS --> CANVAS["GrapesJS canvas (MJML)"]
  CANVAS -->|"Export (Portable capability)"| MJML["mjml-get-code → HTML template"]
  BIND -->|dataset.readDataset rows| SUB["substituteMergeFields<br/>per row, HTML-escaped"]
  MJML --> SUB
  SUB -->|"one .html per row"| OUT["File System Access<br/>directory picker"]

Key files

FileRole
app/services/emailEditor/toMergeField.tscanonical {{columnName}} token
app/services/emailEditor/substituteMergeFields.tsper-row substitution, HTML-escaped
app/services/emailEditor/getEmailHtml.tsthe one MJML compile, shared with the web view
app/services/emailEditor/exportPersonalizedHtml.tsreadDataset → one personalized .html per row
app/services/grapesjs/setBlocks.tswholesale block-category re-sync in the block manager
app/composables/grapesjs/useGrapesJsEditor.tsshared GrapesJS init + resource storage adapter (email/webpage)
app/components/Resource/Email/Editor.vueinline Editor blade bridging the live editor onto the email store

Notes

  • Export surfaces through the Portable capability (PortableFormatMap[Email] personalized-HTML export()), rendered by PortableActions in the resource command bar — the live GrapesJS editor is bridged onto the email store so the command-bar export can reach it.
  • Merge-field and survey-invite block categories re-sync wholesale whenever their reactive source changes (bound dataset columns, published surveys) — no per-block bookkeeping.
  • Substituted values are HTML-escaped: merge fields personalize text, never inject markup. Tokens are inserted escaped and substitution matches both raw and escaped token forms, since the canvas entity-encodes special characters (e.g. a "P&L" column) on serialization.
  • Export is fully client-side; a zip dependency was rejected in favour of the File System Access directory picker.
  • Exported files keep app-origin asset urls (/api/resource-assets/…), which resolve only for a request carrying the owner's session cookie — images are blank in a file opened straight off disk. Durable public asset urls come with email sending.
  • The EmailEditor content class name is frozen (registered in JSONClassMap — renaming breaks superjson deserialization of persisted blobs).