Social cards

Image posts and covers for social platforms. One source file gives a PNG per card at the platform’s pixel size, a gallery page that can show each cover at feed size, and a PDF for LinkedIn documents.

npx slidehtml cards new solar-post       # creates src/cardsets/solar-post/index.tsx
npx slidehtml cards dev solar-post       # gallery preview
npx slidehtml cards check solar-post     # fonts, typecheck, export, QA; last line is PASS or FAIL
npx slidehtml cards build solar-post     # writes dist/cards/solar-post.html and the PNGs in dist/cards/solar-post/

Files live in src/cardsets/<name>/index.tsx. You are done at PASS, after looking at every PNG in dist/cards/<name>/ at full size and at every qa/cards/<name>/feed-NN-<preset>.png, which is the cover shrunk to its estimated feed width.

check needs Playwright’s Chromium (npx playwright install chromium). Its font step needs Python 3 with fonttools and brotli, and the Noto CJK .ttc files (apt install fonts-noto-cjk fonts-noto-cjk-extra, or set NOTO_CJK_DIR). Without them check ends in FAIL. build skips the font step and QA, so use it after a check, not instead of one.

A set

import type { CardSetDef } from '../../cards'

export const set: CardSetDef = {
  title: 'My card set',
  preset: 'xhs-portrait',      // size for every card, unless a card sets its own `preset`
  theme: 'cobalt',
  cards: [
    { t: 'cover', lines: ['三五个字', '说清钩子'], mark: [1], hint: '左滑', source: null },
    { t: 'number', value: '2,779', unit: 'TWh', label: 'What the number is', source: 'Publisher, year' },
    { t: 'ending', lines: ['Last card'], cta: 'One ask only', source: null },
  ],
}

CardSetDef also takes style, iconStyle, lang (zh or en), handle (your account name on each card, ≤ 14) and pages (page numbers on inner cards).

In the gallery, Feed size shows each cover at its estimated feed width and Safe zone draws the safe zone, blocked strips and crop over each card.

Presets

presetPlacementPixelsRatioEvidenceFeed width
xhs-portrait小红书 portrait1080×14403:4community180
xhs-square小红书 square1080×10801:1community180
xhs-landscape *小红书 landscape1440×10804:3community180
bili-wide *B站 cover1920×108016:9conflict180
bili-mobile *B站 cover1200×9004:3unconfirmed180
douyin-cover *抖音 cover1080×19209:16community128
channels-portrait *视频号 portrait cover1080×12606:7community128
channels-wide *视频号 landscape cover1080×60816:9community128
wechat-head *公众号 lead article900×3832.35:1community350
wechat-thumb *公众号 secondary article200×2001:1community60
weibo-square微博 nine-grid1080×10801:1community120
weibo-article *微博 article header980×55016:9community350
ig-portraitInstagram carousel1080×13504:5community128
ig-tallInstagram carousel1080×14403:4community128
linkedin-docLinkedIn document, also writes a PDF1080×13504:5community360
yt-thumb *YouTube thumbnail1280×72016:9official168
og-image *Open Graph1200×6301.91:1official360
x-card *X link card1200×6281.91:1conflict360

* Cover-only: every card on this preset takes the feed check. Feed width is in CSS px on a 390 px phone, and every value is an estimate.

What the evidence label means:

  • official: read on the platform’s page or in a search excerpt of it. For yt-thumb this covers the ratio and minimum width; 1280×720 and the duration corner come from third parties. For og-image the Meta page was read only through a search excerpt.
  • community: third-party guides that agree. The platform has not confirmed it.
  • conflict: sources disagree, so the value is a choice. B站 sources give 16:9, 16:10 and 4:3, which is why src/cardsets/solar-bili exports two sizes.
  • unconfirmed: one source, or our own observation.

The evidence for each value is in plans/research-cards.md in the repository. Platforms change these without notice, so check in the app before relying on one.

Templates

