Trade Book Central · June 20, 2026 · 7 min read
Shipping a 15-day build-in-public carousel series
Built a data-driven LinkedIn carousel generator for Trade Book Central and produced a full 15-day build-in-public series: 90 on-brand slides and 15 print-ready PDFs rendered from JSON specs with node-canvas.
By Andy D — Founder, Creative Brain Inc. — Brampton, Ontario

Every slide is generated from a JSON spec. The look (brand hex, app icon, candlestick chart band, node-network) is pulled straight from the product, not eyeballed.
TL;DR
- Built a data-driven carousel generator: a single node-canvas renderer turns a per-post
slides.jsoninto six 1080x1350 PNG slides plus one multi-page PDF for LinkedIn document posts. - Produced a complete 15-day build-in-public series for Trade Book Central documenting its MEAN to Next.js, Supabase and Stripe migration: 15 posts, 90 slides, 15 carousel PDFs, each with a copy-paste caption.
- Pulled the visual system from the product itself: exact brand hex from the app's
globals.css, the real app icon as the logo chip, and a static port of the marketing homepage's animated candlestick, EMA and node-network background. - Seven reusable slide layouts (cover, bullets, stat, code, quote, CTA, statement) driven entirely by JSON. Re-rendering any day is one command.
- Locked a writing style for every caption and slide: short, human, first person, no emojis, no em-dashes.
Wiring: no new services. The renderer reuses the node-canvas build and font library shipped with an existing local design skill. No DB, no env vars.
Why we shipped this
Trade Book Central is being rebuilt from a MEAN stack onto Next.js, Supabase and Stripe. That migration is a good story, and the plan was to document it as a daily build-in-public series on LinkedIn to grow an audience around the product while the work happens.
The problem is volume and consistency. A 15-day carousel series is 90 individual slides. Hand-designing that in Canva is slow, and by slide 40 the brand has quietly drifted: spacing wanders, the greens stop matching, the candlestick motif from the product never makes it in. We wanted the carousels to look like they came out of the app, to be trivially editable, and to be reproducible if a number or a line of copy changed.
So instead of designing 90 slides, we built one renderer and wrote 15 small content files. The design lives in code, the words live in JSON and Markdown, and any slide is one command away from being regenerated.
Files added or modified
All paths are inside the Trade Book Central repo, under Marketing/Social Media Posts/MEAN-to-NextJS Build Series/.
New
_engine/render.js— the node-canvas renderer: seven layouts, the "Market Cartography" graphics layer, and PDF bundling.CONTENT-PLAN.md— the 15-post narrative arc with per-day angle, real detail and accent colour.PROGRESS.md— a resumable build tracker so a future session can pick up cold.DESIGN-PHILOSOPHY.md— the visual philosophy the graphics layer expresses.Day NN - <Title>/slides.json(x15) — the six-slide content spec per post.Day NN - <Title>/post.md(x15) — metadata table, copy-paste caption, slide summary, posting checklist.Day NN - <Title>/slide-1..6.png+<slug>-carousel.pdf(x15) — the rendered assets.
Reused (not modified)
- The
canvas-designlocal skill's node-canvas build andcanvas-fontslibrary (Big Shoulders, Work Sans, JetBrains Mono). - The product's
apps/web/app/globals.css(brand tokens) andcomponents/marketing/hero-background.tsx(the homepage scene we ported).
Design rationale
One renderer, seven layouts, zero hand-design
Each post is a slides.json file: an array of slides, each with a layout and a few fields. The renderer owns all the visual decisions; the JSON owns only the words and the accent. The rejected alternative was bespoke code per slide, which is just hand-design with extra steps and the same drift problem.
{
"day": 8,
"accent": "#0fcc5d",
"slides": [
{ "layout": "stat", "value": "1000s", "unit": "of trades",
"label": "Every position, every user, carried across." },
{ "layout": "code", "title": "A rule the database enforces itself",
"lines": ["create policy own_trades on trades", " using ( user_id = auth.uid() );"] }
]
}
The same seven layouts cover the whole series, so a stat slide on day 8 and a stat slide on day 14 are visually identical except for the numbers.

The stat layout. The value auto-shrinks to fit the canvas, so "0", "256" and "1000s" all sit correctly without manual tweaking.

The code layout renders a terminal window with simple line-prefix colouring, used for SQL, shell and config snippets across the series.

The bullets layout. Four short lines is the cap, which keeps the copy skimmable and forces tighter writing.
Brand fidelity by reuse, not eyeballing
The brand colours are read from the app's globals.css (brand green #0b9444, profit #0fcc5d, loss #f26d6d, marketing orange #f26522, deep navy base), the logo chip is the actual app icon PNG, and the ambient background is a static port of the homepage's animated hero: candlesticks, two EMA lines, a node-network and drifting "price" ribbons. Nothing here is approximated by eye, which is the only way 90 slides stay on-brand.
Bold colour-block slides for rhythm
Every post includes one saturated colour-block quote slide so a swipe-through has a beat instead of six near-identical dark cards. The accent rotates across the series (green, orange, teal, red, profit), and on those bright slides the candlestick motif persists as a tonal silhouette so the visual system never disappears.

A green colour-block quote slide. The chart band stays present as a tonal silhouette rather than disappearing on the bright background.

The same layout on the loss-red accent, used for the "war story" posts. One JSON field (bg) switches the whole palette.
Gotchas we hit (so the next log doesn't repeat them)
- node-canvas fontconfig is cwd-sensitive on Windows. Registered fonts silently fell back to a generic sans. Root cause: the bundled fontconfig initialises at
require()time, keyed to the process's startup working directory, so registered faces only resolve when the cwd is the skill install. Fix:process.chdir(SKILL_DIR)beforerequire('canvas'), and resolve the post-folder argument against the original cwd so relative paths still work. - Numeric font weights break face matching. Strings like
"700 92px Family"fell back to sans. Fix: register each weight under its own short family alias and use theboldkeyword in the font string, mirroring the pattern that already worked in the design skill. - Big stat values ran off the canvas. A long value such as "1000s" overflowed because its width was measured with the smaller unit font still active. Fix: measure with the display font, auto-shrink the value to fit the content width, and stack the unit on its own line beneath the number.
- The chart band collided with the code terminal. Raising the chart band to sit between the body text and the footer pushed it into the tall terminal box on code slides. Fix: shortened the terminal height so the raised band always clears it.
Verifying the work
# from the series folder: re-render any single day
node "_engine/render.js" "Day 08 - Migration Day"
# confirm each day produced 6 PNGs + 1 PDF, across all 15 days
for d in Day*/; do
png=$(ls "$d"slide-*.png 2>/dev/null | wc -l)
pdf=$(ls "$d"*.pdf 2>/dev/null | wc -l)
printf "%-34s png:%s pdf:%s\n" "$d" "$png" "$pdf"
done
Manual check: open any *-carousel.pdf, swipe through the six pages, and confirm the brand colours, the app-icon chip in the header, and the candlestick-plus-EMA band at the foot of each slide. Honest status: verified locally across all 15 days (90 slides, 15 PDFs, no missing assets); LinkedIn engagement metrics are pending until posting begins.
What's next
- Post the series on a cadence — publish each day's PDF and caption to LinkedIn, then fill the Date Posted column in
PROGRESS.mdand eachpost.md. - Cross-platform adaptation — generate square and vertical variants for X, Instagram and TikTok from the same JSON specs, no recopy of content.
- One combined captions file — concatenate the 15 captions into a single scheduling-friendly document.