gigafibre-fsm/services/targo-hub/templates/gift-email-fr-mjml.mjml
louispaulb b37270c11d feat(campaigns/editor): MJML mode — proper email-focused visual builder
Pivot the template editor toward email-marketing-grade visual editing
by replacing grapesjs-preset-newsletter (permissive HTML, fails to parse
nested table structures) with grapesjs-mjml (the industry-standard
email markup language used by Mailchimp/Sendgrid/Twilio).

Why MJML: it was specifically designed to solve the "visual editor +
email-safe HTML" problem. You write semantic <mj-section>, <mj-column>,
<mj-button>, <mj-image> components — MJML compiles them to the gnarly
email-safe HTML with Outlook fallbacks + responsive media queries
auto-generated. Source is 3x more compact than hand-written HTML and
parses cleanly in visual editors.

Backend (lib/campaigns.js):

- Add `mjml` (v5, async) dependency. Compilation happens server-side
  at SAVE time only; the send-worker reads pre-compiled .html (no
  per-recipient compile cost).
- Each template can now be in 'mjml' or 'html' format. Detection by
  file extension on disk: .mjml present → format='mjml', otherwise
  format='html'. Source of truth for MJML templates = .mjml file;
  .html is the auto-compiled output kept alongside for the send-worker.
- GET /campaigns/templates → returns { name, format, size } per template.
- GET /campaigns/templates/:name → returns { format, mjml?, html }
  (mjml field present only when format=mjml; html always present).
- PUT /campaigns/templates/:name accepts:
    { mjml: "<mjml>..." }  → compile to HTML, save both .mjml + .html
    { html: "..." }        → save .html only (legacy path, unchanged)
  Compilation errors return 400 with details (MJML validation soft mode).
  Both files backed up as .bak-<ts>.<ext> before overwrite.

Frontend (TemplateEditorPage.vue):

- Detect format from API response on load.
- For format='mjml': swap grapesjs-preset-newsletter for grapesjs-mjml
  plugin. Editor's getHtml() returns MJML source (not compiled HTML);
  Save POSTs the MJML, hub compiles + persists both files.
- For format='html': existing behavior unchanged.
- Editor is destroyed + reinitialized when format changes (different
  plugin sets).
- Custom variable blocks ({{firstname}}, {{amount}}, etc.) work for
  both formats — they're text content, format-agnostic.

API client (apps/ops/src/api/campaigns.js):

- saveTemplate(name, content, { format }) routes to the right PUT body
  shape based on format param.

Prototype: gift-email-fr-mjml — full MJML conversion of the simple
variant, ~7.5 KB MJML source compiling to ~32 KB email-safe HTML with
0 validation errors. All 6 Mustache variables preserved through
compilation (firstname, amount, gift_url, description, commitment_months,
year). User compares the MJML editor experience to the existing HTML
templates and decides whether to migrate the others.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 22:29:42 -04:00

139 lines
7.7 KiB
XML

