4 min

14

14 Header Images in 3D HTML Rendered With Playwright, Without a Single AI Image

To illustrate my pillar pages without AI-generated images, I drew a single visual in HTML and 3D CSS, which Playwright renders to an image: one template, one key per page, and a script that exports and uploads the 14 headers in a few minutes.

The problem

My 13 pillar pages and one guide had AI-generated illustrations: neon blue shields and servers, the kind of image you spot immediately. I wanted a consistent family of headers, restrained, giving an impression of solidity and expertise, that can be redone with one command when a page changes.

An @ symbol engraved on the top plate of a stack of four blue plates in perspective, on a deep blue background.

The template: one HTML file, one parameter

The visual is a single HTML file. An address like template.html?s=em picks the symbol, drawn in SVG on a 100-unit grid. The base, on the other hand, is CSS perspective: four plates stacked, tilted at 57 degrees and rotated by 45:

.vis { container-type: inline-size; width: 2516px; height: 2428px; perspective: 190cqw; }
.iso { position: absolute; left: 50%; top: 56%; width: 52cqw; height: 52cqw;
       transform-style: preserve-3d;
       transform: translate(-50%, -50%) rotateX(57deg) rotateZ(-45deg); }
.face { background: linear-gradient(135deg, #1a2a8c, #0a0f33);
        border: .22cqw solid rgba(150, 168, 255, .75);
        box-shadow: 0 0 6cqw rgba(116, 138, 255, .35); }

All dimensions are in cqw, proportional to the container’s width: the rendering stays identical whatever the size. A small script places each plate at its height (translateZ) and adds five thin layers on the edge to give it thickness.

A &guide parameter displays the safe zone as a dashed outline (189 px left and right, 158 px top and bottom): that is where the symbol must fit, because the theme crops the image depending on the screen.

The rendering: Playwright and ffmpeg

A script opens the template once per page, waits for the fonts, takes the screenshot at 2516 × 2428 (twice the display size) and converts it to WebP:

const { chromium } = require('playwright');
const { execFileSync } = require('child_process');
const pillars = require('./pillars');   // [{ s: 'em', slug: 'email-marketing', … }]

(async () => {
  const browser = await chromium.launch({ channel: 'msedge' });
  for (const p of pillars) {
    const page = await browser.newPage({ viewport: { width: 2516, height: 2428 } });
    await page.goto('file:///…/template.html?s=' + p.s, { waitUntil: 'networkidle' });
    await page.evaluate(() => document.fonts.ready);
    await page.screenshot({ path: `export/hero-${p.slug}.png` });
    execFileSync('ffmpeg', ['-y', '-i', `export/hero-${p.slug}.png`, '-quality', '88', `export/hero-${p.slug}.webp`]);
    await page.close();
  }
  await browser.close();
})();

Each WebP weighs about 160 KB. A new page means one symbol to draw and one line in the list.

The upload through the API

A second script uploads each image to the media library, fills in the alt text, title and description, then sets it as the page’s featured image. It writes the old and new image to a log: to roll back, you just put the old one back. The alt text is generated (“base of blue plates topped with a symbol”), with a small function handling the grammar so it never ends up awkward.

Three rounds of art direction

The script is the easy part. The style took three successive proposals:

  1. A diagram, a close-up, a symbol. My recommendation was a diagram of connected bricks, with text. Feedback: “not that much text next to a title and a subtitle”.
  2. Two versions without text, a silent diagram and a lone symbol. Feedback: rejected, “I want a designed, punchy visual, one that conveys solidity, seriousness and expertise”.
  3. Three objects in 3D CSS and SVG: a sculpted object under a beam of light, a stack of plates, a precision dial. The stack of plates won.

What I took from it, which became the rule: no text in the image, no labeled bricks, no flat pictograms, no teaching diagram. Only the symbol changes from page to page (a bubble for SMS, a shield for deliverability, scales for the comparison).

The limits

  • The rendering depends on the browser. The 3D CSS is rendered by Chromium: as long as you use the same one, the images are identical from one day to the next.
  • Fonts must be ready before the capture, hence the wait for document.fonts.ready.
  • A symbol is drawn by hand, in SVG. Claude proposes paths, but you have to look at them.
  • The Black Friday guide got its own header: the stack of plates didn’t say “promotional email”, so it was replaced the same day by a wall of tilted emails, also in HTML, with fictional brands.

Key takeaway

A visual in HTML can be redone, versioned and rendered by script, and it doesn’t look like a generated illustration. Fix the template, one key per page, a rendering script and an upload script that keeps the old image: the longest part is finding the style.

Read next

CanvaDitching Cheap AI Images: Claude Drives Canva and Figma0Never Let AI Invent an Interface: My Rule for Product Visuals7Seven Pages Rebuilt in Two Days: The Skill That Redoes a Page End to End