Back to catalog

social-media-cards

HTML/SVG image cards for LinkedIn, X, Substack, and WhatsApp Status. Two-format workflow: landscape for OG images, square for social posts.

Category 🎨 Creative

Social Media Cards

Create HTML/SVG image cards for LinkedIn, Twitter/X, Substack, and WhatsApp Status. Optimized for mobile readability.

Reference files: references/platform-dimensions.md (detailed platform dimensions and research), references/mobile-preview-template.md (phone-frame HTML for mobile testing).

Format Selection — Two Formats Per Card

Every card needs two versions. They share the same content, just different canvas shapes.

Format Dimensions Aspect Ratio Use For
Landscape 1200×628 1.91:1 Website featured image, OG image, link previews (Substack, blog, X link cards)
Square 1080×1080 1:1 Social media feed posts (LinkedIn, X/Twitter, WhatsApp Status)
Why both? A square image can't be a website featured image (OG meta tags expect ~1.91:1). A landscape image gets crushed on mobile social feeds. You need both. Workflow: For each card concept, create card-landscape.html + card-square.html, render both PNGs. Use landscape for the blog post featured image, square when posting on social.

Platform Dimensions Reference

Platform Feed Post Link Preview Notes
LinkedIn 1080×1080 (1:1) or 1080×1350 (4:5) 1200×627 (1.91:1) Square gets more mobile feed space than landscape
X/Twitter 1200×675 (16:9) or 1080×1080 (1:1) 1200×628 (1.91:1) Square works well, takes more vertical feed space
Substack 1456×1048 (14:10) social preview Same Articles are 730px wide; 1080×1080 works as article header
WhatsApp Status 1080×1920 (9:16) N/A Full-screen vertical; square image gets centered with colored bars
1080px wide is the universal baseline across all platforms. Square (1:1) is the safest single format if you can only pick one.

Font Sizing (Mobile-First)

Social platforms scale images to fit mobile viewports (~375px wide). A 1200px card displays at ~31% scale on mobile. What feels "too big" on desktop is just right on mobile.

SVG Text (inside diagram/visual)

Element Minimum Recommended
Main text/labels 22px 26-32px
Sublabels 14px 16-18px
Annotations 12px 14-16px
Fine print — Remove entirely

CSS Text (title, excerpt, footer)

Element Minimum Recommended
Headline 42px 48-52px
Body/excerpt 22px 26-28px
Brand/footer 16px 18px
URL 13px 15px

Rules

  • Cut text ruthlessly — if it can't be read in 2 seconds, it's too much
  • Bold weight 600+ for all text — thin weights vanish on mobile
  • High contrast — white on dark or dark on white. Avoid light gray on light backgrounds
  • Remove side labels and annotations that don't carry the core message
  • No font size below 14px in SVG — it becomes ~4px on mobile, invisible

Mobile Preview Workflow

Before shipping a card, always preview at mobile scale:

1. Create a phone-frame mockup HTML with realistic social platform UI

2. Render the card as PNG (via browser screenshot at 1:1)

3. Embed the PNG in the mockup at 375px width

4. Take a screenshot and verify readability

See references/mobile-preview-template.md for the phone-frame HTML template.

HTML Card Templates

