Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/social-icons-png-output.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@templatical/renderer": patch
"@templatical/editor": patch
---

Render social icons as hosted PNGs instead of inline SVG data URIs so they display in Outlook desktop (the Word rendering engine has no SVG support and rejects base64 in `<img src>`). PNGs are shipped with the npm package and served via the version-pinned unpkg URL by default; override via the new `RenderOptions.socialIconsBaseUrl` to self-host. Replace the Style segmented control in the social icons sidebar with a native dropdown so the 5-option list no longer overflows the sidebar.
3 changes: 0 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@ coverage
.DS_Store
.idea
*.tsbuildinfo
apps/playground/.env
apps/docs/.vitepress/cache
apps/docs/.vitepress/dist
playwright-report/
test-results/
blob-report/
Expand Down
2 changes: 2 additions & 0 deletions apps/docs/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.vitepress/cache
.vitepress/dist
46 changes: 33 additions & 13 deletions apps/docs/api/renderer-typescript.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ interface RenderOptions {
defaultFallbackFont?: string;
allowHtmlBlocks?: boolean; // default: true
renderCustomBlock?: (block: CustomBlock) => Promise<string>;
socialIconsBaseUrl?: string;
}
```

Expand All @@ -49,6 +50,7 @@ interface RenderOptions {
| `defaultFallbackFont` | `'Arial, sans-serif'` | Fallback font stack |
| `allowHtmlBlocks` | `true` | Set 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. |
| `socialIconsBaseUrl` | version-pinned unpkg URL | Base URL (no trailing slash) for the social icon PNG assets. Resolved per icon to `${baseUrl}/${style}/${platform}.png`. See [Social icons](#social-icons) below. |

### Custom blocks

Expand Down Expand Up @@ -77,6 +79,34 @@ const mjml = await renderToMjml(content, {
});
```

### 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:
Expand All @@ -88,13 +118,12 @@ import {
convertMergeTagsToValues,
isHiddenOnAll,
toPaddingString,
generateSocialIconDataUri,
renderBlock,
getCssClassAttr,
getCssClasses,
getWidthPercentages,
getWidthPixels,
SOCIAL_ICONS,
DEFAULT_SOCIAL_ICONS_BASE_URL,
RenderContext,
} from '@templatical/renderer';
```
Expand Down Expand Up @@ -154,15 +183,6 @@ toPaddingString({ top: 10, right: 20, bottom: 10, left: 20 });
// '10px 20px 10px 20px'
```

### `generateSocialIconDataUri(platform, style, size)`

Generates a base64-encoded SVG data URI for a social media platform icon. Used internally by the renderer for social icon blocks:

```ts
const uri = generateSocialIconDataUri('twitter', 'circle', 32);
// 'data:image/svg+xml,...'
```

### `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.
Expand All @@ -179,9 +199,9 @@ Generate CSS class attributes from a block's visibility settings. Used internall

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

### `SOCIAL_ICONS`
### `DEFAULT_SOCIAL_ICONS_BASE_URL`

A map of all built-in social platform SVG icon data, keyed by platform and style.
The default value of `RenderOptions.socialIconsBaseUrl` — the version-pinned unpkg URL pointing at this package's bundled social icon PNGs. See [Social icons](#social-icons).

## Compiling MJML to HTML

Expand Down
46 changes: 33 additions & 13 deletions apps/docs/de/api/renderer-typescript.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ interface RenderOptions {
defaultFallbackFont?: string;
allowHtmlBlocks?: boolean; // Standard: true
renderCustomBlock?: (block: CustomBlock) => Promise<string>;
socialIconsBaseUrl?: string;
}
```

Expand All @@ -49,6 +50,7 @@ interface RenderOptions {
| `defaultFallbackFont` | `'Arial, sans-serif'` | Fallback-Schriftart-Stack |
| `allowHtmlBlocks` | `true` | Auf `false` setzen, um HTML-Blöcke aus der Ausgabe zu entfernen |
| `renderCustomBlock` | -- | Wandelt benutzerdefinierte Blöcke in HTML um. Wird einmal pro benutzerdefiniertem Block aufgerufen. Editor-Konsumenten übergeben `editor.renderCustomBlock`; Headless-Konsumenten verwenden einen eigenen Resolver. Wenn weggelassen, fällt der Renderer auf das `renderedHtml`-Feld des Blocks zurück (falls vorhanden) und lässt den Block andernfalls weg. |
| `socialIconsBaseUrl` | versionsgebundene unpkg-URL | Basis-URL (ohne abschließenden Schrägstrich) für die PNG-Assets der Social-Media-Icons. Wird pro Icon zu `${baseUrl}/${style}/${platform}.png` aufgelöst. Siehe [Social-Media-Icons](#social-media-icons) unten. |

### Benutzerdefinierte Blöcke

Expand Down Expand Up @@ -77,6 +79,34 @@ const mjml = await renderToMjml(content, {
});
```

