Commit Graph

3 Commits

Author SHA1 Message Date
louispaulb
0fb9089f4e fix(campaigns/templates): center logos via nested-table pattern
The native-block imageBlock factory was emitting img tags wrapped only
by a td with text-align:center. That doesn't actually center the image
because text-align only affects inline content, and the img has
display:block. The result: top header logo and dark-footer logo were
left-aligned despite the textAlign:"center" prop on the block.

Fix: wrap each img in an inner <table align="<textAlign>"> exactly the
way MJML/Litmus/Mailchimp do it. This is the canonical email-client
pattern that works in Outlook 2007-2019 (which ignores margin:0 auto
on inline tables but respects table align attributes).

Also: the AI converter dumped the entire dark footer band into a
SINGLE htmlBlock with malformed table markup (a stray </td> outside
its row). Split into proper image + text native blocks so:
  1. The logo inherits the new centered nested-table pattern
  2. The URL+copyright text is now individually editable in Unlayer
  3. The {{year}} placeholder is in a text block where it belongs

And one AI hallucination correction: the converter assigned
textAlign:"left" to the top header logo (probably because the
surrounding column had align="left" in the MJML output). Original
design intent was centered — fixed in the spec.

Verified live: both logos (140px top, 120px footer) now render with
align="center" on their nested wrapper table.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 18:45:30 -04:00
louispaulb
0fd1e9f6b5 feat(campaigns/templates): Gemini-powered HTML→native converter
Scales the native-block migration from "one template per manual spec"
to "any compiled .html template, one CLI command, ~5 seconds, ~$0.001
per template" via Gemini Flash semantic interpretation.

Pipeline (ai-convert-to-native.js):
  1. Read existing compiled .html
  2. Send inner body to Gemini Flash with a tight JSON schema
     (block.type ∈ text/image/button/divider/html, plus type-specific
     fields like fontSize/color/padding/href).
  3. AI returns { preheader, ariaLabel, blocks: [...] }
  4. Deterministic emit of a templates-spec/<name>-native.js file —
     no AI-touched markup goes into the final compiled output.
  5. Validation: every {{var}} in source MUST survive into the spec;
     warn loudly if any are dropped (the AI occasionally omits minor
     placeholders like {{year}} in the copyright line).

Why deterministic emit matters:
  Gemini understands SEMANTICS reliably ("this paragraph is the
  greeting, this div is the CTA, this span is a chip") but
  hallucinates DETAILS when generating final HTML. Splitting the
  responsibilities means the AI only outputs structured JSON
  describing the layout, and build-native-template.js produces the
  bytes shipped to recipients.

First conversion: gift-email-fr → gift-email-fr-native
  - 15 blocks identified by Gemini in 3006 tokens (Flash, ~5s).
  - 4 row groups: view-in-browser, white card (intro/chips/CTA/
    footer copy), contact info, dark footer band.
  - 7 text + 1 image + 1 button + 6 html blocks (chips, multi-logo
    strip, brand-logo card, expiry section stay as raw HTML —
    correct, those have no native equivalent).
  - HTML payload: 19,664 bytes vs original 39,913 bytes — **-51%**.
  - One AI omission caught by the new sanity check: {{year}} was
    stripped from the © line in the dark footer. Hand-patched in the
    generated spec. Re-running with stricter prompt should reduce
    that occurrence rate.

Hub preview endpoint now defaults vars.year to current year (matches
the test-send endpoint that already did this), so the sample render
shows "© 2026 TARGO Communications" instead of "©  TARGO ...".

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 18:29:15 -04:00
louispaulb
2919fa86af feat(campaigns/templates): native-block reminder template (proof of concept)
Until now, every Unlayer-edited template stored as a single giant
"Custom HTML" block (~37 KB). The operator couldn't manipulate the
greeting, the CTA, or the expiry badge independently — they had to
edit raw HTML inside one block.

New scripts/build-native-template.js generates matched .json
(Unlayer design tree) + .html (compiled output) from a JS template
spec under scripts/templates-spec/. Each block becomes a separate
entry in the design tree with its own type:
  - 9 text blocks  : greeting, urgency, body, expiry, prorata,
                     Option 2 text, signature, contact, dark footer
  - 2 image blocks : header logo, footer logo
  - 1 button block : the CTA (🎁 {{amount}})
  - 4 html blocks  : view-in-browser, Option 1 chip, brand-logo
                     card, Option 2 chip (kept as raw HTML — too
                     custom for native equivalents)

gift-email-native-reminder-fr ships as the proof of concept:
- Compiled HTML: 30,867 bytes (vs 39,484 for the MJML-compiled
  reminder-fr — saves 22%)
- JSON: 42,274 bytes (essentially same as before, but now broken into
  16 individually-editable blocks instead of 1 monster Custom HTML)

What this unlocks in Unlayer:
- Click any text → font / color / size / padding / alignment in the
  right panel
- Click the CTA → button-specific controls (corner radius, hover
  color, padding)
- Drag-reorder blocks within the email
- Mobile preview reflects each block's responsive defaults
- Save a block to the personal library for reuse in other campaigns

Limitations on the 4 html blocks:
- Chips (Option 1 / Option 2) require raw HTML edit because the
  rounded badge styling has no native equivalent
- Brand-logo strip needs precise inline img widths Unlayer can't set

Once the operator validates rendering across Gmail/Outlook/Apple
Mail, we'll port the rest: gift-email-fr/en + the existing reminder
templates can all migrate using the same build script.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 15:31:15 -04:00