Skip to content

Renderer

@templatical/renderer converts Templatical template JSON to MJML. Works in both browser and Node.js environments.

The renderer produces MJML only. To compile MJML to HTML for email sending, use any MJML library (mjml for Node.js, spatie/mjml-php for PHP, etc.).

bash
npm install @templatical/renderer

renderToMjml(content, options?)

Renders a TemplateContent object to an MJML string. Returns a Promise<string> — async so custom blocks (which may require async work to resolve) can be rendered in line.

ts
import { renderToMjml } from '@templatical/renderer';

const mjml = await renderToMjml(templateContent);

Parameters:

ParameterTypeDescription
contentTemplateContentThe template to render
optionsRenderOptionsOptional rendering configuration

Returns: Promise<string> -- MJML markup

RenderOptions

ts
interface RenderOptions {
  customFonts?: CustomFont[];
  defaultFallbackFont?: string;
  allowHtmlBlocks?: boolean;      // default: true
  renderCustomBlock?: (block: CustomBlock) => Promise<string>;
  socialIconsBaseUrl?: string;
}
OptionDefaultDescription
customFonts[]Custom font definitions for <mj-font> declarations in rendered output
defaultFallbackFont'Arial, sans-serif'Fallback font stack
allowHtmlBlockstrueSet to false to strip HTML blocks from output
renderCustomBlock--Resolves custom blocks to HTML. Called once per custom block. Editor consumers pass editor.renderCustomBlock; headless consumers wire their own resolver. If omitted, custom blocks fall back to the block's renderedHtml field (if present) and otherwise are omitted.
socialIconsBaseUrlversion-pinned unpkg URLBase URL (no trailing slash) for the social icon PNG assets. Resolved per icon to ${baseUrl}/${style}/${platform}.png. See Social icons below.

Custom blocks

When the content tree contains custom blocks, the renderer asks the supplied renderCustomBlock callback to convert each one to HTML. From the editor:

ts
const mjml = await renderToMjml(editor.getContent(), {
  renderCustomBlock: editor.renderCustomBlock,
});

Headless / Node.js consumers (no editor mounted) can provide their own resolver — for example, running the same Liquid template against the block's fieldValues:

ts
import { Liquid } from 'liquidjs';

const engine = new Liquid();
const definitionsByType = new Map(/* your CustomBlockDefinition list, keyed by type */);

const mjml = await renderToMjml(content, {
  async renderCustomBlock(block) {
    const definition = definitionsByType.get(block.customType);
    if (!definition) return '';
    return engine.parseAndRender(definition.template, block.fieldValues);
  },
});

Social icons

Social icon blocks are emitted as <img src="…/{style}/{platform}.png">. The default socialIconsBaseUrl points at the version-pinned unpkg mirror of @templatical/renderer, which ships pre-rasterized PNGs (16 platforms × 5 styles) alongside the package:

https://unpkg.com/@templatical/renderer@<version>/assets/social/{style}/{platform}.png

Why PNGs. Outlook desktop (Word rendering engine) does not support SVG and rejects base64 data URIs in <img src>. Hosted PNGs are the only format that renders across every mainstream email client.

Why version-pinned. Email is archival — recipients open messages months or years after they're sent. The version pin freezes the icon visuals at render time so a future redesign or regression in the package doesn't retroactively break already-delivered emails. It also avoids a per-image 302 redirect and unlocks long-lived immutable cache headers.

Self-hosting. Override socialIconsBaseUrl to serve the assets from your own CDN — useful for air-gapped environments, brand-specific theming, or removing the unpkg dependency:

ts
const mjml = await renderToMjml(content, {
  socialIconsBaseUrl: 'https://cdn.example.com/email-assets/social',
});

The exact filenames the renderer expects are {style}/{platform}.png where style is one of solid | outlined | rounded | square | circle and platform is one of facebook | twitter | instagram | linkedin | youtube | tiktok | pinterest | email | whatsapp | telegram | discord | snapchat | reddit | github | dribbble | behance. The shipped 192×192 PNGs are a reasonable starting point if you want to mirror them.

The package also exports DEFAULT_SOCIAL_ICONS_BASE_URL if you want to compose URLs against the same default:

ts
import { DEFAULT_SOCIAL_ICONS_BASE_URL } from '@templatical/renderer';

Utilities

The renderer also exports utility functions:

ts
import {
  escapeHtml,
  escapeAttr,
  convertMergeTagsToValues,
  isHiddenOnAll,
  toPaddingString,
  renderBlock,
  getCssClassAttr,
  getCssClasses,
  getWidthPercentages,
  getWidthPixels,
  DEFAULT_SOCIAL_ICONS_BASE_URL,
  RenderContext,
} from '@templatical/renderer';

escapeHtml(text)

Escapes HTML entities (<, >, &, ", ') in a string. Use when inserting user content into HTML:

ts
escapeHtml('<script>alert("xss")</script>');
// '&lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;'

escapeAttr(text)

Escapes a string for safe use in HTML attribute values:

ts
const alt = escapeAttr('Photo of "sunrise" at O\'Hare');
// 'Photo of &quot;sunrise&quot; at O&#x27;Hare'

convertMergeTagsToValues(html)

Converts merge tag HTML spans (used internally by the editor's rich text system) back into their plain text syntax. The editor stores merge tags as <span data-merge-tag="..."> elements; this function strips the spans and leaves the raw merge tag syntax:

ts
import { convertMergeTagsToValues } from '@templatical/renderer';

// Input: editor's internal HTML format
const editorHtml = '<span data-merge-tag="{{ first_name }}">First Name</span>';

const cleaned = convertMergeTagsToValues(editorHtml);
// Output: '{{ first_name }}'

TIP

You typically don't need to call this directly -- the renderer calls it internally when processing title and paragraph blocks. It's exported for advanced use cases where you're working with editor HTML outside the normal rendering pipeline.

isHiddenOnAll(block)

Returns true if a block's visibility has all viewports set to false. Useful for skipping blocks that shouldn't render at all:

ts
if (isHiddenOnAll(block)) {
  // Skip this block entirely
}

toPaddingString(padding)

Converts a SpacingValue to a CSS padding string:

ts
toPaddingString({ top: 10, right: 20, bottom: 10, left: 20 });
// '10px 20px 10px 20px'

renderBlock(block, context)

Renders a single block to its MJML representation. Used internally by renderToMjml() but exported for advanced use cases where you need to render individual blocks.

RenderContext

The context object passed to block renderers. Contains render options and font configuration.

getCssClassAttr(block) / getCssClasses(block)

Generate CSS class attributes from a block's visibility settings. Used internally for responsive hiding.

getWidthPercentages(layout) / getWidthPixels(layout, containerWidth)

Calculate column widths for a given ColumnLayout. Returns an array of percentage or pixel values per column.

DEFAULT_SOCIAL_ICONS_BASE_URL

The default value of RenderOptions.socialIconsBaseUrl — the version-pinned unpkg URL pointing at this package's bundled social icon PNGs. See Social icons.

Compiling MJML to HTML

After rendering to MJML, compile to HTML using any MJML library:

ts
import { renderToMjml } from '@templatical/renderer';
import mjml2html from 'mjml';

const mjml = await renderToMjml(templateContent);
const { html } = mjml2html(mjml);

// html is ready to send via your email service

See How Rendering Works for more on the rendering pipeline.