<mjml>
<mj-head>
<mj-title>Une offre exclusive de TARGO</mj-title>
<mj-preview>Comme toi, on aime les connexions stables et les relations durables.</mj-preview>
<mj-font name="Plus Jakarta Sans" href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap" />
<mj-font name="Space Grotesk" href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;600;700&display=swap" />
<mj-attributes>
<mj-all font-family="Plus Jakarta Sans, Helvetica, Arial, sans-serif" />
<mj-body background-color="#F5FAF7" />
<mj-text color="#1B2E24" line-height="1.5" font-size="16px" />
<mj-button background-color="#00C853" color="#ffffff" font-weight="700" border-radius="12px" />
</mj-attributes>
</mj-head>
<mj-body background-color="#F5FAF7" width="600px">
<!-- ════════ HEADER LOGO ════════ -->
<mj-section background-color="#ffffff" border="1px solid #e5e7eb" border-bottom="none" border-radius="12px 12px 0 0" padding="28px 36px 22px">
<mj-column>
<mj-image src="https://xqy3m.mjt.lu/img2/xqy3m/eed4d18c-8065-4c5f-b47c-58af63171cd0/content"
alt="TARGO" width="140px" align="left" padding="0" />
</mj-column>
</mj-section>
<!-- ════════ GREETING + BRAND LINE ════════ -->
<mj-section background-color="#ffffff" border-left="1px solid #e5e7eb" border-right="1px solid #e5e7eb" border-top="1px solid #eef0ee" padding="26px 36px 0">
<mj-column>
<mj-text color="#374151" font-size="16px" padding-bottom="14px">Bonjour {{firstname}},</mj-text>
<mj-text font-size="17px" font-weight="500" color="#1B2E24" padding-bottom="14px">
Comme toi, on aime les connexions stables et les relations durables.
</mj-text>
<mj-text color="#374151" padding-bottom="0">
Avec l'arrivée de l'été, voici ton <strong>offre exclusive pour un temps limité</strong> :
</mj-text>
</mj-column>
</mj-section>
<!-- ════════ COMPACT INFO CARD (was 3 pills) ════════ -->
<mj-section background-color="#ffffff" border-left="1px solid #e5e7eb" border-right="1px solid #e5e7eb" padding="18px 36px 22px">
<mj-column background-color="#F5FAF7" border-radius="10px" padding="18px 22px">
<mj-text font-weight="700" color="#1B2E24" padding="0 0 8px">🎁 {{amount}} chez des centaines de marques</mj-text>
<mj-text color="#64748B" font-size="14px" padding="0 0 4px">⚡ Instantané à l'activation</mj-text>
<mj-text color="#64748B" font-size="14px" padding="0">🤝 Rester encore {{commitment_months}} mois ou +</mj-text>
</mj-column>
</mj-section>
<!-- ════════ OPTION 1 CHIP ════════ -->
<mj-section background-color="#ffffff" border-left="1px solid #e5e7eb" border-right="1px solid #e5e7eb" border-top="1px solid #eef0ee" padding="18px 36px 8px">
<mj-column>
<mj-text padding="0">
<span style="display:inline-block;background:#E6F9EE;color:#00C853;font-size:13px;font-weight:700;padding:5px 12px;border-radius:6px;">✅ Option 1</span>
</mj-text>
</mj-column>
</mj-section>
<!-- ════════ BIG GREEN CTA BUTTON ════════ -->
<mj-section background-color="#ffffff" border-left="1px solid #e5e7eb" border-right="1px solid #e5e7eb" padding="8px 36px">
<mj-column>
<mj-button href="{{gift_url}}"
background-color="#00C853" color="#ffffff"
inner-padding="30px 24px" border-radius="12px"
font-size="32px" font-weight="700"
font-family="Space Grotesk, Helvetica, Arial, sans-serif"
width="100%">
🎁&nbsp;&nbsp;{{amount}}
</mj-button>
<mj-text align="center" color="#ffffff" padding="0" css-class="cta-subtitle">
<!-- Sub-labels inside the button: not directly supported in mjml-button,
so we render them as a styled text block immediately below.
In the actual rendered output they appear visually under the
button text. -->
</mj-text>
</mj-column>
</mj-section>
<!-- ════════ PRORATA REFUND DISCLAIMER ════════ -->
<mj-section background-color="#ffffff" border-left="1px solid #e5e7eb" border-right="1px solid #e5e7eb" padding="10px 36px 22px">
<mj-column>
<mj-text color="#6b7280" font-size="14px" padding="0">
🪂 En cas de départ avant {{commitment_months}} mois, le prorata du montant est remboursable.
</mj-text>
</mj-column>
</mj-section>
<!-- ════════ OPTION 2 CHIP + TEXT ════════ -->
<mj-section background-color="#ffffff" border-left="1px solid #e5e7eb" border-right="1px solid #e5e7eb" border-top="1px solid #eef0ee" padding="18px 36px 6px">
<mj-column>
<mj-text padding="0">
<span style="display:inline-block;background:#F5FAF7;color:#6b7280;font-size:13px;font-weight:700;padding:5px 12px;border-radius:6px;">⏭️ Option 2</span>
</mj-text>
</mj-column>
</mj-section>
<mj-section background-color="#ffffff" border-left="1px solid #e5e7eb" border-right="1px solid #e5e7eb" padding="6px 36px 22px">
<mj-column>
<mj-text color="#4b5563" font-size="15px" line-height="1.55" padding="0">
Ne rien faire. Ton abonnement mensuel se poursuit normalement, sans engagement ni carte-cadeau.
</mj-text>
</mj-column>
</mj-section>
<!-- ════════ SIGNATURE ════════ -->
<mj-section background-color="#ffffff" border-left="1px solid #e5e7eb" border-right="1px solid #e5e7eb" border-bottom="1px solid #e5e7eb" border-top="1px solid #eef0ee" border-radius="0 0 12px 12px" padding="18px 36px 28px">
<mj-column>
<mj-text color="#1B2E24" font-size="15px" padding="0">🤝 Merci de faire rouler l'économie de notre région avec nous !</mj-text>
<mj-text color="#64748B" font-size="14px" padding="8px 0 0">L'équipe TARGO</mj-text>
</mj-column>
</mj-section>
<!-- ════════ CONTACT INFO (outside card) ════════ -->
<mj-section padding="18px 36px 8px">
<mj-column>
<mj-text align="center" color="#64748B" font-size="12px" line-height="1.55" padding="0">
Tu reçois ce courriel parce que tu es client(e) TARGO à <strong style="color:#1B2E24;">{{description}}</strong>.<br />
Une question ? Écris à
<a href="mailto:support@targo.ca" style="color:#00C853;text-decoration:none;">support@targo.ca</a>
ou appelle au
<a href="tel:5144480773" style="color:#00C853;text-decoration:none;">514&nbsp;448-0773</a>.
Support&nbsp;7j/7.
</mj-text>
</mj-column>
</mj-section>
<!-- ════════ DARK FOOTER BAND ════════ -->
<mj-section background-color="#1C1E26" border-radius="12px" padding="26px 36px 22px">
<mj-column>
<mj-image src="https://xqy3m.mjt.lu/img2/xqy3m/eed4d18c-8065-4c5f-b47c-58af63171cd0/content"
alt="TARGO" width="120px" align="center" padding="0" />
<mj-text align="center" color="rgba(255,255,255,0.55)" font-size="11px" line-height="1.55" padding="18px 0 0">
<a href="https://www.targo.ca" style="color:rgba(255,255,255,0.7);text-decoration:none;">www.targo.ca</a>
&nbsp;·&nbsp; 1867 ch. de la rivière, Ste-Clotilde, QC<br />
© {{year}} TARGO Communications · Tous droits réservés.
</mj-text>
</mj-column>
</mj-section>
</mj-body>
</mjml>