og-generator
Skill0 starsChanged 4 months agoMITView on GitHub
Generate a per-page Open Graph share image (og:image, the link-preview card on LinkedIn, Slack, WhatsApp, X, iMessage) for every page of a website by screenshotting it, with cookie banners, chat bubbles and popups filtered out, one card per language, and an optional framed style (the page in a browser window beside its title, on the brand colour). Works on a repo's dev server or straight on a production URL, from a home page, one page, a list, path patterns or a sitemap. Starts with an assessment discussed with the user, reviews samples before any full run, and saves a per-site preset so later runs skip the analysis. Use when asked for OG images, share images, social cards, link previews or twitter cards for a site, when shared links show an empty or wrong card, or to regenerate cards after content changed.
In this repository9 other files it gives its agents.
- brand-voiceSkill
- document-extractionSkill
- image-genSkill
- linkedin-carouselSkill
- nextjs-conventionsSkill
- pharosSkill
- seo-auditSkill
- skill-distributionSkill
- supabase-migrationsSkill
The file
We split this skill into the rules it gives an agent
7 rules. About 0 are common, 2 are distinctive, and 5 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈2
Distinctive · estimate
This author's own choices. Worth a look.
5
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 30
All scripts are in scripts/ next to this file.
- Very specific: line 61
If an approved preset exists (preset.mjs show <target> says reviewed: <date>), read its notes, skip to step 5, and only go back to sampling …
- Distinctive: line 65
Pointing at pages: Every script takes the same target forms:
- Distinctive: line 108
Navbar and style: do not decide in the abstract.
- Very specific: line 118
Write every decision into the preset with its reason (exclude: [{ "pattern": ..., "why": ...
- Very specific: line 147
Test it with --selector, then add it to hide with a why.
- Very specific: line 179
Write the choices into the preset (navbar, style, framed, rules), re-sample once to show the result, and when the user is happy:
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
OG generator
A share card is the first thing anyone sees of a page they have not visited. A real screenshot of that page makes the best one, with three conditions: nothing on it that the page's visitors would not want to see (cookie banners, chat bubbles, promo popups), nothing cut in half, and the right language. Getting those right is site-specific, so this skill works in a fixed order: assess, decide with the user, sample, review, approve, run. The decisions go into a preset, so the second run on a site is one command.
Ground rules
- Never shoot a full run before a sample run has been reviewed and approved, unless the site already has an approved preset (then a full run is the job).
- Never install anything into the user's project.
preflight.mjsputs playwright, sharp and Chromium in~/.cache/og-generator/tools, outside every repo, so no deploy ever downloads a browser. - Analytics are blocked during every capture (GA, GTM, Meta, Clarity, Hotjar, Plausible,
...), so a run is not a burst of fake visitors. Self-hosted analytics: add the host to
blockRequests. - Production sites are somebody's live server: two pages at a time by default. Say so before a full run of hundreds of pages, and run it when the user agrees.
- Look at the images yourself. The checks catch a lot; they do not replace reading every contact sheet. Every visual decision gets a reason recorded in the preset.
All scripts are in scripts/ next to this file. Run them with node <skill>/scripts/<x>.mjs
from the site's repo, or from anywhere for a site without a repo.
Step 0: preflight
node scripts/preflight.mjsChecks Node (>= 20.9), installs playwright + sharp + Chromium's headless shell into the
tools directory if missing, and proves a render works. It is idempotent; run it at the start
of every session. If it reports missing OS libraries (fresh Linux box), it prints the
install-deps command: that needs root, so ask the user before running it with
--system-deps. A fonts warning means text in non-web fonts may render as boxes.
Step 1: find the preset
node scripts/preset.mjs where <target><target> is what the user pointed at: https://site.com, a page, a sitemap URL, or a dev
server (http://localhost:3000). Where the preset lives:
- In a git repo:
og.preset.jsonin the repo (nearest from the working directory up), committed with the site. Run from the site's own repo. - Not a repo (a client's WordPress, any production site): a per-site workspace,
~/og-generator/<host>/, holding the preset, assessment, previews and images. Ask the user to confirm that location (or name another, passed as--preset <file>from then on) before writing there.
If an approved preset exists (preset.mjs show <target> says reviewed: <date>), read
its notes, skip to step 5, and only go back to sampling if generate warns that new templates
appeared since approval. Otherwise continue.
Pointing at pages. Every script takes the same target forms:
| You want | Pass |
| the whole site (sitemap discovered) | https://site.com |
| a specific sitemap (an index is followed) | https://site.com/sitemap_index.xml or --sitemap URL |
| one page | https://site.com/pricing |
| a few pages | --urls https://site.com/a,https://site.com/b or --paths /a,/b |
| a section | --include '/blog/**' (* = one segment, ** = any depth, re: = regex) |
| a slice of the list | --range 1-50, --range 51- |
| a dev server | http://localhost:3000 (its sitemap lists production URLs; the paths are reused) |
Sitemaps are found through robots.txt (Sitemap: lines) first, then /sitemap.xml,
/sitemap_index.xml, /sitemap-index.xml, /wp-sitemap.xml; gzip and nested indexes are
handled. No sitemap: the home page's links are crawled over plain HTTP (client-rendered
navigation is invisible to that; pass --paths or a sitemap URL). To look yourself:
curl -s https://site.com/robots.txt | grep -i sitemap, then
curl -s <sitemap> | grep -o '<loc>[^<]*' | head. Check what a run would shoot with
node scripts/generate.mjs <target> --list.
Step 2: assess, then decide with the user
node scripts/assess.mjs <target> --write-presetReads the whole page list, groups it into templates (/blog/* = every post), detects the
languages, then opens one page per template and reports: the share image each has today
(and whether it is the wrong shape), robots, the stack, the consent banner and the stored
value that silences it (verified on a fresh profile), the theme mechanism, the analytics it
blocked, and the fixed chrome in frame. It writes og-assessment.md beside the preset and a
draft preset.
Read og-assessment.md and settle every question in it with the user before sampling.
Present it compactly: what you found, then the questions with your recommendation for each:
- Pages to leave out: noindex pages, utility pages (search, tag archives, the HTML sitemap), legal pages, and pages that already carry a better image of their own (posts with a featured image, products with a photo). The report suggests; the user decides.
- Languages: one card per page per language (recommended: the screenshot is the copy, and a Dutch link showing an English headline says the page is not really in Dutch), one language for all, or a subset.
- Theme: light or dark, if the site has both.
- Navbar and style: do not decide in the abstract. The first sample shows every page with the navbar, without it, and framed, side by side; decide on those.
- Floating UI: chat bubbles and back-to-top buttons are hidden by default; anything else floating (a promo popup, a sticky CTA bar) is listed.
- Format:
webpq80 (default; works on LinkedIn, Facebook, X, WhatsApp, Slack, iMessage, Discord, Bluesky, Mastodon) orjpegwhen the audience lives in Outlook, Teams or older clients that were never tested with WebP. - Where the images go: in a repo, the folder the site serves (
public/ogby default) plus wiring; without a repo, a folder to upload in the CMS.
Write every decision into the preset with its reason (exclude: [{ "pattern": ..., "why":
... }], hide: [{ "selector": ..., "why": ... }], rules with why), and log it:
node scripts/preset.mjs note <target> "what was decided, and why". Every field is in
references/preset.md.
Step 3: sample, then review like an editor
node scripts/generate.mjs <target> --sample # 12 pages: one per template, every language
node scripts/generate.mjs <target> --sample 5 # fewerNothing is written to the output folder. Each sampled page is shot as a first-time
visitor sees it and as the share image, and until the preset has decided navbar,
style and framed.layout, as every look side by side: with navbar, navbar hidden, framed
(window right), framed (window bottom). The contact sheets
(.og-preview/samples/contact-sheet-N.png) put them side by side with the check results
under each; report.md has the details.
Read every contact sheet, then open the full-size image of any card you are unsure
about. This is the step that makes the cards good. references/review.md is the full
method; the core of it:
- Consent: the visitor shot should show the banner and the share image should not. A
consent-visiblefail, or a banner you can see that the check missed, is never shipped. - Anything floating (
floating-overlay, or just visible): find out what it is withnode scripts/probe.mjs <page> --at x,y(card coordinates) or--text "...". It prints the element, the floating ancestor to hide, and a selector that survives a reload (ids with long numbers, likewidget-1792343158523, change on every load; iframes are matched bysrc). Test it with--selector, then add it tohidewith awhy. - Cut in half: a headline or picture sliced by an edge (
headline-out-of-frame,text-clipped-top,media-clipped-bottom). Fix with the navbar choice,framing.focus, or a per-template rule; or pick framed for that template. - Not finished loading: spinners, skeletons, blank image slots, half-faded text: raise
settleMsor setwaitFor(globally or in a rule). - Wrong page:
redirected,http-status,lang-mismatch: a login wall, a locale redirect, a 404 in the sitemap. - Taste: would someone scrolling a feed understand the card at 400px wide? If the screenshot is busy (dense nav, small text, mostly photo), framed is usually better.
Fix, re-run the sample (--paths for just the pages you changed), and repeat until every
card is one you would post yourself. Prefer fixes that last: on a repo, a data-og-hide,
data-og-focus or data-og-card attribute on the component (see references/review.md)
travels with the code; on a production site, selectors in the preset are the only option.
Step 4: show the user, adjust, approve
Show the user the contact sheets (send the PNG files; a terminal cannot show images) and say,
in a few lines, what you found and what you changed. Then ask them to choose, per template
if it helps (a rule with match can set navbar and style for /blog/* only):
- with navbar (brand recognition) or navbar hidden (more room for the headline), and
- screenshot or framed. Framed is the page in a browser window on the site's brand
colour beside its title, set in the site's own heading font. Offer it on every first run,
and offer adjustments: background colour (
framed.background, one colour or two for the gradient), title colour, layout (window-rightorwindow-bottom), which screenshot goes in the window (screenshot: "nonav", the default, hides the site's navbar inside the frame;"nav"keeps it), title source (h1,og:title,title), title size, and the URL / site name / icon on or off. The run prints the brand colour it detected; pin the chosen one so every card matches.
Write the choices into the preset (navbar, style, framed, rules), re-sample once to
show the result, and when the user is happy:
node scripts/preset.mjs approve <target> --by "<who approved>"That records the date and the templates that existed, so a later run warns about new ones.
Step 5: the full run
node scripts/generate.mjs <target>
node scripts/generate.mjs <target> --include '/blog/**' # one section
node scripts/generate.mjs <target> --prune # also delete cards no page maps toWrites <outDir>/<key>.<ext> per page plus og-manifest.json (page -> file; --csv adds
og-manifest.csv for a CMS upload). A card whose pixels did not change is not rewritten, so
in a repo the diff is only the cards that moved. It also writes contact sheets of every card
(.og-preview/full/): page through them before handing over. Images of another format left
from an earlier run for the same page are reported as stale (--prune removes them).
The naming scheme: / -> home, /pricing -> pricing, /nl/features/x ->
nl-features-x (trailing slash, query and fragment ignored). The site's metadata must compute
the same key, or every page silently falls back to the default card.
Step 6: wire it
- Repo:
references/wiring.mdhas the metadata helper for Next.js (App Router and Pages), Astro, Nuxt, SvelteKit, Remix, Hugo and plain HTML, with the key function in each, a default-card fallback,og:image:width/height/altandtwitter:card=summary_large_image. The preview folder (.og-preview/) must be git-ignored; the output folder is committed. - No repo (WordPress, Webflow, Shopify, ...): hand over the folder and
og-manifest.csv, with the per-page field to set (Rank Math / Yoast: the page's Social image), fromreferences/wiring.md.
Step 7: verify
node scripts/verify.mjs <production-url> # one page per template
node scripts/verify.mjs <production-url> --allReads each page's HTML the way a link-preview crawler does (no JavaScript) and checks that
og:image is absolute, names this page's card, loads as an image, is 1.91:1, under 300 KB
(WhatsApp's limit) and that twitter:card is summary_large_image. Then suggest the user
re-scrape one URL in the LinkedIn Post Inspector and the Facebook Sharing Debugger, because
both cache old cards for days.
Files
scripts/preflight.mjs: check and install the toolchain (outside the project).scripts/assess.mjs: the site inventory and the questions;--write-presetdrafts the preset.scripts/generate.mjs:--list,--sample [N](dry run with variants), or the full run.scripts/probe.mjs: what is at a point / carries a text / floats in frame, with a stable selector.scripts/preset.mjs:where,list,show,note,approve.scripts/verify.mjs: the deployed tags and images, per page.references/preset.md: every preset field.references/review.md: the review method, defects and their fixes, source markers.references/consent.md: how consent is kept out of the picture, and when to accept instead.references/wiring.md: metadata per framework and CMS.assets/og.preset.example.json: a complete preset to copy from.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

