Engineering NotebookTech

Generative Black-and-White Cover Art: An SVG Pipeline With No Stock Images

How we generate unique monochrome cover art for every article from code — keeping a strict black-and-white house style at zero stock-photo cost.

JS
J Supreme Studio
Engineering · May 28, 2026 · 9 min read
Sharer/Y𝕏inf

Every article on this blog ships with its own cover image. None of them are stock photos. None of them were touched by a designer. They are generated from code — a seed string in, a unique black-and-white composition out — and the whole system fits in a file you could read over coffee. This is how it works, and why generating art beats buying it.

The constraint that started this was boring: a content publication needs cover art, and our house style is strict black-and-white minimalism with no color accent. Stock photography fails that brief on contact — it is full of color, full of other people's faces, full of the same eight laptops-on-a-desk shots every other studio uses. The honest options were pay a designer per post or generate it. We generate it.

Why generative beats stock for a brand system

Stock images are a branding liability dressed up as a convenience. They drag in colors you did not choose, lighting you cannot control, and a visual vocabulary shared with every competitor who shopped the same library. For a studio whose entire identity is the absence of noise — Space Grotesk, an ink scale, hairlines, no accent hue — a photo of a smiling team in a glass office is not on-brand. It is anti-brand.

Generative art inverts the trade. The palette is whatever you write into the code. The composition obeys rules you set. And because the same input always yields the same output, the system — not a human's taste on a given afternoon — guarantees consistency across hundreds of covers. You are not picking images that happen to look related. You are running a function that cannot produce an off-brand result.

  • Brand lock — the palette lives in code. A mono cover cannot accidentally ship with a blue gradient because blue is not in the function.
  • Zero marginal cost — the hundredth cover costs the same as the first: one render call.
  • Uniqueness for free — each slug seeds a different composition, so no two posts collide and nothing is shared with the rest of the internet.
  • License-clean — nothing is sourced from anyone. There is no attribution, no rights expiry, no copyright audit waiting to bite you later.

Determinism: same seed, same art

The non-negotiable property is determinism. Given a slug, the generator must always produce the exact same image. That is what lets a cover be regenerated on demand instead of stored as a binary blob, what makes the output diff-able in review, and what means a colleague running the pipeline on another machine gets pixel-identical results.

Math.random() is the enemy here — it is seedless and gives you a different picture every run. Instead we hash the slug into a 32-bit integer and feed it to a tiny seeded PRNG (mulberry32 is plenty). The hash makes the seed; the PRNG makes a repeatable stream of numbers; every geometric decision downstream pulls from that stream.

typescript
// Deterministic seed from any string (slug, title, motif).
function hashSeed(str: string): number {
  let h = 1779033703 ^ str.length;
  for (let i = 0; i < str.length; i++) {
    h = Math.imul(h ^ str.charCodeAt(i), 3432918353);
    h = (h << 13) | (h >>> 19);
  }
  return (h >>> 0);
}

