#!/usr/bin/env node 'use strict' /** * convert-html-to-unlayer.js — one-time converter from our existing compiled * .html templates into Unlayer design JSON. Run after MJML→Unlayer migration * so the visual editor loads existing templates instead of starting blank. * * Strategy: wrap the entire HTML body content in a single "Custom HTML" block * inside a minimal Unlayer design. This is the MIN VIABLE conversion — the * template renders correctly in the canvas, the user can edit the HTML * directly, and they can incrementally replace the HTML block with native * Unlayer blocks (Text, Image, Button) on their own schedule. * * Usage: * node convert-html-to-unlayer.js gift-email-fr * node convert-html-to-unlayer.js gift-email-en */ const fs = require('fs') const path = require('path') function htmlToUnlayer (innerBodyHtml, opts = {}) { const preheader = opts.preheader || '' return { counters: { u_row: 1, u_column: 1, u_content_html: 1 }, body: { id: 'BODY-1', rows: [ { id: 'ROW-1', cells: [1], columns: [ { id: 'COL-1', contents: [ { id: 'HTML-1', type: 'html', values: { html: innerBodyHtml, hideDesktop: false, displayCondition: null, containerPadding: '0px', _meta: { htmlID: 'u_content_html_1', htmlClassNames: 'u_content_html' }, selectable: true, draggable: true, duplicatable: true, deletable: true, hideable: true, }, }, ], values: { _meta: { htmlID: 'u_column_1', htmlClassNames: 'u_column' }, }, }, ], values: { displayCondition: null, columns: false, backgroundColor: '', columnsBackgroundColor: '', padding: '0px', anchor: '', hideDesktop: false, _meta: { htmlID: 'u_row_1', htmlClassNames: 'u_row' }, selectable: true, draggable: true, duplicatable: true, deletable: true, hideable: true, }, }, ], values: { popupPosition: 'center', popupWidth: '600px', popupHeight: 'auto', borderRadius: '10px', contentAlign: 'center', contentVerticalAlign: 'center', contentWidth: '600px', fontFamily: { label: 'Plus Jakarta Sans', value: "'Plus Jakarta Sans', sans-serif", url: 'https://fonts.googleapis.com/css?family=Plus+Jakarta+Sans:400,500,600,700', }, textColor: '#1B2E24', popupBackgroundColor: '#FFFFFF', backgroundColor: '#F5FAF7', preheaderText: preheader, linkStyle: { body: true, linkColor: '#00C853', linkHoverColor: '#005026', linkUnderline: true, linkHoverUnderline: true, }, _meta: { htmlID: 'u_body', htmlClassNames: 'u_body' }, }, }, schemaVersion: 12, } } // ── CLI ────────────────────────────────────────────────────────────────── const name = process.argv[2] if (!name) { console.error('Usage: convert-html-to-unlayer.js ') process.exit(1) } const TEMPLATES_DIR = path.resolve(__dirname, '..', 'templates') const htmlPath = path.join(TEMPLATES_DIR, name + '.html') const jsonPath = path.join(TEMPLATES_DIR, name + '.json') if (!fs.existsSync(htmlPath)) { console.error(`✗ No HTML at ${htmlPath}`) process.exit(1) } const fullHtml = fs.readFileSync(htmlPath, 'utf8') // Extract just the body inner content — Unlayer wraps everything in its own // at preview/export time, so we don't want duplicated // doctype/head/body tags. const bodyMatch = fullHtml.match(/]*>([\s\S]*?)<\/body>/i) const innerHtml = bodyMatch ? bodyMatch[1].trim() : fullHtml // Pull preheader text if a hidden
is present // (standard email preheader pattern, also what MJML's compiles to) const preheaderMatch = innerHtml.match(/]*display:\s*none[^>]*>\s*([^<]+?)\s*<\/div>/i) const preheader = preheaderMatch ? preheaderMatch[1].trim() : '' const design = htmlToUnlayer(innerHtml, { preheader }) // Optional: backup existing .json if (fs.existsSync(jsonPath)) { const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19) fs.copyFileSync(jsonPath, jsonPath.replace(/\.json$/, `.bak-${ts}.json`)) } fs.writeFileSync(jsonPath, JSON.stringify(design, null, 2), 'utf8') console.log(`✓ Converted ${name}.html (${fullHtml.length}b) → ${name}.json (${JSON.stringify(design).length}b)`) console.log(` preheader: "${preheader.slice(0, 80)}${preheader.length > 80 ? '…' : ''}"`) console.log(` inner HTML: ${innerHtml.length}b in one Custom HTML block`)