Unified HTML→PNG asset render harness for social bugs, video cards, apparel mockups, and any screenshot-based asset pipeline.
This tool consolidates the hand-rolled pattern that was rebuilt independently across:
flickdaymedia/scripts/story-assets/render-*.mjs— social bugs, reel overlays, logosletspepper/scripts/story-assets/render-*.mjs— social bugs, highlight cards, standingsletspepper/scripts/apparel/render-apparel.mjs— DTF print art + mockups
Every script launched a headless browser, loaded an HTML template, injected data, and saved a PNG. render-kit makes the next one a single command, not a new script.
- Template → PNG (below): the original harness — one HTML template + data → asset(s).
- Walkthrough → many outputs (templates/walkthrough/CONTRACT.md): one captured walkthrough manifest (a still + hotspot + caption per step) → an interactive click-through player and a motion video, from the same stills, so they can't drift.
# interactive click-through (self-contained folder: index.html + copied stills)
render-kit walkthrough create-tournament.interactive.json --emit interactive --out-dir out/create-tournament
# motion video over the stills (Ken-Burns push + spotlight + captions; silent)
render-kit walkthrough create-tournament.interactive.json --emit video --out out/create-tournament.mp4 --canvas 1920x1080The manifest is produced by an app-specific capture harness (rally-hq's tests/e2e/record-interactive.spec.ts is the reference producer). Per-step motion is decided by lib/hotspot-motion.mjs — pure and unit-tested (node lib/hotspot-motion.mjs --selftest). Capture at 3× DPI when the stills feed the video emitter, so deep zooms stay crisp. Narrated promo video is deliberately a different tool (forge-signal/templates/demo-reel), not this lane.
cd /Users/nino/Workspace/dev/tools/render-kit
npm install
chmod +x bin/render-kit.mjs
npm link # global symlink, optionalOr run directly:
node /Users/nino/Workspace/dev/tools/render-kit/bin/render-kit.mjs [options]Render a single asset:
render-kit templates/example/social-card.html \
--data templates/example/data.json \
--out /tmp/render-kit-test.pngRender multiple variants:
render-kit templates/example/social-card.html \
--out-dir ./output \
--variants templates/example/variants.jsonrender-kit <template.html> [options]
Options:
--data <file.json> Inject JSON data (exposed as RENDER_DATA global)
--out <file.png> Output single PNG file
--out-dir <dir> Output directory (use with --variants)
--selector <selector> CSS selector of element to crop (default: full page)
--scale <N> Device scale factor (default: 1, e.g. 2 for 2x)
--width <N> Viewport width in pixels
--height <N> Viewport height in pixels
--variants <file.json> Array of variants [{name, data}, ...] to render
--help Show help
Option 1: CLI flags (required)
render-kit template.html --width 1080 --height 1920 --out output.pngOption 2: HTML comment in template (optional)
Add to the <head> of your template:
<!-- viewport: 1080x1920 -->Then omit --width and --height.
Your template receives JSON data as window.RENDER_DATA. Use it to populate dynamic content:
<script>
if (window.RENDER_DATA) {
const { title, subtitle } = window.RENDER_DATA;
document.getElementById('title').textContent = title;
}
</script>CLI:
render-kit template.html --data data.json --out output.pngdata.json:
{
"title": "My Title",
"subtitle": "Subtitle"
}Use --variants to render one PNG per array entry. Each entry can override data:
render-kit template.html --out-dir ./output --variants variants.jsonvariants.json:
[
{
"name": "card-a",
"title": "Variant A",
"subtitle": "First output"
},
{
"name": "card-b",
"title": "Variant B",
"subtitle": "Second output"
}
]Renders: output/card-a.png, output/card-b.png
Crop to a specific element (useful for transparent alpha assets):
render-kit template.html --selector ".pill" --out output.pngRequires the selector to exist in your HTML. Screenshots with omitBackground: true for transparency.
Render at higher DPI for print-ready assets (2x = double resolution):
render-kit template.html --scale 2 --out output.pngSteps:
- Extract the HTML template from the script's functions into a standalone
.htmlfile - Convert data injection from template literals to
window.RENDER_DATA:- Old:
const greeting = (name) =>${name} - New: Template +
document.getElementById('name').textContent = window.RENDER_DATA.name
- Old:
- Move the jobs array to a
variants.jsonfile with{name, ...dataFields} - Replace the script with a single
render-kitcommand
Example: Porting social-bugs
Old script (render-social-bugs.mjs):
const HANDLES = [
{ slug: 'letspepper', handle: 'letspepper.open' },
{ slug: 'flickday', handle: 'flickday.media' },
]
// ... loop over HANDLES, build jobs, screenshotNew approach:
- social-card.html — template with
${handle}placeholder replaced bywindow.RENDER_DATA.handle - variants.json —
[{name: 'ig-letspepper', handle: 'letspepper.open'}, ...] - Command:
render-kit social-card.html --out-dir ./output --variants variants.json
Use @import url() in your CSS (same as the existing scripts):
@style
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono&display=swap');
</style>render-kit waits for document.fonts.ready before screenshotting, so fonts always load.
If you need offline rendering, embed fonts as data URIs in your CSS (as done in flickdaymedia/_brand-v2.mjs):
@font-face {
font-family: 'MyFont';
src: url('data:application/font-woff2;base64,...');
}render-kit templates/social-pill.html \
--data bug-data.json \
--selector ".pill" \
--scale 2 \
--out ./social-bugs/ig-handle.pngrender-kit templates/highlight-intro.html \
--out-dir ./highlights \
--variants teams.jsonrender-kit templates/apparel-back.html \
--scale 1 \
--out ./print/back-12in.png
# Then stamp DPI if needed:
# magick ./print/back-12in.png -density 300 ./print/back-12in.png- Browser: Playwright's chromium
- File handling: Renders to a temp HTML file via
file://URL - Fonts: Waits for
document.fonts.ready+ 250ms buffer - Cleanup: Temp files removed after each render
- Concurrency: Sequential renders (one page at a time)
MIT