stack-diagram
Produces engineering-drawing-style architecture diagrams: real logos on white tiles, labeled group boxes (like cloud-vendor VPC diagrams), orthogonal arrows with monospace labels, on a cool graph-paper background. One self-contained HTML file is the source of truth; PNG / TSX / artifact are derived from it.
Requirements
- Python 3.9+ (standard library only for and )
- Network access to the icon sources (svgl.app, raw.githubusercontent.com, cdn.jsdelivr.net, plus the brand's own site or repo for ) when fetching icons
- Playwright + Chromium for :
pip install playwright && python3 -m playwright install --with-deps chromium
Workflow
- Settle the content (nodes, groups, edges)
- Fetch icons —
- Plan coordinates — read
- Build the HTML from —
- Render a PNG and look at it before delivering
- Deliver in the requested format(s)
Step 1 — Content
From the user's list of technologies/services, decide:
- Nodes: each tech = one node (name + a short lowercase role like , )
- Groups: draw a box around things that share a runtime, vendor, or repo (e.g. a cloud provider box containing a runtime box; a monorepo box for tooling). Nesting one level deep is fine; deeper gets noisy. Write group labels in the brand's own casing (, ); the template shows them as written.
- Edges, three kinds:
- accent-colored solid = the primary request/data flow (one continuous path reads best)
- neutral solid = secondary internal connections
- neutral dashed = dev-time / deploy-time relationships (CI, git hooks, IaC deploy)
If the user gave only a flat list of tools, infer sensible groups and flows from what the tools are — only ask when a tool is ambiguous enough that the diagram would be wrong either way.
Step 2 — Icons
bash
python3 scripts/fetch_icons.py <icons-dir> tanstack hono drizzle "betterauth:better-auth" "orpc=https://orpc.unnoq.com/logo.webp"
Spec forms:
(name used as slug),
(different slug),
(explicit URL). The script tries, in order:
svgl → devicon → gilbarbara/logos, writes files plus
, and flags icons whose fill is white. svgl (svgl.app) is first because it carries current official marks for modern dev brands (TanStack, Better Auth, Hono...). When a logo has light/dark versions, the light one becomes
(tiles are white in both themes) and the dark one is saved as
for group labels. svgl is matched by exact title only, so a miss just falls through. Its API also exposes
variants — occasionally useful for a wide group-label, never for tiles.
Rules that matter more than the mechanics:
- Never recolor a logo. Users notice. If a logo is white or too light for a white tile, put it on a dark tile ( class) instead of touching its fill. The manifest's flag is a heuristic — a white fill can be an inner detail on a colored mark (tRPC, Ubuntu), so glance at the SVG before reaching for the dark tile.
- Official marks only. simple-icons is deliberately not a source: its glyphs are monochrome redraws, and users call them wrong on sight. When every source misses (), find the official file and pass it as — look in the project's repo (root , , , ) and on its site (, , ). Some marks live only in the repo (Lefthook's is at the root of ).
- Abstract concepts get pictograms, not text. A node like "dev environment", "cron", "queue", "cache", or "webhook receiver" has no brand — reach for a Lucide line icon via (e.g. , ; browse lucide.dev/icons). No text-in-a-tile, no emoji.
- Vendor sub-products (Cloudflare D1/R2/KV, AWS's endless services...) usually have no distinct logo: just reuse the vendor logo on the tile and let the node name carry the distinction ("Cloudflare D1" / "sqlite db").
- Non-brand nodes (user, browser, internet, devices, bare-metal server): copy a glyph from into the icons dir under the node's name, or use — don't use emoji. These aren't brand logos, so tinting them to the diagram's ink color is fine (the fetch script does it for Lucide automatically).
- Group-label icons render at 14px — they need a square glyph mark. Wordmarks and wide lockups turn to mud at that size; if the main icon is a wordmark, fetch the brand's square icon separately for the label (svgl usually has it; otherwise the project's own icon or favicon file).
- Group labels sit on the group's tint, not on a white tile, so in the dark theme a mark with black parts vanishes there. If saved , use the template's / pair in the label; if there is no dark variant and the mark has black parts, add class to the label's (white backing, dark theme only).
- Raster logos (png/webp) are fine; the build script embeds everything as data URIs, so nothing is loaded at view time.
Step 3 — Layout
Read
for the coordinate system, spacing constants, and — most importantly — the
edge anchoring rules that prevent arrows from running through label text. The one-line version: a node is its tile
plus the text under it; vertical arrows connect node-box boundaries, never tile-to-tile through text.
Step 4 — Build
Copy
, replace the sample GROUPS / EDGES / NODES / header sections with yours (the template's markup doubles as format documentation), then inline the assets. Set
on
to the language of the diagram's labels (
by default; use
for Japanese so browsers pick Japanese glyph variants). Two options the template already carries: a
dark variant (set
on
; tiles stay white so logos survive, group-label icons follow the Step 2 rule, and arrowheads and legend swatches follow the tokens automatically), and a "deploy to platform X" edge where X is a group, not a node — point the arrow at the most representative node
inside the group and label the edge (
), which reads correctly.
bash
python3 scripts/build.py my-diagram.html <icons-dir> out.html --fonts <skill>/assets/fonts
Icons are referenced in markup as
; fonts as
(Geist) /
(JetBrains Mono). The build fails loudly on unresolved tokens. Keep every color and font behind the CSS custom properties already in the template — tweak the tokens, not scattered values.
Step 5 — Verify by looking
Render and actually read the image:
bash
python3 scripts/render_png.py out.html check.png --selector .canvas --scale 2
If the diagram contains Japanese text, the render host needs a Japanese font, or CJK characters silently come out in Chinese glyph variants (users notice immediately). The script warns and prints a fix; the template's font stack already names the Noto JP faces.
Checklist while looking at it:
- No arrow crosses label text; no arrowhead lands inside text
- Edge labels readable (they have a paper-colored halo — confirm it's working)
- Every icon visible (white-on-white is the classic failure → dark tile), including group-label icons in the dark render
- Group labels sit in their group's top-left corner, clear of nodes and edges; each group's left and right padding match
- Nothing sticks out of its group box; nothing clipped at canvas edge
Fix and re-render until clean. This loop is cheap; shipping a diagram with overlapping text is not.
Step 6 — Deliver
- HTML (default): the built file itself. Self-contained, works offline.
- PNG: always produced by screenshotting the built HTML with headless Chromium (, for crisp text) — never by drawing a separate image with matplotlib/PIL/standalone SVG rasterizers; the HTML is the single source of truth and the browser's text/SVG rendering is what makes it look right. By default the script emits BOTH themes ( light + ) with square corners (radius is stripped for the screenshot only) — deliver both unless the user asked for one; captures the document as-authored. Target for just the diagram, for the whole page.
- TSX: convert per (self-contained React component, no deps).
- Artifact (where the environment offers an artifact or page-publishing tool): publish the built HTML with the environment's artifact tool. Everything is already inlined as data URIs, so the artifact's blocked-network sandbox is a non-issue. Prefer this when the user will iterate on the diagram in conversation; pair it with a PNG when they need something to paste elsewhere.
If several formats are requested, build the HTML once and derive the rest from it.
In the footer, credit the icon sources actually used — the
field of each entry in the icons dir's
(e.g.
icons: svgl / devicon / Lucide
). Brand logos remain the trademarks of their owners. Keep the template's "made with stack-diagram" link after the credits unless the user asks to remove it; a default PNG captures only the canvas, so the link stays out of it.