tUseFields, with limitsShapes
coverBig-word coverlines 1–3, mark, sub ≤ 24, motif, hint ≤ 8tall, portrait, square
wideCover on a wide ratiolines 1–2 (≤ 8 a line on 16:9, ≤ 4 on wechat-head), mark, sub ≤ 20, motif, alignlandscape, wide, banner
numberOne number that is the pointvalue ≤ 7, unit ≤ 6, label ≤ 22, note ≤ 40, barsall but banner
chartOne chart and the sentence it provestitle ≤ 22, takeaway ≤ 40, chartall but banner
compare2–3 items side by sidetitle ≤ 22, items (label ≤ 12, value ≤ 7, unit, num, note ≤ 22, focus), verdict ≤ 36all but banner
quoteA sentence with attributiontext ≤ 44, by ≤ 24all but banner
listPointstitle ≤ 22, items (head ≤ 14, body ≤ 40, icon): 3–5 on portrait, 3 on square and 4:3tall, portrait, square, landscape
stepsOrdered stepstitle ≤ 22, steps (head, body): 3–4 on portrait, 3 on squaretall, portrait, square
textA point a chart cannot maketitle ≤ 22, paras 1–3, ≤ 70 eachtall, portrait, square
imageScreenshot, photo or decorimage, title ≤ 22, caption ≤ 40tall, portrait, square
endingLast cardlines 1–2, ≤ 8 each, mark, takeaways 0–3, ≤ 24 each, cta ≤ 22, sources 0–3, ≤ 44 eachtall, portrait, square

motif on a cover is one of: { kind: 'bars', values }, { kind: 'icon', icon }, or a sticker, { kind: 'emoji', e: '🎤', around: ['✨', '💡'] }. Emoji also work in any text field; see the Emoji section of the guide.

Every card also takes source (≤ 44, or null), kicker (≤ 14), tone (paper, ink, accent, wash), preset and cover.

Shapes: tall is 9:16, portrait goes up to 6:7, landscape is 4:3, wide goes up to 2:1, banner is wider than that (wechat-head). A template on a shape it has no layout for fails QA by name.

chart takes line (1–3 series), bars (≤ 12) or rank (≤ 8 rows on portrait, ≤ 5 on square or wide, names ≤ 6).

Rules

  • Limits count one CJK character as 1 and a Latin letter or digit as 0.5. QA counts titles, body fields, kicker and source. Headlines are not counted: they are sized from the longest line, and QA fails them if that makes them too small at feed size. Body text never shrinks to fit; cut the words or split the card.
  • A cover headline fits about 6 CJK characters a line on 小红书, about 5 on 抖音 and other three-column grids, and 2–4 on wechat-thumb. Choose every line break yourself. Do not repeat the post title, which the platform usually prints beside the cover.
  • The first card is a cover, and on a preset marked * so is every card. A cover needs a headline, so it must be cover, wide or number. To put another template on such a preset, set cover: false on that card.
  • Facts need sources. Name the publisher in source and mark calculations. Use null only on a card that states no fact. QA can only notice a missing source when the card contains a digit, so a fact written in words is yours to check. If you cannot find a source, leave the fact out.
  • quote needs a real attribution in by. Never put words in someone’s mouth.
  • image.kind follows the project’s image rules: screenshot is your own work and may be evidence, photo is atmosphere only and prints its credit, decor is decoration only.

What QA checks

Text outside the card, inside the safe zone or under a blocked strip. Text under 10 px in the file, or too small when the card is opened on a phone. One character alone on the last line. Overlapping text, overflow, clipped content, charts squeezed too flat. Content limits. A number with no source, a declared source missing from the card, a photo with no credit, an image that did not load. Headline size and contrast measured on the PNG shrunk to feed width, and whether the headline survives a central crop. Contrast at full size. System-font fallback. Each PNG’s exact pixel size, read from the file. PDF page count, if pdfinfo is installed. Sideways scrolling in the gallery on a phone, page errors and network requests.

Results are in qa/cards/<name>/report.json.

Known limits

  • No Chinese platform spec here is officially confirmed. No public official page was found for 小红书, 抖音, 视频号 or B站, and the WeChat page tried returned 404.
  • Feed widths are estimates, from arithmetic or observation. The minimum headline size (16 CSS px at feed size, 14 on wechat-thumb) is a judgement. Safe zones, blocked strips and central crops are working assumptions.
  • Nothing has been tested by publishing in the real apps. Check the first post in the app.
  • QA finds one character alone on a line. It does not find a word split across two lines, so a person must still look at every card.