Skip to content
Back to skills

Og Images

ASecurity

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

Pro shows the line behind each finding and how to fix it

Scanned September 19, 2026

npx -y skills add karero/website-builder --skill og-images --agent claude-code

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.

Security grade badge for Og Images
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/karero-og-images/badge)](https://www.skillsdirectory.com/skills/karero-og-images)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
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.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.