// Seeded PRNG — same seed in, same number stream out.
function mulberry32(seed: number) {
  return function () {
    seed |= 0;
    seed = (seed + 0x6d2b79f5) | 0;
    let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}

const rng = mulberry32(hashSeed("generative-mono-svg-cover-art-pipeline"));
const pick = <T,>(arr: T[]) => arr[Math.floor(rng() * arr.length)];
const range = (lo: number, hi: number) => lo + rng() * (hi - lo);

From here, rng() is the single source of all variation. Position, count, rotation, stroke width, which motif fires — every number that shapes the composition is drawn from that one seeded stream. Change the slug, get a new picture. Keep the slug, get the same picture forever.

Geometric primitives and the motif system

The art itself is built from a small kit of primitives — lines, circles, arcs, dots, concentric rings, grids — composed by motifs. A motif is a named layout strategy: grid, orbit, wave, circuit, pulse, ledger, nodes, aperture, signal. Each blog post declares one (this one is circuit), and the generator switches on it to lay primitives out in a characteristic way. The motif gives a post-to-post family resemblance while the seed guarantees each instance is unique.

The trick that keeps it on-brand is that primitives never choose their own color. They draw with abstract roles — ink, line, faint — and the palette layer resolves those roles to actual values. Swap the palette and the entire system re-skins; with our palette, every role resolves to a shade of black, white, or grey.

typescript
// The whole palette. Mono by construction — no hue exists to leak.
const MONO = {
  bg:    "#ffffff",
  ink:   "#0a0a0a",
  line:  "#0a0a0a",
  faint: "#e5e5e5",
} as const;

// 'circuit' motif: nodes on a grid joined by right-angled traces.
function circuit(rng: () => number, w: number, h: number): string {
  const cols = 6 + Math.floor(rng() * 4);
  const step = w / cols;
  const parts: string[] = [];
  let x = step / 2;
  let y = h / 2;
  for (let i = 0; i < cols; i++) {
    const ny = y + (rng() < 0.5 ? -1 : 1) * step * (rng() < 0.6 ? 1 : 0);
    parts.push(`<path d="M${x} ${y} H${x + step / 2} V${ny}"` +
      ` stroke="${MONO.line}" stroke-width="1.5" fill="none"/>`);
    parts.push(`<circle cx="${x}" cy="${y}" r="${3 + rng() * 4}"` +
      ` fill="${MONO.ink}"/>`);
    x += step; y = ny;
  }
  return parts.join("");
}

Wrap that fragment in an <svg> with the mono background, a subtle hairline border, and the slug's title set in the brand typeface, and you have a finished cover — as text. SVG is the right substrate precisely because it is text: tiny, resolution-independent, trivially generated by string concatenation, and reviewable in a pull request like any other code.

Fig 1 — the circuit motif: nodes seeded on a grid, joined by right-angled traces; same slug always renders the same path.

The render path: SVG to PNG with puppeteer and sharp

SVG renders natively in the browser, so for in-app use — the cover on the article page itself — we just inline it. But the open graph and social-card world wants raster: an actual PNG at a fixed size that Twitter, WhatsApp, and LinkedIn can thumbnail. That is the half of the pipeline we reuse across nearly every project in the studio, from launch kits to highlight covers.

There are two render paths and we use both depending on the job. For pure SVG-to-PNG with no layout or web fonts to resolve, sharp rasterizes the SVG string directly — it is fast and has no browser to spin up. When the cover needs real CSS, web-font loading, or HTML composition around the SVG, we render through headless Chrome with puppeteer and screenshot the node. Same pattern that drives our generative content pipelines everywhere else.

typescript
import sharp from "sharp";
import puppeteer from "puppeteer-core";

// Path A — pure SVG string, no fonts/CSS: sharp is fastest.
export async function svgToPng(svg: string, out: string) {
  await sharp(Buffer.from(svg))
    .resize(1200, 630, { fit: "cover" }) // OG card size
    .png()
    .toFile(out);
}

// Path B — needs web fonts / CSS layout: headless Chrome.
export async function htmlToPng(html: string, out: string) {
  const browser = await puppeteer.launch({
    channel: "chrome",
    args: ["--no-sandbox"],
  });
  const page = await browser.newPage();
  await page.setViewport({ width: 1200, height: 630, deviceScaleFactor: 2 });
  await page.setContent(html, { waitUntil: "networkidle0" });
  await page.screenshot({ path: out, type: "png" });
  await browser.close();
}
  1. Hash the slug to a seed and build the seeded RNG.
  2. Select the motif, draw the primitives, resolve roles through the mono palette — producing one SVG string.
  3. Inline that SVG for the on-page cover; rasterize it for the OG card.
  4. Pick the render path: sharp for plain SVG, puppeteer for anything that needs CSS or web fonts.
  5. Write the PNG to disk (or stream it) at 1200x630 — every social platform's preferred card size.

Stop buying images that look like everyone else's. Write the function that can only produce yours.

Studio principle

Where this pattern travels next

The same three ideas — a seed, a palette in code, a headless render — power far more than blog covers. They generate Instagram launch puzzles, highlight-cover sets, OG cards, and the still frames that get composed into reels with ffmpeg and Remotion. The blog is just the simplest expression of it. If you want the social-card angle specifically, we wrote it up in Dynamic OG Images at the Edge; for the motion side, see Creative Automation at Scale.

The deeper lesson is about constraints. A strict black-and-white house style reads as a taste decision, but for a pipeline it is an engineering decision: fewer free variables means deterministic output, which means no human in the loop, which means a small team ships hundreds of on-brand assets without a designer on call. Minimalism is not just how it looks. It is what makes the automation possible.

0
stock photos used
1200x630
OG card, every post
1
function, infinite covers

Want a brand system that generates its own assets instead of buying them? That is the kind of thing we build into every platform we ship.

Start a project
#svg#generative#automation#design-systems#next.js

Questions, answered

Why generate cover art instead of using stock photos?
Three reasons: brand control, cost, and licensing. Stock images bring in colors, lighting, and a visual vocabulary you did not choose and that competitors also use. Generative art puts the palette and composition rules in code, so every cover is on-brand by construction, costs nothing per render, is unique to your slug, and carries no copyright or attribution risk.
How do you guarantee the same article always gets the same cover?
Determinism. We hash the slug into a 32-bit seed and feed it to a seeded PRNG (mulberry32) instead of Math.random(). Every geometric decision — position, count, rotation, which motif — draws from that single seeded stream, so the same slug always produces a pixel-identical SVG, and we pin the Chrome renderer so the PNG is identical too.
When do you use puppeteer versus sharp to make the PNG?
Sharp rasterizes a plain SVG string directly and is fastest when there are no web fonts or CSS layout to resolve. Puppeteer drives headless Chrome and is used when the cover needs real CSS, web-font loading, or HTML composed around the SVG. Both output a 1200x630 PNG for social cards.
Recommended · affiliate

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.

Found the signal useful? Pass it on.

Sharer/Y𝕏inf
The Communications Debrief

Get the signal in your inbox.

Field notes on building software, marketing systems and the markets — what we shipped, the problems and the fixes. One clean dispatch, no noise.

Double opt-in. No spam, unsubscribe anytime.