A single client launch can need a dozen feed posts, a handful of stories, highlight covers, a flyer, and two or three reels. Do that across 30+ platforms and you are either drowning a designer or you are automating. We automated. One generator, fed a data file, produces a full campaign set — posts, carousels, covers, and video — every asset on-brand because the brand lives in code, not in someone's afternoon. This is how the pipeline is built.
The usual objection is that automation belongs to engineering and creative is supposed to be hand-made. That is true right up until your brand rules are strict enough to write down. Ours are: black-and-white minimalism, a fixed type stack, an ink scale, hairline rules, no color accent. Once a rule is that precise, it is not a taste call anymore — it is a constant you can encode. And anything you can encode, you can repeat a hundred times without a human in the loop.
When automation belongs in creative
Code automation is obvious; creative automation makes people flinch. The fear is that a generator produces soulless, samey output. It does — if the rules are vague. The whole game is the inverse: the stricter and more legible your brand system, the better a candidate it is for automation, because there are fewer free variables for a machine to get wrong.
A loose brand — pick any font that feels nice, any photo that vibes, accent colors by mood — cannot be automated, because there is no function to write. A tight brand can. When the headline is always Space Grotesk, the body is always Inter, the eyebrow is always uppercase mono, and the palette is three shades of ink on white, the design is a template with holes in it. Fill the holes from data and you have a finished asset. The discipline that makes a brand recognizable is the same discipline that makes it scriptable.
So the line we draw is not 'creative vs. code'. It is 'decided vs. undecided'. The decided parts — layout grid, type, palette, the shape language — get encoded once. The undecided parts — the headline, the offer, the photo, the order of slides — stay in a data file a human edits. The generator is just the glue that turns decided rules plus undecided data into a deliverable.
Data-driven SVG templates as the source of truth
Every asset starts as an SVG template, because SVG is text. Text is the right substrate for the same reason it is right for the rest of our stack: it is tiny, resolution-independent, generated by string concatenation, and reviewable in a pull request like any other code. A template is a function — campaign data in, finished SVG markup out — never a static file someone duplicates and edits by hand.
The campaign itself is one JSON (or TypeScript) file. It holds the variable content: brand tokens, the list of posts, each post's headline and subhead, which template to use, any image path. The generator iterates that array, and for each entry calls the matching template function. Add a post to the array, get a post in the output folder. Nothing is positioned by hand twice.
// The campaign is data. Editing this file edits the whole set.
const campaign = {
brand: { ink: "#0a0a0a", bg: "#ffffff", faint: "#e5e5e5",
head: "Space Grotesk", body: "Inter" },
posts: [
{ tpl: "feed", eyebrow: "ISLANDWIDE", head: "Same-day across Jamaica.", sub: "Kingston to Montego Bay." },
{ tpl: "feed", eyebrow: "TRACKED", head: "Know where it is.", sub: "Live status, every leg." },
{ tpl: "story", eyebrow: "BOOK NOW", head: "Two taps to a courier.", sub: "On the web. On your phone." },
],
};
// A template is a pure function: data in, SVG string out.
function feed({ brand, eyebrow, head, sub }: Post): string {
return `<svg viewBox="0 0 1080 1080" xmlns="http://www.w3.org/2000/svg">
<rect width="1080" height="1080" fill="${brand.bg}"/>
<rect x="64" y="64" width="952" height="952" fill="none"
stroke="${brand.faint}" stroke-width="1"/>
<text x="96" y="180" font-family="${brand.body}" font-size="28"
letter-spacing="6" fill="${brand.ink}">${eyebrow}</text>
<text x="96" y="560" font-family="${brand.head}" font-size="96"
font-weight="700" fill="${brand.ink}">${head}</text>
<text x="96" y="640" font-family="${brand.body}" font-size="40"
fill="${brand.ink}">${sub}</text>
</svg>`;
}Two properties matter here. First, a template can only draw with brand tokens — the headline font is brand.head, never an arbitrary string — so a post physically cannot ship off-brand. Second, because the input is data, the output is reproducible: the same campaign file always produces the same set. That is the same determinism principle behind our generative cover-art pipeline, applied to whole campaigns instead of single images.
Rendering: headless Chrome, SVG to PNG
SVG is the master format, but Instagram and WhatsApp want raster — an actual PNG at a fixed size. So every SVG string goes through a render step. For pure SVG with no web fonts to resolve, a rasterizer handles it directly; when the template uses real web fonts or CSS layout, we render through headless Chrome with puppeteer-core and screenshot the node at a pinned viewport. Same render path we reuse across the whole studio.
import puppeteer from "puppeteer-core";
// Render an array of SVG strings to PNGs at a fixed canvas.
export async function renderAll(svgs: string[], dir: string, size = 1080) {
const browser = await puppeteer.launch({ channel: "chrome",
args: ["--no-sandbox"] });
const page = await browser.newPage();
await page.setViewport({ width: size, height: size, deviceScaleFactor: 2 });
for (let i = 0; i < svgs.length; i++) {
await page.setContent(svgs[i], { waitUntil: "networkidle0" });
await page.screenshot({ path: `${dir}/post-${i + 1}.png`, type: "png" });
}
await browser.close();
}One browser instance, looped over the campaign array, writes the whole set to a folder. Carousels are the same idea with the array shape changed: instead of one SVG per post, a post owns an ordered list of slide SVGs, and the renderer names them slide-1, slide-2, and so on. Highlight covers, flyers, and OG cards are all just more templates pointed at the same render loop. The pinned Chrome channel matters — a floating browser version can shift anti-aliasing and break the 'same data, same PNG' contract.
Motion: ffmpeg reels and Remotion video
Stills are half the brief. The other half is video — reels and stories that move. We use two motion paths depending on how much the video needs to think about its own data, and both reuse the still pipeline as their first stage.
ffmpeg for frame-sequenced reels
The simplest reel is a sequence of stills with timing, motion, and audio. We generate the frames the same way we generate posts — one SVG per frame, rendered deterministically to PNG — then hand the numbered sequence to ffmpeg, which stitches it into an MP4 at the target frame rate, scales to the vertical 1080x1920 reel canvas, and muxes a music bed. No editor, no timeline, no manual export.
# frame-0001.png ... frame-0120.png were rendered from SVG templates.
# Stitch to a 30fps vertical reel and lay a music bed under it.
ffmpeg -y \
-framerate 30 -i frames/frame-%04d.png \
-i audio/bed.m4a -shortest \
-vf "scale=1080:1920:force_original_aspect_ratio=cover,crop=1080:1920" \
-c:v libx264 -pix_fmt yuv420p -movflags +faststart \
out/reel.mp4The deterministic-frame approach is the unlock: because each frame is a pure function of frame index plus campaign data, the motion is repeatable and reviewable. Tweak the data, re-render the frames, re-stitch — the reel updates with zero manual editing. The frames are just the still pipeline run per timestep.
Remotion for data-driven video
When the video needs to react to its content — a product showreel where slide count, copy length, and pacing all depend on the data — frame-by-frame SVG gets awkward. That is where Remotion earns its place: video as React. Each frame is a component that reads the current frame number and the campaign props, and renders accordingly, so the same brand tokens and the same template logic that drive the stills now drive the motion. One source of truth, two output formats.
import { useCurrentFrame, interpolate } from "remotion";
// A scene is a React component. Same brand tokens as the still templates.
export const Headline: React.FC<{ head: string; ink: string }> = ({ head, ink }) => {
const frame = useCurrentFrame();
const opacity = interpolate(frame, [0, 15], [0, 1], { extrapolateRight: "clamp" });
const y = interpolate(frame, [0, 15], [24, 0], { extrapolateRight: "clamp" });
return (
<h1 style={{ fontFamily: "Space Grotesk", fontWeight: 700,
fontSize: 96, color: ink, opacity,
transform: `translateY(${y}px)` }}>
{head}
</h1>
);
};- Stills — data-driven SVG templates rendered to PNG through headless Chrome. Posts, carousels, covers, flyers, OG cards.
- Simple motion — deterministic SVG frames rendered to PNG, stitched by ffmpeg into reels with a music bed.
- Smart motion — Remotion, where each frame is a React component reading campaign props, for showreels whose pacing depends on the data.
- One brand layer — the same tokens and template logic feed every path, so a still and a reel from the same campaign are visibly siblings.
Encode the brand once. After that, a campaign is a data file, and a hundred assets is a single command.
— Studio principle
One generator, a full campaign set
Pulled together, the shape is simple. A campaign is one data file. A gen script reads it, runs each entry through its template, renders the stills, sequences the reel frames, and writes everything to a clearly-named output folder — posts in one subfolder, stories in another, covers in a third, video at the root. This is exactly how our launch kits get built: BP Couriers' Instagram puzzle, the highlight-cover sets, the weekly ad creative for The Language Cradle, the reels that ride on top. One operator, a data edit, a full set out.
The payoff is leverage. A studio of a few people cannot hand-design a dozen assets per launch across dozens of brands and still ship software. Encoding the brand collapses that work into an edit and a render. The creative decisions still belong to a human — the headline, the offer, the photo, the story arc across a carousel — but the production is mechanical, which is exactly where it should be. If you want the still-only version of this thinking, we wrote up the generative cover-art pipeline; the same backbone runs everything in our Supreme Suite launch tooling.
None of this replaces a designer's judgment about what makes a campaign land. It replaces the part where a person manually nudges the same headline into the same box for the ninetieth time. That part is not creative. It is data entry wearing a design tool's clothes — and it is exactly the part a generator should own.
Want a brand that produces its own campaigns instead of commissioning each asset by hand? That is the kind of pipeline we wire into every platform we ship.
Start a projectQuestions, answered
- Does automating creative make every asset look the same?
- Only if the brand rules are vague. The opposite is true for a tight system: a strict, legible brand — fixed type stack, ink-scale palette, defined layout grid — has fewer free variables, so a generator reproduces it faithfully. The creative variation (headline, offer, photo, slide order) still comes from a human-edited data file; the generator only handles the mechanical production around those decisions.
- When do you use ffmpeg versus Remotion for video?
- ffmpeg handles frame-sequenced reels: we render deterministic SVG frames to PNG, then stitch the numbered sequence into a vertical MP4 with a music bed. Remotion handles data-driven video, where the pacing or scene count depends on the content — each frame is a React component reading campaign props, reusing the same brand tokens as the stills. Simple, repeatable motion goes to ffmpeg; content-reactive video goes to Remotion.
- How does one generator stay on-brand across a whole campaign?
- Templates are pure functions that can only draw with brand tokens — the headline font is always the brand's headline font, the palette is always the brand palette — so an asset physically cannot ship off-brand. The campaign data file supplies only the variable content. One brand layer feeds stills, reels, and Remotion video alike, so every asset in a set is a visible sibling.
The stack we actually use
The tools below run our own systems and the ones we build for clients. A few are affiliate links — they support The Signal at no cost to you.
Some links on this site are affiliate links. If you click through and sign up or buy, J Supreme Tech may earn a commission at no extra cost to you. We only recommend tools we actually use to build and run our own systems. Full disclosure.