### Social-Media-Icons

Social-Icon-Blöcke werden als `<img src="…/{style}/{platform}.png">` ausgegeben. Der Standardwert von `socialIconsBaseUrl` verweist auf den versionsgebundenen unpkg-Mirror von `@templatical/renderer`, der vorgerasterte PNGs (16 Plattformen × 5 Stile) mit dem Paket ausliefert:

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

**Warum PNGs.** Outlook Desktop (Word-Rendering-Engine) unterstützt kein SVG und lehnt base64-Daten-URIs in `<img src>` ab. Gehostete PNGs sind das einzige Format, das in allen gängigen E-Mail-Clients zuverlässig dargestellt wird.

**Warum versionsgebunden.** E-Mails sind Archivinhalt — Empfänger öffnen Nachrichten Monate oder Jahre nach dem Versand. Die Versionsbindung friert die Icon-Darstellung zum Renderzeitpunkt ein, sodass ein späteres Redesign oder ein Regressionsfehler im Paket bereits zugestellte E-Mails nicht rückwirkend beschädigt. Außerdem entfällt eine 302-Weiterleitung pro Icon und es können langlebige, unveränderliche Cache-Header gesetzt werden.

**Selbst hosten.** Überschreiben Sie `socialIconsBaseUrl`, um die Assets über Ihr eigenes CDN auszuliefern — nützlich für Air-Gapped-Umgebungen, markenspezifische Themen oder um die Abhängigkeit von unpkg zu entfernen:

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

Die exakten Dateinamen, die der Renderer erwartet, sind `{style}/{platform}.png`, wobei `style` einer von `solid | outlined | rounded | square | circle` und `platform` einer von `facebook | twitter | instagram | linkedin | youtube | tiktok | pinterest | email | whatsapp | telegram | discord | snapchat | reddit | github | dribbble | behance` ist. Die ausgelieferten 192×192-PNGs sind ein sinnvoller Ausgangspunkt, wenn Sie sie spiegeln möchten.

Das Paket exportiert außerdem `DEFAULT_SOCIAL_ICONS_BASE_URL`, falls Sie URLs gegen denselben Standardwert komponieren möchten:

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

## Hilfsfunktionen

Der Renderer exportiert außerdem Hilfsfunktionen:
Expand All @@ -88,13 +118,12 @@ import {
convertMergeTagsToValues,
isHiddenOnAll,
toPaddingString,
generateSocialIconDataUri,
renderBlock,
getCssClassAttr,
getCssClasses,
getWidthPercentages,
getWidthPixels,
SOCIAL_ICONS,
DEFAULT_SOCIAL_ICONS_BASE_URL,
RenderContext,
} from '@templatical/renderer';
```
Expand Down Expand Up @@ -154,15 +183,6 @@ toPaddingString({ top: 10, right: 20, bottom: 10, left: 20 });
// '10px 20px 10px 20px'
```

### `generateSocialIconDataUri(platform, style, size)`

Erzeugt eine base64-kodierte SVG-Data-URI für ein Social-Media-Plattform-Icon. Wird intern vom Renderer für Social-Icon-Blöcke verwendet:

```ts
const uri = generateSocialIconDataUri('twitter', 'circle', 32);
// 'data:image/svg+xml,...'
```

### `renderBlock(block, context)`

Rendert einen einzelnen Block in seine MJML-Darstellung. Wird intern von `renderToMjml()` verwendet, aber für fortgeschrittene Anwendungsfälle exportiert, in denen Sie einzelne Blöcke rendern müssen.
Expand All @@ -179,9 +199,9 @@ Erzeugen CSS-Klassen-Attribute aus den Sichtbarkeitseinstellungen eines Blocks.

