generate-consistent-og-images — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited generate-consistent-og-images (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Two variables per request: aspect ratio and a one-sentence prompt. Everything else — brand reference image, color palette, style type, negative prompt, seed, rendering speed, magic-prompt flag — is locked at the project's DESIGN.md level so every image in a set looks like it belongs in the same family.banner but now need banner_tall for WhatsApp / iMessage)DESIGN.md at allThe Lossless Group optimizes share imagery for mobile chat previews — iMessage and WhatsApp first — and treats LinkedIn / Twitter / Bluesky / Slack as concentric rings around that target.
In practice this means:
3x4 (WhatsApp / iMessage default) and 2x3 (extra-tall) variants are first-class assets, not afterthoughts.This shapes the aspect-ratio enum below and the naming convention.
IDEOGRAM_API_KEY in ~/.secretsSame pattern as the crawl-fetch-ingest skill: keys never live in source.
# ~/.secrets (chmod 600)
export IDEOGRAM_API_KEY="..."# ~/.zshenv adds one line (already in place if other Lossless skills work)
[ -f ~/.secrets ] && source ~/.secretsNo MCP server is required. Ideogram's v3 endpoint accepts a direct HTTPS request with Api-Key header. Bash + curl is enough. If a future MCP wrapper is preferred, register it the same way the firecrawl/tavily wrappers are registered in crawl-fetch-ingest's setup.md — but it's not necessary for this skill.
The skill assumes the project has some established aesthetic. If it doesn't — brand-new site, no design decisions yet — that conversation has to happen before image generation. Use the astro-knots skill or pair with the user on a Brand & Style first pass before invoking this one.
Five steps, in order. Steps 1–2 are setup the first time the skill runs against a project; steps 3–5 are the per-image work that repeats.
DESIGN.mdLook for DESIGN.md at the project root.
Brand & Style, Colors, Typography, Layout, Elevation & Depth, Shapes, Components).templates/design-md-scaffold.md). Once a first-pass DESIGN.md exists, return here to add the imagery: extension and proceed.The skill cannot proceed to image generation without a DESIGN.md because the imagery recipe (next step) references its color palette tokens.
imagery: block to DESIGN.mdThe imagery: block is outside the Google Stitch spec's standard token groups, but spec-compliant consumers preserve unknown top-level keys (per the spec's "Consumer Behavior for Unknown Content" table). It's safe to keep there as the single source of truth.
The canonical block lives at templates/imagery-block.yaml in this skill. Open that file, adapt three things to the target project, paste into the project's DESIGN.md frontmatter (after components:, before the closing ---):
| Field | What to adapt |
|---|---|
style_reference.path | The project's canonical hero/banner image. If the project has none yet, generate a one-off using a quick prompt + the project's colors: palette, save it, and use that as the reference for all subsequent runs. |
color_palette.members | Map from the project's colors: tokens to Ideogram's color_hex + color_weight shape. Weight the surface background highest (0.4–0.5) and brand accents lower. |
defaults.seed | Pick a fixed seed for the project. Any integer is fine; bump only when the canon shifts. |
Everything else in templates/imagery-block.yaml — style_type: AUTO, magic_prompt: OFF, rendering_speed: QUALITY, the aspect_ratios enum, the negative_prompt set, the prompt constraints — is project-agnostic and should be copied verbatim. The whole point of locking those is that they don't vary across projects either.
API constraint worth knowing:style_type: AUTOis required wheneverstyle_reference_imagesis uploaded. The v3 endpoint rejectsDESIGN/REALISTIC/FICTIONin that combination with the error "Please use AUTO or GENERAL style type with style_codes, style_reference_images or style_preset." The reference image carries the aesthetic;AUTOlets it through.
The matching prose section (## Imagery) belongs in the body of DESIGN.md immediately before ## Do's and Don'ts. The reference implementation in content-farm/splash/DESIGN.md is the canonical example.
Read the project's DESIGN.md imagery: block. The Ideogram request is built by:
imagery.defaults as-is.imagery.color_palette as-is.imagery.style_reference.path as style_reference_images.imagery.negative_prompt as-is.imagery.aspect_ratios for aspect_ratio — based on the user's stated target format (see § "The aspect-ratio enum" below).prompt field — see § "Prompt convention" below.No other field varies. If the user asks to vary something else (e.g., "use a brighter palette"), that's a DESIGN.md change, not a per-request override.
The canonical curl call lives at templates/ideogram-request.sh. Substitute {prompt} and {aspect_ratio} and run. The script reads IDEOGRAM_API_KEY from the environment (~/.secrets).
POST https://api.ideogram.ai/v1/ideogram-v3/generate
Content-Type: multipart/form-data
Api-Key: $IDEOGRAM_API_KEYThe response carries a url for each generated image. Images expire on the Ideogram side; download immediately to the target project's public/ (or equivalent static asset directory).
ogimage__{Site-Name}--{Format-Or-Variant}.{ext}Site-Name — the project's canonical name in PascalCase or Hyphen-Joined-PascalCase: Content-Farm, Reach-Edu-Hub, Calmstorm-Decks, Perplexed.Format-Or-Variant — one of: Default, Banner, BannerTall, BannerTallMax, Portrait, PortraitTall, Square. Default aliases whichever format is the canonical share image (typically Banner)..jpg per the open-graph-share-seo-geo skill's JPEG-over-WebP rule. Convert from Ideogram's PNG output with ffmpeg -i in.png -q:v 2 out.jpg (quality 2 is near-lossless and lands ~95 KB at 1200×630).Example set for content-farm:
public/ogimage__Content-Farm--Default.jpg # the canonical banner (also serves as style_reference)
public/ogimage__Content-Farm--Banner.jpg # alias of Default
public/ogimage__Content-Farm--BannerTall.jpg # WhatsApp / iMessage default tall
public/ogimage__Content-Farm--BannerTallMax.jpg # dramatic-tall variant
public/ogimage__Content-Farm--Portrait.jpg # LinkedIn portrait / IG feed
public/ogimage__Content-Farm--PortraitTall.jpg # Stories / Reels / TikTok
public/ogimage__Content-Farm--Square.jpg # avatars, square unfurlsIf the image is destined for a CDN (per the open-graph-share-seo-geo skill's "host the OG image remotely" rule), upload there as well; the same file naming travels.
| Format key | Ideogram aspect_ratio | When to use |
|---|---|---|
banner | 16x9 | Default share — OpenGraph, Twitter / X, Slack, generic |
banner_tall | 3x4 | WhatsApp & iMessage chat-preview cards (the Lossless default tall) |
banner_tall_max | 2x3 | Dramatic-tall variant; use when the subject genuinely benefits from height |
portrait | 4x5 | LinkedIn portrait, Instagram feed post |
portrait_tall | 9x16 | Instagram Stories, Reels, TikTok |
square | 1x1 | Avatars, square OG fallbacks, Discord embeds |
The keys live in imagery.aspect_ratios of the project's DESIGN.md; the values may differ if a project has a different target mix, but the format names stay the same across projects so cross-site automation can talk in one vocabulary.
The prompt is the only free-text channel in the request. Two ingredients per prompt, nothing else:
Bad framing (subject-first; produces 85%-tall subjects that swallow the overlay zone):
"An isometric quill pen with paper-leaf feathers, growing from a stack of paper-cut card tiles, in the lower third of the frame, top two-thirds open."
Good framing (empty-region-first, two clauses, explicit numeric split):
"Top 1/3 of frame is empty negative space, dark gradient sky. Bottom 2/3 contains an isometric quill pen with paper-leaf feathers, growing from a stack of paper-cut card tiles."
Three rules that survive aspect-ratio changes:
subject in top half). The negative prompt isn't a substitute for the positive empty-region declaration — it's a belt alongside the positive suspenders.The underlying principle: empty space won't be left as residue. It has to be declared, named, and given content — otherwise the subject expands to fill the canvas. This was discovered empirically over iter1 → iter2 → iter3 on the Perplexed OG set; iter1 and iter2 used subject-first framing and produced subjects at 75-85% canvas height; iter3 used empty-region-first framing and produced subjects at 40-65% canvas height with the upper region genuinely empty.
Target ≤220 characters total. Past that, hard composition asks start losing to subject elaboration.
Forbidden in the prompt (already encoded via locked channels, never repeat):
Lossless, Content Farm, plugin names)cyan, dark, warm)vibrant-minimal, build-in-public, monospace)paper-cut, isometric, atmospheric) — unless the subject genuinely requires them and the style reference image already establishes them as canonComposition templates that survive aspect-ratio shifts cleanly. Note the two-clause structure — empty-region declaration first (with concrete content), subject placement second:
Top 1/3 of frame is empty negative space, dark gradient sky. Bottom 2/3 contains {subject}.Left 1/3 of frame is empty negative space, soft atmospheric backdrop. Right 2/3 contains {subject}.Top half of frame is empty atmospheric gradient. Bottom half contains {subject}, centered horizontally.Top-right 2/3 of frame is empty negative space, dark muted sky. Bottom-left 1/3 contains {subject}.The locked recipe is for share imagery — the assets a third party sees when a link is unfurled. The user / team may also need:
These are legitimate departures from OG-image canon. They may use different palette weights, different aspect ratios, different style references, even a different style_type. When the user asks for one of these:
aspect_ratios enum (use Ideogram's resolution field instead of aspect_ratio for pixel-exact sizing).ogimage__ naming convention. illustration__Section-Name--Variant.jpg, hero__Page-Name.jpg, or whatever the project's local convention is — but never use the ogimage__ prefix, since that name implies the canonical recipe was followed.The point: don't pollute the OG-image set with one-off custom imagery, and don't constrain one-off custom imagery with OG-image rules.
User says: "Regenerate the WhatsApp tall card for content-farm."
# 1. Project: content-farm/splash. DESIGN.md exists, imagery: block exists.
# No setup work needed.
# 2. Pick aspect ratio: banner_tall (3x4)
# 3. Author one-sentence prompt with subject + composition:
PROMPT="Top 1/3 of frame is empty negative space, dark gradient sky. Bottom 2/3 contains an isometric sprout growing from a stack of paper-cut tiles."
# 4. Call Ideogram (see templates/ideogram-request.sh):
bash templates/ideogram-request.sh "$PROMPT" 3x4
# 5. Download response[0].url, convert PNG → JPEG, save as:
# public/ogimage__Content-Farm--BannerTall.jpgUser says: "I just scaffolded reach-edu-hub. Generate the OG banner."
# 1. Check for DESIGN.md at reach-edu-hub/ → absent
# 2. Pair with the user to author DESIGN.md from the spec:
# - Frontmatter token groups (colors, typography, rounded, spacing, components)
# - Eight prose sections in spec order
# Reference: github.com/google-labs-code/design.md and
# content-farm/splash/DESIGN.md as a shape example
# 3. No style_reference exists yet. Generate a one-off with only color_palette
# + style_type=AUTO to seed the canon, save as the reference, then add
# the imagery: block pointing at it.
# 4. Now run the canonical recipe across banner, banner_tall, portrait, square.Ideogram generations are non-deterministic in composition (even with seed + every other parameter locked) and expensive in attention to curate. When we land a "good" image after multiple iterations, the prior candidates and the prior canonical pick are historical record worth keeping — they justify the final pick, document what was tried, and give a baseline if a future run drifts.
Two preservation layers:
The templates/ideogram-request.sh script writes each run into a fresh timestamped directory:
<project>/.ideogram-candidates/<subject>-<aspect>-<YYYYMMDD-HHMMSS>/
candidate-0.png
candidate-1.png
candidate-2.png
candidate-3.png
response.json ← Ideogram's echo: actual prompt, seed used, resolution, expired URLThe dot-prefixed parent directory (.ideogram-candidates/) sits outside the project's static-asset directory (public/, static/, etc.) so Astro / 11ty / Next / any static-site framework doesn't deploy 1.4MB PNGs to production. Per-run subdirs are timestamped → never overwritten across runs. Two BannerTall passes done an hour apart produce two distinct subdirectories.
Decide per project whether to .gitignore .ideogram-candidates/. Pro keeping in git: auditable history of every gen pass. Con: PNGs bloat repo size. Lossless default: keep in git unless the repo is shared with non-team contributors who'd be confused by the byte size.
The canonical-named files (ogimage__{Site}--{Format}.jpg) live in public/ because the splash needs them at deploy time. When you re-run the recipe and pick a NEW winner for the same format, don't overwrite the existing canonical file directly. Instead:
ogimage__{Site}--{Format}.jpg to <project>/.ogimage-archive/ogimage__{Site}--{Format}--{YYYY-MM-DD}.jpg (creating the archive directory if needed — also dot-prefixed, also outside public/).# Pseudo-recipe — the actual ffmpeg step lives in your conversion script:
ARCHIVE="<project>/.ogimage-archive"
mkdir -p "$ARCHIVE"
if [[ -f "public/ogimage__Perplexed--BannerTall.jpg" ]]; then
mv "public/ogimage__Perplexed--BannerTall.jpg" \
"$ARCHIVE/ogimage__Perplexed--BannerTall--$(date +%Y-%m-%d).jpg"
fi
ffmpeg -y -i "<new-pick>.png" -q:v 2 "public/ogimage__Perplexed--BannerTall.jpg"The unfurler URL stays stable (it's still …/ogimage__Perplexed--BannerTall.jpg); only the bytes behind it change. Old bytes survive in .ogimage-archive/ with a date stamp so you can A/B which version unfurled how.
Without Layer 1, repeat runs erase candidates we considered. Without Layer 2, the splash's deployed history becomes irrecoverable as soon as you generate v2.
negative_prompt with the failure mode (subject in top half). Empty space won't be left as residue; it has to be declared, named, and given content. See the "Composition" subsection above for the full rule and the iter1→iter3 evidence that drove the refinement.banner_tall alongside banner.content-farm/splash/DESIGN.md is canonical (frontmatter groups, eight prose sections in order). The values (palette, fonts, sizes) belong to Content Farm and must be re-authored for each new project.public/ verbatim. ~16 MB of PNG candidates ships to GitHub Pages otherwise. Keep raw candidates at <project>/.ideogram-candidates/ — dot-prefixed, outside public/.AUTO whenever style_reference_images is set.templates/imagery-block.yaml in this skill — the canonical imagery: recipe block, ready to drop into a new DESIGN.mdtemplates/ideogram-request.sh in this skill — the canonical curl invocationcontent-farm/splash/DESIGN.md — the canonical full-example of a Lossless DESIGN.md with both the standard Stitch sections and the imagery: recipemaintain-design-md skill — owns Step 1 of this flow when no DESIGN.md exists, plus the maintenance discipline (drift audits, when code changes should bounce back into the doc)open-graph-share-seo-geo skill — sibling skill on the delivery side: where to host the file, what HTTP headers it needs, JPEG-over-WebP rule, the og:image:type invariant, cache-busting recipescrawl-fetch-ingest skill — the precedent for the ~/.secrets pattern this skill borrowsastro-knots skill — when scaffolding a new site that needs a DESIGN.md from scratch~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.