- name
- event-meta-image
- description
- 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.
# `/event-meta-image` — Generate event / workshop social cards
You 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.)
---
## [Step 0/6] Pick the mode
- **Event-bound** — there is a target `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.
- **Standalone** — the user just wants images ("generate event images for …", no event attached). Collect inputs directly (Steps 3–5), skip frontmatter, and write the PNGs to an output dir (default `.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).
## Interaction mode — when to ask questions
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.
- **Unattended — never ask.** Trigger when `$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:
- **overline** = `event_type`; **title** = frontmatter `title`; **speakers** = frontmatter `presenters[].photo`; **additionalText** = the derived "With …" line, with **additionalTextShort** = its names-only form.
- **people** = frontmatter presenters **plus** any external co-presenter named in the title/body.
- **logos** = `["pulumi"]` **plus** any partner company named in the title/body.
- Auto-resolve each derived photo/logo via the Step 3 / Step 4 ladders. **On a resolution miss, skip that one asset with a warning and keep going — never block.** No button, no secondary title.
- **Default — ask only when needed.** Same automatic derivation and search as unattended mode, but fall back to a single targeted `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`.
## [Step 1/6] Locate the event (event-bound)
1. If `$ARGUMENTS` contains an event path or slug, use `content/events/<slug>/index.md`.
2. Otherwise run `git status` / `git diff --name-only` for changed `content/events/*/index.md`.
3. Multiple candidates → ask with `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).
## [Step 2/6] Seed from frontmatter
Pull from frontmatter:
- **title** (required — the card's main title; the renderer fits it to the box).
- **event_type** → the default **overline** (uppercased automatically).
- **presenters**: `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` (plus its names-only form as `additionalTextShort`), and `["pulumi"]` as `logos`.
## [Step 3/6] Add people not in frontmatter (external co-presenters)
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):
1. An existing `presenters[].photo` (already have it).
2. `static/images/team/<kebab-name>.{jpg,jpeg,png}`.
3. `static/images/people/<kebab-name>.{jpg,jpeg,png}`.
4. **Web**: `WebSearch` for a headshot / company bio photo, then `WebFetch` the image. LinkedIn images are usually auth-gated — expect this to fail and fall through.
5. **Ask** the user for a photo URL or local file path.
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.
## [Step 4/6] Company / partner logos
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:
- **Default to the WORDMARK** — the lockup that includes the **company name** (e.g. the Microsoft squares *plus* "Microsoft"), not the bare glyph. A mark-only icon next to "Pulumi" reads as unfinished. Only fall back to a glyph if no wordmark exists or the glyph IS the brand (rare).
- **Prefer the HORIZONTAL variant.** The renderer scales every logo to a fixed row *height*, so a vertical/**stacked** lockup (mark stacked above the name) renders its text tiny and unbalanced next to Pulumi. Choose the horizontal lockup (mark beside the name, or the wordmark alone). Rule of thumb: check the SVG `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).
- **Must be an SVG or a TRANSPARENT PNG.** The card background is near-white (`#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:
1. `.claude/commands/event-meta-image/assets/logos/<name>.svg` (cached — see `references/logos.md`).
2. `static/logos/**` (the site's own brand assets — often clean SVG wordmarks).
3. **Web** — `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>"`.
4. **Ask the user** for a transparent SVG/PNG wordmark. Prefer asking over shipping a mark-only or white-background logo.
**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.
## [Step 5/6] Options
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.
- **overline** — default `event_type` (e.g. `WORKSHOP`, `WEBINAR`, `PANEL`); honor an override if `$ARGUMENTS` gives one.
- **secondary title** — hidden. Set it only if the user explicitly asks for a small line above the title.
- **which people / logos to include and their order** — when there are more than 5 people, pick the 5 to show; order logos Pulumi-first.
**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.
## [Step 6/6] Render the five sizes
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`):
```json
{
"overline": "WORKSHOP",
"title": "Advanced CI/CD for AWS using Pulumi and GitHub Actions",
"secondaryTitle": "",
"additionalText": "With Jane Doe - Staff Engineer, Acme and John Roe",
"additionalTextShort": "With Jane Doe 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.)
- `additionalTextShort` is the names-only byline (`With A and B`) used by the two **square** sizes, whose text column is too narrow for full "Name - Role, Company" runs. Optional: supply it whenever `additionalText` carries roles, or omit it and let a `presenters` array derive both. The other three sizes always use `additionalText`.
- `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) |
```bash
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"
```
### Event-bound writes (only)
After rendering into `content/events/<slug>/`, set frontmatter:
```yaml
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`).
### Standalone writes (only)
Write all five PNGs to the chosen output dir and report their paths. No frontmatter, no bundle.
## Wrap-up
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.
## Error handling
- `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.
- "Offset is outside the bounds of the DataView" or a decode error → the image is corrupt/truncated; re-fetch it. (Mislabeled extensions like a PNG named `.jpg` are handled automatically — the renderer sniffs the real type.)
- No `title` → ask the user. Long titles auto-shrink to fit; you don't need to trim them.
- Don't generate or AI-synthesize brand imagery or the Pulumi mascot; use the bundled logo and approved assets only.
## References
- `references/logos.md` — cached company-logo inventory + the resolution ladder.
- Renderer internals / layout: `scripts/meta-images/events.mjs`. Shared CLI: `scripts/meta-images/render-event.mjs`.
Auf GitHub ansehen