Landscape (1200×628) — for website featured image / OG

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link href="https://fonts.googleapis.com/css2?family=Spectral:ital,wght@0,400;0,600;1,400&family=Source+Code+Pro:wght@400;500;600&display=swap" rel="stylesheet">
<style>
  html, body { margin: 0; padding: 0; width: 1200px; height: 628px; overflow: hidden; }
  * { margin: 0; padding: 0; box-sizing: border-box; }
  .card { width: 1200px; height: 628px; background: #fff; display: flex; overflow: hidden; }
  .visual { flex: 0 0 580px; background: #f8f8fa; border-right: 1px solid rgba(54,55,55,0.10); display: flex; align-items: center; justify-content: center; }
  .visual svg { width: 100%; height: 100%; }
  .text { flex: 1; padding: 48px; display: flex; flex-direction: column; justify-content: center; }
  .text h2 { font-family: 'Spectral', serif; font-size: 48px; font-weight: 600; line-height: 1.15; color: #363737; }
  .accent-bar { width: 56px; height: 4px; background: #4f46e5; border-radius: 2px; margin: 16px 0 24px; }
  .text .excerpt { font-family: 'Spectral', serif; font-size: 26px; line-height: 1.5; color: #757575; font-style: italic; }
  .footer { position: absolute; bottom: 0; left: 580px; right: 0; padding: 16px 48px; border-top: 1px solid rgba(54,55,55,0.10); display: flex; justify-content: flex-end; }
  .footer .url { font-family: 'Source Code Pro', monospace; font-size: 15px; color: #757575; }
</style>
</head>
<body>
<div class="card">
  <div class="visual"><!-- SVG diagram here --></div>
  <div class="text">
    <h2>Title</h2>
    <div class="accent-bar"></div>
    <p class="excerpt">Subtitle text</p>
  </div>
  <div class="footer">
    <span class="url">jjude.com</span>
  </div>
</div>
</body>
</html>
Footer rule: Only the site URL on the right (justify-content: flex-end). No brand name on the left — it's redundant for personal blogs.

Square (1080×1080) — for social media posts

Layout: text on top, diagram below. In a square card, the title hooks the reader, then the diagram explains. This reads top-down naturally on mobile.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link href="https://fonts.googleapis.com/css2?family=Spectral:ital,wght@0,400;0,600;1,400&family=Source+Code+Pro:wght@400;500;600&display=swap" rel="stylesheet">
<style>
  html, body { margin: 0; padding: 0; width: 1080px; height: 1080px; overflow: hidden; }
  * { margin: 0; padding: 0; box-sizing: border-box; }
  .card { width: 1080px; height: 1080px; background: #fff; display: flex; flex-direction: column; overflow: hidden; }
  .text-block { padding: 52px 60px 32px 60px; text-align: center; }
  .text-block h2 { font-family: 'Spectral', serif; font-size: 52px; font-weight: 600; line-height: 1.15; color: #363737; }
  .accent-bar { width: 56px; height: 4px; background: #4f46e5; border-radius: 2px; margin: 16px auto 20px; }
  .text-block .excerpt { font-family: 'Spectral', serif; font-size: 28px; line-height: 1.5; color: #757575; font-style: italic; }
  .visual { flex: 1; background: #f8f8fa; display: flex; align-items: center; justify-content: center; overflow: hidden; }
  .visual svg { width: 100%; height: 100%; display: block; }
  .footer { padding: 20px 60px 28px; border-top: 1px solid rgba(54,55,55,0.10); display: flex; justify-content: flex-end; }
  .footer .url { font-family: 'Source Code Pro', monospace; font-size: 15px; color: #757575; }
</style>
</head>
<body>
<div class="card">
  <div class="text-block">
    <h2>Title</h2>
    <div class="accent-bar"></div>
    <p class="excerpt">Subtitle text</p>
  </div>
  <div class="visual"><!-- SVG diagram here --></div>
  <div class="footer">
    <span class="url">jjude.com</span>
  </div>
</div>
</body>
</html>

Text Diagram Approval Workflow

Before generating any image, show the user a text/ASCII diagram of the layout. This prevents wasted render cycles on wrong layouts.

Steps:

1. Draw the diagram layout in text (use monospace art). Include: node positions, label placement, spacing.

2. For each format, specify where text and diagram go:

  • Landscape (1200×628): diagram LEFT, text RIGHT. Footer at bottom spanning full width.
  • Square (1080×1080): text TOP, diagram BOTTOM. Footer at bottom spanning full width.

3. Propose 2-3 title/subtitle options. Let the user pick.

4. Only after user confirms the layout AND text, generate the HTML cards and render PNGs.

Confirmed layout conventions (Joseph):
  • Landscape: .visual div LEFT (580px), .text div RIGHT. Footer with jjude.com brand.
  • Square: .text-block TOP, .visual flex-1 below. Footer with jjude.com brand.
  • White background, indigo accent (#4f46e5), Spectral serif + Source Code Pro monospace.
  • Always include jjude.com in the footer.

Rendering PNGs

Do NOT use browser_vision for rendering. The browser tool captures the full viewport (1280×720), not the card element. A 1080×1080 square card extends below the viewport, and a 1200×628 landscape card has white space around it. The result always has a white "border" from the browser background.

Correct approach: Playwright element.screenshot()

Use Playwright with Chromium to take element-level screenshots:

# Setup (one-time)
uv venv /tmp/pw-env && source /tmp/pw-env/bin/activate
uv pip install playwright
PLAYWRIGHT_BROWSERS_PATH=/tmp/pw-browsers python3 -m playwright install chromium
# render_cards.py
import asyncio
from playwright.async_api import async_playwright

CARDS = [
    {"html": "/path/to/card.html", "output": "/path/to/card.png", "selector": ".card"},
    {"html": "/path/to/card-square.html", "output": "/path/to/card-square.png", "selector": ".card"},
]

async def main():
    async with async_playwright() as p:
        browser = await p.chromium.launch()
        for card in CARDS:
            page = await browser.new_page()
            await page.goto(f"file://{card['html']}")
            await page.wait_for_load_state("networkidle")
            element = await page.query_selector(card["selector"])
            await element.screenshot(path=card["output"])
            print(f"Saved: {card['output']}")
            await page.close()
        await browser.close()

asyncio.run(main())

Run with: source /tmp/pw-env/bin/activate && PLAYWRIGHT_BROWSERS_PATH=/tmp/pw-browsers python3 render_cards.py

Safety net: html/body constraints

Always add this to card HTML as a viewport-bleed safety net:

html, body { margin: 0; padding: 0; width: Npx; height: Npx; overflow: hidden; }

Where N matches the card dimensions (1200×628 for landscape, 1080×1080 for square).

SVG Diagram Conventions (inside .visual)

When the card includes a diagram (hub-and-spoke, flow, architecture), follow these rules inside the SVG:

Arrowheads

Use SVG <marker> with orient="auto", never hand-drawn <polygon> triangles. Markers auto-rotate to match the line's angle. Hand-drawn triangles only look right on horizontal/vertical lines — they break on diagonals.
<defs>
  <marker id="arrow-end" viewBox="0 0 10 10" refX="8" refY="5"
          markerWidth="6.5" markerHeight="6.5" orient="auto">
    <path d="M0,0 L10,5 L0,10 z" fill="#4f46e5" fill-opacity="0.55"/>
  </marker>
  <marker id="arrow-start" viewBox="0 0 10 10" refX="2" refY="5"
          markerWidth="6.5" markerHeight="6.5" orient="auto">
    <path d="M10,0 L0,5 L10,10 z" fill="#4f46e5" fill-opacity="0.55"/>
  </marker>
</defs>
Two separate markers (not auto-start-reverse) — better WebKit compatibility for headless rendering.

Node Boxes

White fill, light gray stroke (#d1d5db), rx="6", drop shadow filter. Never flat gray fills.

Icon + Label Positioning (vertical rhythm)

Inside each node box (origin Y, height H):

  • Icon band: Y+10 to Y+30 (20px tall), icon vertically centered in that band
  • Title baseline: Y + h×0.68
  • Subtitle baseline: title + 14px
  • Horizontal center: icon and text both centered on box_x + width/2

Connection Lines

Arrows must start/end at element borders, not from arbitrary interior points. Calculate border intersection: e.g., for a box at (50, 46) width 130, the right border is x=180.

Drop Shadow Filter

<filter id="ds" x="-4%" y="-4%" width="108%" height="116%">
  <feDropShadow dx="0" dy="1" stdDeviation="3" flood-color="#363737" flood-opacity="0.06"/>
</filter>

Pitfalls

  • Don't use browser_vision for rendering — it captures the full viewport (1280×720), not the card element. A 1080×1080 card extends below the viewport; a 1200×628 card has white space around it. Always use Playwright element.screenshot() (see Rendering PNGs section).
  • Don't make only one format — always create BOTH landscape (1200×628) and square (1080×1080). Landscape for website OG/featured image, square for social posts. A square image fails as an OG image; a landscape image gets crushed on mobile feeds.
  • Don't use landscape for LinkedIn/X feed posts — square or portrait gets more feed real estate on mobile
  • Don't trust desktop preview — always check at 375px mobile scale using the phone-frame mockup
  • Don't use thin/light font weights — they disappear on mobile screens
  • Don't keep SVG annotations that aren't essential — they become noise on mobile
  • Don't use viewBox smaller than 580px for the visual half — SVG text below 22px is unreadable at mobile scale
  • Don't forget Substack — 1080×1080 square works as article header and social preview; landscape (1200×628) works for link cards
  • Don't put gray/colored background only in the .visual div — if you want full-bleed color, set it on .card itself. Otherwise the text block and footer sit on white, creating a visible boundary. Alternatively, use preserveAspectRatio="xMidYMid slice" on the SVG to fill edge-to-edge.
  • Always add html, body { width: Npx; height: Npx; overflow: hidden; } to card HTML — prevents viewport bleed-through even if the rendering tool doesn't clip to the element
  • Don't hand-draw arrowhead polygons — they only look right on axis-aligned lines. Use SVG <marker> with orient="auto" (see SVG Diagram Conventions).
  • Don't position icons at arbitrary offsets — use the vertical rhythm formula (icon band Y+10 to Y+30, title at Y+h×0.68). Icons and text must share the same horizontal center (box_x + width/2).
  • Don't let arrows originate from inside boxes — calculate the element border intersection point. Arrows connect border-to-border, not interior-to-interior.
  • Save generated images to the correct wiki path — produce/hermes/social-media/images/generated/{slug}/ (NOT under blogs/jjude/ or blogs/ctofieldnotes/). Social media is cross-cutting for both blogs.