Berechnen Spaltenbreiten für ein gegebenes `ColumnLayout`. Gibt ein Array von Prozent- oder Pixelwerten pro Spalte zurück.

### `SOCIAL_ICONS`
### `DEFAULT_SOCIAL_ICONS_BASE_URL`

Eine Zuordnung aller eingebauten SVG-Icon-Daten für soziale Plattformen, nach Plattform und Stil indiziert.
Der Standardwert von `RenderOptions.socialIconsBaseUrl` — die versionsgebundene unpkg-URL, die auf die in diesem Paket mitgelieferten PNGs der Social-Media-Icons verweist. Siehe [Social-Media-Icons](#social-media-icons).

## MJML zu HTML kompilieren

Expand Down
1 change: 1 addition & 0 deletions apps/playground/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.env
29 changes: 18 additions & 11 deletions packages/editor/src/components/toolbar/SocialToolbar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
import type {
SocialIcon,
SocialIconsBlock,
SocialIconStyle,
SocialPlatform,
} from "@templatical/types";
import { generateId } from "@templatical/types";
Expand Down Expand Up @@ -74,6 +75,7 @@ function removeSocialIcon(iconId: string): void {
>
<div class="tpl:flex tpl:items-center tpl:gap-2">
<select
data-testid="social-platform-select"
:class="inputClass"
class="tpl:flex-1"
:value="icon.platform"
Expand Down Expand Up @@ -116,17 +118,22 @@ function removeSocialIcon(iconId: string): void {
</div>
<div class="tpl:mb-3.5">
<label :class="labelClass">{{ t.social.style }}</label>
<SlidingPillSelect
:options="[
{ value: 'solid', label: t.social.styleSolid },
{ value: 'outlined', label: t.social.styleOutlined },
{ value: 'rounded', label: t.social.styleRounded },
{ value: 'square', label: t.social.styleSquare },
{ value: 'circle', label: t.social.styleCircle },
]"
:model-value="block.iconStyle"
@update:model-value="updateField('iconStyle', $event)"
/>
<select
:class="inputClass"
:value="block.iconStyle"
@change="
updateField(
'iconStyle',
($event.target as HTMLSelectElement).value as SocialIconStyle,
)
"
>
<option value="solid">{{ t.social.styleSolid }}</option>
<option value="outlined">{{ t.social.styleOutlined }}</option>
<option value="rounded">{{ t.social.styleRounded }}</option>
<option value="square">{{ t.social.styleSquare }}</option>
<option value="circle">{{ t.social.styleCircle }}</option>
</select>
</div>
<div class="tpl:mb-3.5">
<label :class="labelClass">{{ t.social.size }}</label>
Expand Down
4 changes: 2 additions & 2 deletions packages/editor/tests/socialToolbar.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ describe('SocialToolbar CRUD', () => {
]);
const wrapper = mountIt(block);

const selects = wrapper.findAll('select');
const selects = wrapper.findAll('[data-testid="social-platform-select"]');
expect(selects).toHaveLength(2);
expect((selects[0].element as HTMLSelectElement).value).toBe('facebook');
expect((selects[1].element as HTMLSelectElement).value).toBe('twitter');
Expand Down Expand Up @@ -52,7 +52,7 @@ describe('SocialToolbar CRUD', () => {
]);
const wrapper = mountIt(block);

const select = wrapper.findAll('select')[1];
const select = wrapper.findAll('[data-testid="social-platform-select"]')[1];
(select.element as HTMLSelectElement).value = 'instagram';
await select.trigger('change');

Expand Down
1 change: 1 addition & 0 deletions packages/renderer/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
assets/social/
6 changes: 4 additions & 2 deletions packages/renderer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"@templatical/types": "workspace:*"
},
"devDependencies": {
"@resvg/resvg-js": "^2.6.2",
"mjml": "^5.1.0",
"tsup": "^8.5.1",
"typescript": "^6.0.3",
Expand All @@ -19,7 +20,8 @@
}
},
"files": [
"dist"
"dist",
"assets"
],
"homepage": "https://templatical.com",
"keywords": [
Expand All @@ -41,7 +43,7 @@
"directory": "packages/renderer"
},
"scripts": {
"build": "tsup",
"build": "tsup && node scripts/rasterize-social.mjs",
"test": "vitest run --config vitest.config.ts",
"typecheck": "tsc --noEmit"
},
Expand Down
Loading
Loading