GTM Vault
Menu

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:

  1. Load the Claude in Chrome tools and open any page on a normal origin (a Substack admin page works).
  2. Inject assets/gold-template.html verbatim into an iframe via srcdoc, with the placeholders already replaced. Wait ~3s for the Google Fonts @import to resolve.
  3. Measure the rendered geometry with getBoundingClientRect() on .series-label, .title, .divider, .subtitle, plus a Range over each <br>-separated title line. Read getComputedStyle for font size, weight, letter-spacing and colour.
  4. 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 letterSpacing is unreliable
  5. 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/image with {"image": "<dataURL>"} and a JSON content-type returns {id, url, imageWidth, imageHeight, bytes}. A multipart FormData body is rejected with "Invalid value"; it must be the JSON data URL. Then PUT /api/v1/drafts/{id} with cover_image set 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>.

LinesSizeExample
1-272-80px"Clip<br>Farming" at 80px
360-70px"The 8 Layers<br>of a Modern<br>Revenue System" at 64px
452-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 · Architecture for Architecture section posts
  • GTM Vault · Applied for Applied Architecture posts
  • GTM Vault · Annual for annual roundups
  • GTM Vault for 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

Every GTM Vault skill