Export HTML and Inline CSS from BuilderJS
This is the output side of the BuilderJS contract: how you get finished HTML out of the editor, and how you inline its CSS so the result survives email clients that strip stylesheets. BuilderJS is the embeddable editor only — it produces the markup, and your own app owns sending, hosting, and storage. The export surface is small and real: builder.getHtml() returns the rendered HTML with absolute media URLs, builder.getHtmlWithRelativeLinks() returns the same markup with the themeMediaUrl prefix stripped for portable storage, and the bundled export-inline.php reference handler promotes inlinable style rules onto each element. Everything below uses the verbatim API — no invented methods.
Mount it
// 1) Export from the editor (real API — verbatim).
// getHtml() → absolute media URLs (same-domain consumers)
// getHtmlWithRelativeLinks() → themeMediaUrl prefix stripped (portable storage)
const portableHtml = builder.getHtmlWithRelativeLinks();
const designJson = builder.getData(); // JSON source of truth — load() restores it
// 2) Inline CSS for email via the bundled reference handler.
// POST the rendered HTML to /backend/export-inline.php.
// For email use the ABSOLUTE flavour so clients fetch images from where they're hosted.
async function inlineForEmail() {
const body = new FormData();
body.append('html', builder.getHtml());
body.append('format', 'json'); // → { ok, html, inlinedDeclarations, inlinedRules, keptRules, styleBlocks }
const res = await fetch('/backend/export-inline.php', { method: 'POST', body });
const data = await res.json();
// data.html is the inlined markup; hand it to YOUR sending path.
// BuilderJS does not send — you bring the ESP / SMTP.
await fetch('/your-app/queue-email', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ html: data.html, source: designJson }),
});
return data; // { inlinedRules, keptRules, ... } for a live preview
}
// 3) Restore later — 4 positional args, in this order:
// builder.load(themeJson, themeTemplatesJson, themeConfigData, mediaUrl);
getHtml() vs getHtmlWithRelativeLinks() — pick by URL space
Two methods, two storage situations. builder.getHtml() returns the rendered canvas HTML with every media URL in absolute form (the themeMediaUrl prefix you passed as load()'s 4th argument is included). Use it when the consumer of the HTML lives on the same origin as the editor — preview iframes, same-domain renderers, server-side render pipelines. builder.getHtmlWithRelativeLinks() does exactly one substitution: it strips that themeMediaUrl prefix so asset paths come out relative (assets/avatar.png instead of https://app.example.com/.../assets/avatar.png). Use it when the saved HTML will be served from a different domain or moved between deployments. Both are pure reads — no side effects, no engine-state mutation — so you can call them anywhere a code path makes sense.
Why email needs inlined CSS, and where BuilderJS draws the line
A web page rendered in a browser is happy with a head stylesheet, so for page output the plain getHtml() markup is usually all you need. Email is different: several major clients discard head and external CSS before showing a message, leaving any non-inlined rule with nothing to apply. The fix is an inliner — a step that copies matching stylesheet rules onto each element's style attribute. BuilderJS keeps this as an explicit output step rather than baking it into getHtml(), so you control when it runs. For a deeper, concept-first explanation of why inboxes strip styles, see the companion guide on inline CSS for email; this page is the how-to for actually producing the inlined output.
Inline at export with the export-inline.php reference handler
The bundle ships a server-side companion to the editor's Export ▸ Inline CSS action: demo/backend/export-inline.php. POST your rendered HTML to it as form data under the html key, with an optional format of download (default — streams an .html attachment) or json (returns {ok, html, inlinedDeclarations, inlinedRules, keptRules, styleBlocks} for live previews). The heavy lifting lives in _lib/CssInliner.php, a zero-dependency DOMDocument-based inliner; the contract is plain string-in, string-out, so you can swap in pelago/emogrifier or tijsverkoyen/css-to-inline-styles by changing a single require line. Most buyers inline server-side, right before handing the HTML to their mail transport — the one place every send path passes through.
Choosing where to inline: client-side topnav vs server-side handler
BuilderJS gives you two entry points to the same inlining step. The editor's topnav Export ▸ Inline CSS action lets an end-user download an inlined file directly from the chrome — convenient for ad-hoc designs. The export-inline.php handler is the programmatic path for automated send pipelines: your backend reads the stored HTML, POSTs it for inlining, and forwards the result to your ESP or SMTP. Because both reduce to CssInliner::inline(string $html): array, the output is identical regardless of trigger. The handler deliberately lets a genuine inliner failure surface as a 500 rather than streaming a half-transformed file — a corrupt email-ready export is worse than a loud error.
What stays yours after export
Export is the boundary where BuilderJS hands control back to your application. getHtml() / getHtmlWithRelativeLinks() return strings; export-inline.php returns inlined strings; getData() returns the JSON source of truth. None of these send, host, store, or schedule anything — BuilderJS never phones home. You decide where the JSON lives (database, object storage, a file), how the HTML is delivered (your ESP, your SMTP, your CDN), and when to regenerate. Pair save with load: store relative HTML and JSON, then pass the new media base URL as load()'s 4th argument and the engine prepends it again on the next open. The round-trip is lossless and the source you self-host under a one-time license.
Frequently asked questions
-
What's the difference between getHtml() and getHtmlWithRelativeLinks()?
Both return the rendered canvas HTML. getHtml() keeps media URLs absolute (the themeMediaUrl prefix is included), which is right when the HTML is consumed on the same origin as the editor. getHtmlWithRelativeLinks() does a single substitution — it strips that themeMediaUrl prefix so asset paths come out relative — which keeps saved HTML portable across domains and deployments. Both are pure reads with no side effects.
-
How do I inline the CSS for email?
POST your exported HTML to the bundled demo/backend/export-inline.php handler with the markup under the html form key. Add format=download (default) to stream an inlined .html attachment, or format=json to get {ok, html, inlinedDeclarations, inlinedRules, keptRules, styleBlocks} back for a live preview. The inlining itself is done by _lib/CssInliner.php, a zero-dependency DOMDocument inliner you own and can replace.
-
Does BuilderJS send the email or host the page after I export?
No. BuilderJS is the embeddable editor only — it produces HTML and JSON and inlines CSS, but it never sends email, hosts pages, schedules, or stores designs for you. After export you connect the HTML to your own sending path (your ESP, SMTP server, or backend) and your own hosting. That clean split keeps you in full control of delivery and storage.
-
Can I inline CSS without a server round-trip?
Yes for ad-hoc use: the editor's topnav Export ▸ Inline CSS action lets an end-user download an inlined file directly from the chrome. For automated send pipelines, the export-inline.php handler is the programmatic path. Both reduce to the same CssInliner::inline() call, so the inlined output is identical regardless of which trigger you use.
-
Should I store the absolute or relative HTML?
Store relative HTML via getHtmlWithRelativeLinks() when the saved markup may move between domains, deployments, or a CDN — it strips the hardcoded themeMediaUrl prefix that would otherwise break on migration. On the next builder.load(themeJson, themeTemplatesJson, themeConfigData, mediaUrl) call, pass the new media base URL as the 4th argument and the engine reattaches it. Use absolute getHtml() only when the consumer is same-domain.