Generate branded per-page Open Graph (social share) cards for a website: a 1200×630 JPEG ≤300 KB for every page, rendered by a committed Python+Pillow script and wired so each page sets its own `image=` (with a default fallback) and the test suite proves the card exists at the right size. Use when building or auditing a site's share previews — "OG image", "og:image", "social share image", "share card", "WhatsApp/Slack/LinkedIn preview", "Twitter card image", "the picture when I share the link...
6 stars
0 votes
0 copies
4 views
Added September 19, 2026
ai-agentspythongobash
Security analysis
A96/100
mediumInstalls packages at runtime which could introduce malicious dependencies
Installs into .claude/skills of the current project.
Are you the author of Og Images?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/karero-og-images)
---
name: og-images
description: >
Generate branded per-page Open Graph (social share) cards for a website: a 1200×630
JPEG ≤300 KB for every page, rendered by a committed Python+Pillow script and wired
so each page sets its own `image=` (with a default fallback) and the test suite proves
the card exists at the right size. Use when building or auditing a site's share previews —
"OG image", "og:image", "social share image", "share card", "WhatsApp/Slack/LinkedIn
preview", "Twitter card image", "the picture when I share the link", "per-page OG images",
"generate share images". For general marketing image creation/optimization use the
`image` skill; this one is specifically the build-time, tested, per-page card generator
for the suite's Astro starter.
metadata:
version: 1.0.0
---
# OG images — per-page share cards
When a URL is shared (WhatsApp, Slack, iMessage, X, LinkedIn, Discord), the `og:image`
is the preview. Get it wrong and the link shares with **no picture** or a badly cropped
one — a silent conversion leak. This skill makes every page render a consistent,
on-brand **1200×630 JPEG ≤300 KB**, generated by a committed script and enforced by the
test gate.
## The three hard rules (messengers enforce them)
1. **1200×630, exactly** (1.91:1). Square images get cropped on X/Teams; off-ratio ones
letterbox. The starter's `Base.astro` already emits `og:image:width/height` = 1200×630.
2. **≤300 KB.** WhatsApp silently **drops** previews heavier than ~300 KB — the link then
shares with no image at all. Encode JPEG and compress to fit.
3. **Extension matches the bytes.** A PNG renamed `.jpg` trips strict scrapers. Use JPEG
for photographic cards, PNG only for flat logo/gradient cards that still come ≤300 KB.
## How it's wired in the Astro starter
- **`scripts/generate_og_cards.py`** — the generator. Renders one card per page you list,
plus a `default.jpg`. Run it with **`npm run og`**. Idempotent; commit the output.
- **`src/layouts/Base.astro`** — `image` prop defaults to `/images/og/default.jpg`. A page
gets its own card by passing `image="/images/og/<slug>.jpg"`; a page with no `image=` falls
back to the default — which only **exempt utility pages** should do (the test below enforces
it). `og:image` + `twitter:image` are derived from that one prop.
- **`tests/seo.spec.ts`** — for every page, reads the referenced card off disk and asserts
it exists in `public/`, is ≤300 KB, a real JPEG/PNG, and exactly 1200×630. It also enforces
that every page has its **own** card (not the generic default) and that no two pages share
one — except an `OWN_CARD_EXEMPT` allowlist (home + utility pages, default `['/', '/privacy']`).
Opt-out: as you add content pages, leave them OUT of the allowlist and the guard flags any
that forgot a card. A missing/oversized/shared card **fails the build** — OG cards can't
silently rot, and a new page can't silently ship on the generic default.
The starter ships a generated `public/images/og/default.jpg` so the suite is green from
commit 1; you replace it with your brand by editing the script and re-running `npm run og`.
## Setup for a site (two edits, one command)
1. **Fill the `BRAND` block** in `scripts/generate_og_cards.py` — `BRAND_NAME`,
`SITE_URL_LABEL`, the background/accent/text colours (copy the hexes from
`src/styles/global.css` / `BRAND.md`), and optionally point `LOGO` at a transparent
**raster** PNG for a faint emblem watermark (SVG won't load — Pillow needs raster).
The card is a **dark** share card by design: light text on a deep background reads well
in every chat app regardless of whether the site theme is light or dark.
2. **List per-page cards** in the `PAGES` block — one `(slug, title, [subtitle, …])` per
content page. Then set `image="/images/og/<slug>.jpg"` on that page's `<Base>`. Only
**exempt utility pages** (home, privacy, …) may fall back to `default.jpg`; every other
content page must be listed here and wired to its own card, or `seo.spec.ts` fails it.
3. **Run `npm run og`**, then `npm test`. Commit the generated JPEGs.
`npm run og -- --check` exits non-zero if any card exceeds 300 KB — wire it into CI or the
pre-push hook if you want generation (not just validation) gated.
## Long or unbreakable words (German compounds)
`fit_title()` shrinks the font until every wrapped line actually fits the card's text
area, not just until the line count looks right. Three overflow cases fail the run
loudly (`OG card OVERFLOW: ...`) instead of drawing past the edge, all unconditional
(not gated behind `--check`, which only guards file size):
- a single unbroken word wider than the text area at the smallest size (52pt) — long
German compounds (`Suchmaschinenoptimierung`, `Datenschutz-Grundverordnung`) are the
classic trigger;
- a title still needing more than 3 lines at 52pt (it would collide with the
subtitles/footer);
- a subtitle wider than the text area (subtitles render at a fixed 30pt).
If you hit one: shorten the text to a near-synonym, or insert a plain space at a
natural compound boundary (`Suchmaschinen Optimierung`) — the generator deliberately
does not hyphenate compounds automatically.
## Prerequisite
Pillow, a local authoring dependency only (not shipped to the site or its runtime):
```bash
pip3 install Pillow
```
On **Windows**, run authoring/build inside **WSL2** — the suite's recommended Windows
environment (`SETUP.md`), where `python3` and the bash helpers all work. Running natively
is fine too: `py -m pip install Pillow`, and `npm run og` already falls back `python3 → py
-3 → python`.
## Going further — photo-driven cards
The generator deliberately ships the **text-card** core (emblem + wordmark + accent bar +
title + subtitles + footer URL) because it works for any site with zero per-page art. When
a content type has a natural image — a speaker headshot, a product shot, an author avatar —
extend `build_page()` to read that image from frontmatter and composite a circular portrait
+ glow beside the text: iterate a content collection (`src/content/<type>/*.md`), render one
portrait card per entry alongside the static-page cards, and keep the same encode-under-300 KB
step. The template ships the text-card core; the portrait variant is a small, additive change.
## Don't
- **Don't hand-export cards in Figma/Canva per page.** They drift and balloon past 300 KB.
The script is the single source of truth; re-running it is free.
- **Don't ship a square logo as `og:image`.** It crops badly. Always 1200×630.
- **Don't point `og:image` at a remote/CDN URL the repo can't verify.** Keep cards on-site
in `public/` so `seo.spec.ts` can prove they exist and fit.