Environment-agnostic rendering SDK for Loopwind templates. Works in browsers, Cloudflare Workers, and Node.js.
Import: loopwind/sdk
import { renderToSvg, initSatori, loadDefaultFonts, tw, transformClassNames } from 'loopwind/sdk';
import satori from 'satori'; // or from esm.sh CDN
initSatori(satori);
const fonts = await loadDefaultFonts();
const svg = await renderToSvg({
component: MyTemplate,
props: { title: 'Hello' },
width: 1200,
height: 630,
fonts,
});import cfSatori from '@cf-wasm/satori';
import { renderToSvg, initSatori, loadDefaultFonts } from 'loopwind/sdk';
initSatori(cfSatori);
const fonts = await loadDefaultFonts();
export default {
async fetch(request) {
const svg = await renderToSvg({
component: MyTemplate,
props: { title: 'Hello' },
width: 1200,
height: 630,
fonts,
});
return new Response(svg, { headers: { 'Content-Type': 'image/svg+xml' } });
},
};import satori from 'satori';
import { renderToSvg, initSatori, loadDefaultFonts } from 'loopwind/sdk';
initSatori(satori);
const fonts = await loadDefaultFonts();
const svg = await renderToSvg({ component: MyTemplate, props: { title: 'Hello' }, width: 1200, height: 630, fonts });Initialize the Satori rendering engine. Must be called before any render.
// Browser — load from CDN
const { default: satori } = await import('https://esm.sh/[email protected]');
initSatori(satori);
// Cloudflare Workers — use @cf-wasm/satori
import cfSatori from '@cf-wasm/satori';
initSatori(cfSatori);
// Node.js — direct import
import satori from 'satori';
initSatori(satori);Initialize the resvg WASM module for PNG rendering. Only needed if using renderToPng().
// From URL
await initResvgWasm('https://cdn.example.com/resvg.wasm');
// From ArrayBuffer
await initResvgWasm(wasmBuffer);Render a template component to SVG. This is the core rendering function.
Options:
| Option | Type | Required | Description |
|---|---|---|---|
component |
TemplateComponent |
Yes | Template function (default export from a template file) |
props |
Record<string, any> |
No | User-provided template props |
width |
number |
Yes | Canvas width in pixels |
height |
number |
Yes | Canvas height in pixels |
fonts |
SdkFont[] |
Yes | Fonts for text rendering |
config |
DesignConfig |
No | Design tokens (colors, fonts, etc.). Defaults to shadcn zinc theme |
animation |
object |
No | Animation context for video templates |
templates |
Record<string, EmbeddableTemplate> |
No | Templates available for template() helper |
qrGenerator |
function |
No | Custom QR code generator |
imageLoader |
function |
No | Custom image loader (default: fetch → base64) |
loadEmoji |
function |
No | Custom emoji loader |
debug |
boolean |
No | Show Satori bounding boxes |
embedFont |
boolean |
No | Embed fonts in SVG (default: true) |
Animation context:
animation: {
frame: 15, // Current frame number
progress: 0.5, // 0.0 to 1.0
totalFrames: 90, // Total frames in the video
durationMs: 3000, // Total duration in milliseconds
}Example with all options:
const svg = await renderToSvg({
component: MyTemplate,
props: { title: 'Hello', logo: 'https://example.com/logo.png', url: 'https://example.com' },
width: 1200,
height: 630,
fonts,
config: {
colors: { primary: '#6366f1', background: '#0f172a', foreground: '#f8fafc' },
},
animation: { frame: 0, progress: 0, totalFrames: 180, durationMs: 3000 },
templates: {
badge: { component: BadgeTemplate, meta: { size: { width: 200, height: 50 } } },
},
});Render a template to PNG. Extends RenderToSvgOptions with:
| Option | Type | Default | Description |
|---|---|---|---|
scale |
number |
2 | Scale multiplier for high-res output |
resvgWasm |
ArrayBuffer |
— | WASM binary (auto-inits if provided) |
const { data, width, height } = await renderToPng({
component: MyTemplate,
props: { title: 'Hello' },
width: 1200, height: 630,
fonts, scale: 2,
});
// data is Uint8Array of PNG bytesRecursively walks a React element tree and converts className props to inline style via the provided tw() function. This is what allows templates to use className="flex items-center bg-background" instead of inline styles.
import { transformClassNames, tw, DEFAULT_CONFIG } from 'loopwind/sdk';
const twFn = (classes) => tw(classes, null, DEFAULT_CONFIG, animationContext);
const styledElement = transformClassNames(rawElement, twFn);Convert Tailwind class strings to inline React CSS properties. Supports:
- Layout:
flex,grid,absolute,relative - Spacing:
p-4,m-6,gap-8,px-16 - Typography:
text-6xl,font-bold - Colors:
bg-primary,text-muted-foreground,bg-primary/50 - Borders:
border,rounded-xl - Animations:
enter-fade-in-up/0/500,loop-spin/1000,exit-fade-out/2500/500 - Easing:
ease-out,ease-spring/1/100/10
const style = tw('flex items-center p-4 bg-background text-foreground');
// → { display: 'flex', alignItems: 'center', padding: 16, backgroundColor: '#ffffff', color: '#09090b' }
// With animation context (for video templates)
const style = tw('enter-fade-in-up/0/500 ease-out', null, config, {
progress: 0.1, frame: 9, totalFrames: 90, durationMs: 3000,
});
// → { opacity: 0.71, transform: 'translateY(8.5px)' }Load Inter Regular (400) and Bold (700) from jsDelivr CDN. Results are cached.
Load a single font from URL.
const font = await loadFont('https://example.com/MyFont.woff', {
name: 'MyFont',
weight: 400,
style: 'normal',
});Load multiple fonts in parallel.
const fonts = await loadFonts([
{ url: 'https://example.com/Regular.woff', name: 'MyFont', weight: 400 },
{ url: 'https://example.com/Bold.woff', name: 'MyFont', weight: 700 },
]);Generate a QR code data URI. Uses the qrcode npm package if installed, or accepts a custom generator.
const dataUri = await generateQr('https://example.com', { width: 200, margin: 1 });Fetch an image from a URL and return as a base64 data URI.
const dataUri = await fetchImageAsDataUri('https://example.com/photo.jpg');Two-pass asset preparation. Renders the component once with stub helpers to discover which images/QR codes are needed, then pre-fetches them all in parallel.
Returns cached qrHelper and imageHelper functions for the real render pass. This is used internally by renderToSvg() — you typically don't call it directly.
Create a template() helper function for embedding templates within templates.
const templateHelper = createTemplateHelper(
new Map([['badge', { component: Badge, meta: { size: { width: 200, height: 50 } } }]]),
(tmpl, props) => ({ ...props, tw: myTwFn, config: myConfig }),
);Default design config (shadcn/ui zinc theme):
{
colors: {
primary: '#18181b',
'primary-foreground': '#fafafa',
background: '#ffffff',
foreground: '#09090b',
muted: '#f4f4f5',
'muted-foreground': '#71717a',
card: '#ffffff',
border: '#e4e4e7',
// ...
},
fonts: { sans: ['Noto Sans', 'system-ui', 'sans-serif'] },
tokens: { borderRadius: { sm: '0.25rem', md: '0.375rem', lg: '0.5rem', xl: '0.75rem' } },
}interface SdkFont {
name: string;
data: ArrayBuffer;
weight?: 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900;
style?: 'normal' | 'italic';
}
type TemplateComponent = (props: Record<string, any>) => any;
interface PngResult {
data: Uint8Array;
width: number;
height: number;
}
interface EmbeddableTemplate {
component: TemplateComponent;
meta: {
size: { width: number; height: number };
video?: { fps: number; duration: number };
};
}
interface QrOptions {
width?: number;
margin?: number;
errorCorrectionLevel?: 'L' | 'M' | 'Q' | 'H';
color?: { dark?: string; light?: string };
}Templates receive these injected props alongside user-provided props:
| Prop | Type | Description |
|---|---|---|
frame |
number |
Current frame (video only) |
progress |
number |
0.0–1.0 progress (video only) |
config |
DesignConfig |
Design tokens |
tw |
(classes) => CSSProperties |
Tailwind class converter |
qr |
(text) => string |
QR code generator (returns data URI) |
image |
(urlOrKey) => string |
Image loader (returns data URI) |
template |
(name, props?) => JSX |
Embed another template |
path |
object |
Path animation helpers (followLine, followCircle, etc.) |
textPath |
object |
Text-on-path helpers (onPath, onCircle, etc.) |
Templates use Tailwind-style animation classes via className:
// Entrance animations
<h1 className="enter-fade-in/0/500"> // fade in from 0ms, 500ms duration
<h1 className="enter-fade-in-up/0/500"> // fade in + slide up
<h1 className="ease-out enter-fade-in/0/500"> // with easing
// Exit animations
<p className="exit-fade-out/2500/500"> // fade out starting at 2500ms
// Loop animations
<div className="loop-spin/1000"> // spin every 1000ms
<div className="loop-fade/500"> // pulse opacity every 500ms
// Utility-based animations
<div className="enter-translate-x-5/0/800"> // slide in from x offset
<div className="loop-rotate-45/2000"> // oscillate rotationThe SDK automatically patches Satori's SVG matrix transform precision from toFixed(2) to toFixed(6) during rendering. This prevents visible stepping/jumping in rotation and transform animations between frames. The patch is applied and reverted around each renderToSvg() call — no global side effects.
Template (JSX + className) → transformClassNames(tw()) → Satori → SVG
↓
resvg WASM → PNG
The SDK has zero Node.js dependencies. No fs, path, esbuild, sharp, crypto, or os imports. All I/O uses fetch() and standard Web APIs.
| File | Purpose |
|---|---|
src/sdk/index.ts |
Public API — all exports |
src/sdk/render.ts |
renderToSvg(), renderToPng(), initSatori(), transformClassNames() |
src/sdk/helpers.ts |
QR, image, template embedding helpers |
src/sdk/fonts.ts |
Font loading via CDN |
src/sdk/types.ts |
TypeScript type definitions |