Skills/How GTM Vault is run / Produce
Post cover images
Renders a 1200 by 630 cover from a template with a title, subtitle and series label.
Download gtm-vault-thumbnails.zip SKILL.md plus assets/gold-template.html, assets/hero-template.html, assets/law-template.html, scripts/render_thumbnail.py.
Needs: Python with Playwright. Templates are included. Or run it without installing: connect the GTM Vault MCP and ask Claude to run gtm-vault-thumbnails.
Published by GTM Vault (gtmvault.co) as part of How GTM Vault Is Run. Swap the show names, links and templates for your own.
Generates branded 1200x630 OG/thumbnail images for GTM Vault Substack posts using the established gold (#c8a832) design system.
Design System
Three templates. Pick by content type:
1. Gold Template (assets/gold-template.html)
- Use for: Framework posts, Architecture posts, Applied Architecture posts, any general written post
- Look: Gold background, subtle grid overlay, centered black uppercase text, divider, optional subtitle
- Placeholders:
SERIES_LABEL,TITLE_TEXT,SUBTITLE_TEXT,TITLE_SIZE
2. Law Template (assets/law-template.html)
- Use for: GTM Architecture Laws series posts only
- Placeholders:
LAYER_LABEL,TITLE_TEXT,TITLE_SIZE
3. Hero Template (assets/hero-template.html)
- Use for: The GTM Architecture Laws landing page hero only, 1456x800
Rendering
Always replace SUBTITLE_TEXT before TITLE_TEXT. TITLE_TEXT is a substring of
SUBTITLE_TEXT, so the other order corrupts the subtitle.
Method A: Playwright (preferred, but often unavailable in sandboxes)
python scripts/render_thumbnail.py --template gold \
--series "GTM Vault · Architecture" --title "The Org<br>Chart<br>Is Dead" \
--subtitle "How AI-native execution is replacing roles with systems" \
--title-size "72px" --output /path/to/output.png
Method A may not work in a restricted agent sandbox. In GTM Vault's own sandbox it failed
three separate times, including with the install backgrounded via nohup to survive the bash
timeout: the 47.4 MB playwright wheel stalled mid-download and never completed. The sandbox also
had no system Chromium, no wkhtmltoimage, no weasyprint, and neither Inter nor JetBrains Mono.
Try once, give it about a minute, then move to Method B. Do not spend several minutes retrying.
Method B: render in the connected Chrome (verified working)
Chrome has the real fonts from Google Fonts, so fidelity is exact.
Critical step, do not skip: measure the real template first. Do not reimplement the CSS from reading it. An eyeballed reimplementation lands the title block several pixels off. The correct sequence:
- Load the Claude in Chrome tools and open any page on a normal origin (a Substack admin page works).
- Inject
assets/gold-template.htmlverbatim into an iframe viasrcdoc, with the placeholders already replaced. Wait ~3s for the Google Fonts@importto resolve. - Measure the rendered geometry with
getBoundingClientRect()on.series-label,.title,.divider,.subtitle, plus aRangeover each<br>-separated title line. ReadgetComputedStylefor font size, weight, letter-spacing and colour. - Draw to a 1200x630 canvas using those measured coordinates. Reference values for a 3-line title
at 64px:
- series label baseline: y = 161.1 + 13
- title lines top: 195.1 / 259.1 / 323.1, each 77.5 tall, baseline = top + 0.78 * height
- divider: y = 417.9, 60x3, centered
- subtitle baseline: y = 444.9 + 17
- grid: 1px
rgba(0,0,0,0.04)every 60px; background#c8a832 - letter-spacing must be drawn per-character; canvas
letterSpacingis unreliable
- Screenshot both the iframe and the canvas and compare before shipping. They should agree within ~2px on every element.
Getting the image out of the browser
Base64 cannot be returned through the JavaScript tool (the harness strips base64 payloads), so the data URL cannot be round-tripped into the workspace. Two routes that work:
- Upload straight to Substack (best).
POST /api/v1/imagewith{"image": "<dataURL>"}and a JSON content-type returns{id, url, imageWidth, imageHeight, bytes}. A multipartFormDatabody is rejected with "Invalid value"; it must be the JSON data URL. ThenPUT /api/v1/drafts/{id}withcover_imageset to the returned URL. - Trigger a browser download with an
<a download>click. Lands in Chrome's download folder, usually~/Downloads, which is not readable from the sandbox. Ask before downloading.
Substack cover vs social preview
Setting cover_image via the API populates the archive thumbnail but does not update the
"Social preview" card in the editor, which defaults to the first in-post image. There is no
separate cover upload-id field on the draft record. The social preview has to be set by hand
through its "Edit" button in File Settings. Always tell the host this rather than reporting the
thumbnail as fully done.
Title Sizing Guide
Title must fit the centered content area (max-width 700px, 40px padding each side). Use <br>.
| Lines | Size | Example |
|---|---|---|
| 1-2 | 72-80px | "Clip<br>Farming" at 80px |
| 3 | 60-70px | "The 8 Layers<br>of a Modern<br>Revenue System" at 64px |
| 4 | 52-58px | "AI Just Broke<br>Your Pricing.<br>Here's How<br>to Tell." at 56px |
| 5+ | 48-52px | "Distribution Without<br>Architecture Is<br>Structural Drag" at 48px |
Law thumbnails carry the full law statement, so lean to 48-52px.
Series Labels
GTM Vault · Architecturefor Architecture section postsGTM Vault · Appliedfor Applied Architecture postsGTM Vault · Annualfor annual roundupsGTM Vaultfor standalone posts
Copy rules
Follow gtm-vault-writing: no em dashes, no emojis, no exclamation marks, and no terminal period
on the subtitle. The title renders uppercase via CSS, so write it in normal case.
Subtitles should be a compressed claim, not a description. "The count was never the argument" beats "A look at how many layers a revenue system has."
Output Specs
- OG / thumbnails: 1200x630 PNG · Hero: 1456x800 PNG
- Brand gold #c8a832 · Inter (title, subtitle), JetBrains Mono (series label)
- Text inside a crop-safe zone, roughly 150px from the edges