pulumi/event-meta-image
Generate on-brand social cards for a Pulumi event/workshop in five sizes (1200x628 OG, 1200x675, 628x628, 1080x1080, 540x960) using the shared Satori events renderer. Enriches the build-time default with people not in frontmatter (external co-presenters + their photos) and company/partner logos (Pulumi + e.g. Microsoft). Runs event-bound (writes into a content/events/<slug>/ bundle + sets meta_image / meta_image_square) or standalone (just emit the five PNGs to a folder). Use when the user types /event-meta-image or asks to create, generate, regenerate, or enrich an event's meta image, social card, OpenGraph image, or speaker/workshop card. Pass -y / --yes / --non-interactive (or 'no questions') to run end-to-end with sensible defaults and no prompts. A Register button is never added unless explicitly requested.
npx skills add https://github.com/pulumi/docs --skill event-meta-image
/event-meta-image — Generate event / workshop social cardsYou generate the branded event card (the Figma "Workshop / event images" component) in five sizes via the shared Node/Satori renderer — the *same* renderer the build uses for the automatic default, so output is identical to what ships, just enriched with inputs that aren't in frontmatter (external co-presenters, their photos, partner logos).
Skill directory: .claude/commands/event-meta-image/ — paths below are relative to the repo root unless noted.
Renderer CLI: scripts/meta-images/render-event.mjs (shared with the build). One JSON config → one PNG per size.
> You do not need a custom card for most events. Every event already gets an on-brand landscape + square card automatically at build time from its frontmatter (scripts/generate-meta-images.mjs). Use this skill only to (a) enrich a card with people/logos not in frontmatter, (b) override the auto default, or (c) grab the extra social sizes (square-large, portrait, tall) for manual use. (For a plain default with no enrichment, scripts/meta-images/render-event-cards.mjs <slug> renders all five sizes from frontmatter without a config.)
content/events/<slug>/ page. Triggered when $ARGUMENTS names an event path/slug, when git status shows a changed event, or when the user points at one. Writes the PNGs into the page bundle and updates frontmatter..context/event-images/<slug>/, or a path the user gives). No frontmatter or bundle writes.Everything below applies to both modes except the bundle/frontmatter writes in Step 6 (event-bound only).
Orthogonal to event-bound/standalone, pick how much to prompt. Default to searching, not asking: in both modes you still auto-resolve people and logos via the ladders in Steps 3–4 (including WebSearch/WebFetch). The modes differ only in what happens when something can't be resolved or is genuinely ambiguous.
$ARGUMENTS contains -y, --yes, --non-interactive, --no-input, no questions, or "just do it" (and automatically whenever you're running non-interactively, e.g. under claude -p or the eval, where AskUserQuestion can't be answered). Skip every AskUserQuestion. Derive all inputs automatically:event_type; title = frontmatter title; speakers = frontmatter presenters[].photo; additionalText = the derived "With …" line.["pulumi"] plus any partner company named in the title/body.AskUserQuestion only when an asset can't be resolved, or an input is genuinely ambiguous (multiple candidate events, missing title, a partner named but not identifiable). Do not ask to confirm things already known from frontmatter or $ARGUMENTS.$ARGUMENTS contains an event path or slug, use content/events/<slug>/index.md.git status / git diff --name-only for changed content/events/*/index.md.AskUserQuestion. None → ask for the slug/path (or switch to standalone if the user only wants loose images).Read the full index.md (frontmatter + body).
Pull from frontmatter:
presenters: [{ name, role, photo }] — the in-frontmatter speakers. Each photo (e.g. /images/people/<x>.jpg or /images/team/<x>.jpg) resolves under static/.The build default already uses exactly these. If the user only wants the default look in all five sizes (no new people/logos), you can skip straight to Step 6 with overline, title, the presenters' photo paths as speakers, the "With …" line as additionalText, and ["pulumi"] as logos.
Add anyone beyond the presenters: list — e.g. a partner-company co-host. Derive these from the event first (the title/body and $ARGUMENTS); only fall back to AskUserQuestion per the interaction mode (default: ask when a person is named but you can't identify or resolve them; unattended: skip an unresolvable person with a warning, never ask). For each added person collect name + role/company, then resolve a color photo via this ladder (stop at the first hit):
presenters[].photo (already have it).static/images/team/<kebab-name>.{jpg,jpeg,png}.static/images/people/<kebab-name>.{jpg,jpeg,png}.WebSearch for a headshot / company bio photo, then WebFetch the image. LinkedIn images are usually auth-gated — expect this to fail and fall through.Save any fetched/provided external photo to a real file (e.g. .context/event-images/<slug>/<kebab-name>.jpg) and pass that path in the config — the renderer resolves local files and data: URIs, not remote URLs. Photos render color + circular on a violet ring (the renderer can't reproduce the Figma duotone; the ring keeps them on-brand). Up to 5 speakers display (the renderer shrinks the circles so they fit, stacking vertically on landscape and in a row on square/portrait); the byline lists every name. Beyond 5 is rare — pick the 5 to show in Step 5.
Pulumi is always included ("pulumi" → the bundled brand mark — never re-color or restyle it). The renderer lays the logos out in a row joined by a muted + (Pulumi + Microsoft + …), so each entry should read as a company on its own.
Add partner/company logos (e.g. Microsoft, GitHub, Google Cloud, NVIDIA) for any partner named in the event's title/body or $ARGUMENTS. Derive them from the event first — only fall back to AskUserQuestion per the interaction mode (default: ask when a partner is named but you can't source a compliant logo; unattended: skip an unresolvable logo with a warning, never ask). Three hard rules when sourcing each one:
viewBox / PNG dimensions and prefer an aspect ratio ≳ 3:1; reject stacked or roughly-square lockups (aspect ≲ 2) even when they're technically a wordmark, and keep looking (a company's brand/press page usually offers both a "horizontal" and a "stacked" variant — take the horizontal one).#f5f5ff), so a logo with a baked-in white/opaque background shows as an ugly box — never use one. Verify transparency before caching (a PNG reported by file as RGB with no alpha is opaque; RGBA or a colormap with tRNS may be transparent — check the render).Resolve each via this ladder, stopping at the first hit that satisfies both rules:
.claude/commands/event-meta-image/assets/logos/<name>.svg (cached — see references/logos.md).static/logos/** (the site's own brand assets — often clean SVG wordmarks).WebSearch "<company> logo wordmark SVG transparent", then WebFetch/curl the official SVG. Good sources: the company's own brand/press page, Wikimedia Commons (*_logo.svg), and reputable vector libraries. simple-icons (https://raw.githubusercontent.com/simple-icons/simple-icons/develop/icons/<slug>.svg) is glyph-only and monochrome — use it only as a last resort, and recolor the <path> with fill="#<brandcolor>".Cache any clean fetched logo to .claude/commands/event-meta-image/assets/logos/<name>.{svg,png} and add a row to references/logos.md. Pass logo paths (absolute, or relative to the config file) in the config; "pulumi" is the one special name.
Default every option and move on. Only reach for AskUserQuestion per the interaction mode (and never in unattended mode); never re-confirm what $ARGUMENTS or frontmatter already answered.
event_type (e.g. WORKSHOP, WEBINAR, PANEL); honor an override if $ARGUMENTS gives one.Never add a button. showButton stays false unless the user's prompt *explicitly* asks for one (e.g. "with a Register button", "add a CTA", "include a sign-up button"). Do not offer it, do not ask about it, and never enable it in unattended mode. Only when explicitly requested, set showButton: true and buttonText accordingly.
Write one config JSON (e.g. .context/event-images/<slug>/config.json), then loop the renderer over the five sizes.
Config schema (all optional except title):
{
"overline": "WORKSHOP",
"title": "Advanced CI/CD for AWS using Pulumi and GitHub Actions",
"secondaryTitle": "",
"additionalText": "With Jane Doe - Staff Engineer, Acme and John Roe",
"showButton": false,
"buttonText": "Register", // opt-in only — see below; leave showButton false unless explicitly requested
"speakers": ["/images/people/jane.jpg", ".context/event-images/<slug>/john-roe.jpg"],
"logos": ["pulumi", ".claude/commands/event-meta-image/assets/logos/microsoft.svg"]
}
additionalText is the literal "With …" line. Compose it yourself: one presenter → With {name} - {role}; many → names only, Oxford comma (With A, B, and C). (Omit to let the CLI derive it from a presenters: [{name, role}] array instead.)showButton is off unless the user explicitly asked for a button (see Step 5). Omit it or leave false in every default and unattended run.speakers / logos resolve as: data: URI → path relative to the config file → repo path (/images/…, static/, assets/fingerprinted/, static/logos/**) → absolute path. Unresolved entries are skipped with a warning.Render loop — same config, five sizes:
| size | event-bound filename | wired to |
|------|----------------------|----------|
| 1200x628 | meta.png | meta_image (primary OG + twitter:image) |
| 628x628 | meta-square.png | meta_image_square (second og:image) |
| 1200x675 | meta-landscape-tall.png | — (manual social) |
| 1080x1080| meta-square-large.png | — (manual social) |
| 540x960 | meta-portrait.png | — (manual social) |
CFG=.context/event-images/<slug>/config.json
DEST=content/events/<slug> # event-bound; standalone → .context/event-images/<slug>
node scripts/meta-images/render-event.mjs --config "$CFG" --size 1200x628 --out "$DEST/meta.png"
node scripts/meta-images/render-event.mjs --config "$CFG" --size 628x628 --out "$DEST/meta-square.png"
node scripts/meta-images/render-event.mjs --config "$CFG" --size 1200x675 --out "$DEST/meta-landscape-tall.png"
node scripts/meta-images/render-event.mjs --config "$CFG" --size 1080x1080 --out "$DEST/meta-square-large.png"
node scripts/meta-images/render-event.mjs --config "$CFG" --size 540x960 --out "$DEST/meta-portrait.png"
After rendering into content/events/<slug>/, set frontmatter:
meta_image: /events/<slug>/meta.png
meta_image_square: /events/<slug>/meta-square.png
(Optionally set overline: if you customized it.) The three extra sizes are not wired to meta tags — they're for manual social use. Setting meta_image makes the build skip the auto card for this page (the override wins; see layouts/partials/meta-image-url.html).
Write all five PNGs to the chosen output dir and report their paths. No frontmatter, no bundle.
Report: mode; the event (event-bound); overline + title used; people shown (and where each photo came from); logos placed; the five output paths; and for event-bound, the frontmatter that was set. Always remind the user to preview the PNGs (and, event-bound, the live page) before committing. Preview the landscape and square first — those are the ones that ship as og:image.
render-event.mjs prints warning: could not resolve … for any photo/logo it couldn't find — fix the path or re-resolve via the ladder..jpg are handled automatically — the renderer sniffs the real type.)title → ask the user. Long titles auto-shrink to fit; you don't need to trim them.references/logos.md — cached company-logo inventory + the resolution ladder.scripts/meta-images/events.mjs. Shared CLI: scripts/meta-images/render-event.mjs.Take pulumi/event-meta-image from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.