agentleFS
ASNNetworksASNNetworks/floh-skills/skills/og-generator/SKILL.md

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.

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
  1. Very specific:

    All scripts are in scripts/ next to this file.

    line 30
  2. Very specific:

    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 …

    line 61
  3. Distinctive:

    Pointing at pages: Every script takes the same target forms:

    line 65
  4. Distinctive:

    Navbar and style: do not decide in the abstract.

    line 108
  5. Very specific:

    Write every decision into the preset with its reason (exclude: [{ "pattern": ..., "why": ...

    line 118
  6. Very specific:

    Test it with --selector, then add it to hide with a why.

    line 147
  7. Very specific:

    Write the choices into the preset (navbar, style, framed, rules), re-sample once to show the result, and when the user is happy:

    line 179

≈ “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.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

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.mjs puts 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.mjs

Checks 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.json in 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 wantPass
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 pagehttps://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 serverhttp://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-preset

Reads 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:

  1. 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.
  2. 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.
  3. Theme: light or dark, if the site has both.
  4. 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.
  5. 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.
  6. Format: webp q80 (default; works on LinkedIn, Facebook, X, WhatsApp, Slack, iMessage, Discord, Bluesky, Mastodon) or jpeg when the audience lives in Outlook, Teams or older clients that were never tested with WebP.
  7. Where the images go: in a repo, the folder the site serves (public/og by 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      # fewer

Nothing 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-visible fail, 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 with node 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, like widget-1792343158523, change on every load; iframes are matched by src). Test it with --selector, then add it to hide with a why.
  • 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 settleMs or set waitFor (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-right or window-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 to

Writes <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.md has 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/alt and twitter: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), from references/wiring.md.

Step 7: verify

node scripts/verify.mjs <production-url>          # one page per template
node scripts/verify.mjs <production-url> --all

Reads 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-preset drafts 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.

Download now (ZIP)

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
What did you use it for? e.g. Reviewing pull requests in a Rails app

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.