Chrome Web Store Assets as Code: A Puppeteer + Sharp Pipeline
- Chrome Extensions
- Puppeteer
- Sharp
- design
- tooling
- NorthLab Folders
Chrome Web Store listings need a precise kit: screenshots at 1280×800, a 440×280 promo tile, an optional 1400×560 marquee, icons at four sizes — all under size limits, all matching your brand, all re-doable every time the UI or copy changes. The standard workflow is a designer's file that drifts out of date immediately.
For NorthLab Folders I treated the listing as a build target instead: every asset is an HTML template rendered by a script. node scripts/gen-store-assets.mjs regenerates the entire kit.
The renderer: browser as layout engine
The script drives puppeteer-core (pointing at the Chrome already on the machine — no bundled Chromium download) to load each template and screenshot it at deviceScaleFactor: 2. Rendering at 2× and downsampling beats rendering at native size — text antialiasing and 1px hairlines come out crisp instead of fuzzy.
The downsample is Sharp with the Lanczos3 kernel, followed by two store-specific transforms: flattening transparency onto the brand background (#131313) and stripping the alpha channel — the store dislikes alpha PNGs, and a 24-bit flatten is the difference between "rejected asset" and silence. The script then asserts its own output: exact target dimensions, 3 channels, under 1MB. A generator that validates its output means you find out about a violation at build time, not in the dashboard rejection email.
One stylesheet, every asset
All templates share a store.css brand-frame: the gradient background, the type stack, color tokens (text, muted subtitle, the powder-blue brand dot, folder accent colors), and a .window class — a 1px-bordered, round-topped product window that bleeds off the canvas bottom, the visual idiom every modern store listing uses. Because the brand lives in one stylesheet, restyling the entire kit is a CSS edit plus one command. The promo tile and the 1400×560 marquee are the same layout at different scales — the marquee literally scales the sidebar mock by 1.5 in CSS.
Screenshots: staged live captures, framed in HTML
Real product screenshots can't be faked, but raw screenshots make terrible store slides. The hybrid: capture the extension running against real ChatGPT and Claude dark-theme pages with staged, non-personal mock data (folders named "Work," "Research," "Side projects"), drop the raw captures into a gitignored raw/ directory, and let each slide template crop and frame the capture via CSS — percentage-based positioning that centers the sidebar and crops out account details.
So slide one is a hero crop of the docked folders in ChatGPT; slide two puts the ChatGPT and Claude sidebars side by side; three and four zoom the color picker and export menu; slide five is a text-only privacy slide — lock icon, "Private by design," three proof cards (local storage, reads only the two sites, no analytics). For a local-first product, the privacy slide isn't filler; for some users it's the screenshot that decides the install.
The framing-in-HTML split is what keeps it maintainable: when the UI changes, re-stage and re-capture; the layout, copy, and badges all survive in the templates.
Icons from the same philosophy
A sibling script generates the icon set programmatically — the "N." mark built as SVG, supersampled down to 128/48/32/16 via Sharp. Two details that only matter at small sizes: stroke widths and the dot thicken as the target size shrinks (optical compensation — a faithful 16px downscale of a 128px mark reads as mush), and the toolbar variants get a subtle 45%-opacity white rim so the dark mark survives dark browser toolbars.
Why bother
Because listings aren't write-once. Copy changes, screenshots age, stores update requirements, and every future product (or locale) needs the same kit. Assets-as-code converts each of those from a design session into a git diff and one command — the same argument as rendering my OG image from a template, scaled up to a whole marketing surface.