5.8 KiB
Custom card: generate public/og.jpg
For prompt-craft, composition, and blind read-back verification, follow the
imagine and game-asset-core skills — this file owns the card-specific
contract only (size, lockup, wiring).
-
Set the canvas with
aspect_ratio: "16:9". The call looks like{ "prompt": "…", "aspect_ratio": "16:9" }— ratio words in the prompt do not set the canvas. At 2mp, 16:9 renders 1792×1008, so the normalize below cover-crops to 1200×630 trimming only ~3% vertically, which a centered title survives. A narrower canvas is what kills titles: from 3:2 the same crop takes ~21% vertically, straight through the lockup. Paths:- Default — one call with
aspect_ratio: "16:9":imagine_text_to_imagewith the art + baked title. Check the output dimensions viaread_file/ Pillow on the returnedfile_path; if the ratio missed, reframe or use the API path. - Reframe if needed: pass the prior
file_pathintoimagine_image_to_imagewithaspect_ratio: "16:9"and a prompt like "extend the scenery left and right into a wider frame; keep the title lettering and central subject exactly as they are". - Optional — true 2:1 via the xAI Images API:
POST https://api.x.ai/v1/images/generationswith"aspect_ratio": "2:1"andresponse_format: "b64_json"using the injectedXAI_API_KEY(see thexai-apiskill). 2mp 2:1 is 1984×992; normalize then trims only ~2.4% per side and nothing vertical.
Build the prompt from the app's theme, palette, and characters. If the app already has a key generated asset (hero sprite, title scene), pass its
file_pathintoimagine_image_to_imageso the card matches in-game art — same 16:9 + check-the-output rule applies. Last resort only (noimagine_text_to_imageand no xAI Images API): stay on whatever canvas you have and keep the entire title block inside the middle half of the frame height, with the crop-clipping check in step 6 as the gate. - Default — one call with
-
Bake the title in like a game cover. Store-page covers (Stardew Valley, Cuphead) lead with a short stylized logo-type title. Put the exact app name in quotes in the prompt; 1–3 strong words; optional short tagline under the title in smaller lettering (exact words in quotes).
- Stack multi-word titles into a two-line lockup ("SKY" over "STRIKE").
- Center the block both ways with generous margins — avoid "upper third" / percentage placement (models hug the edge).
- Bound the width: lettering spans roughly half to two-thirds of the frame, never border to border.
- Keep comfortable margins anyway. From a 16:9 canvas the normalize below trims only ~3% vertically; from a 2:1 API canvas it trims nothing vertical and ~2.4% per side. The crop turns destructive when the canvas comes back off-ratio — an edit that pinned to its input's ratio, or a model miss — so check the raw canvas dimensions before cropping, and re-ratio first if it isn't ~16:9 or 2:1.
-
Verify glyphs and layout on read-back (see
imagine/game-asset-corefor the blind-describe loop). On a garble or layout miss, regenerate with a corrected prompt — never try to move a logo withimagine_image_to_image(frame translation / seams). After two failed attempts, ship the card artwork-only (titleless).Intentional exception vs
imagine's "rebuild text with code" rule: the share card is a single static PNG; there is no reliable in-sandbox path to composite crisp code-drawn lettering onto generative art for this asset, so a clean titleless card is the correct fallback after two glyph failures. -
Normalize to exactly 1200×630 JPEG with the baked-in ffmpeg (cover-crop — from 16:9 this shaves ~3% top/bottom; from 2:1 ~2.4% per side and nothing vertical). JPEG, not PNG: the card is photographic generative art, and a PNG of it lands at 1–2 MB — heavy enough that link scrapers (X card previews included) time out or skip the image, so the card silently fails to unfurl. JPEG at this quality is ~150–300 KB with no visible loss at unfurl size:
ffmpeg -y -i card-raw.jpg \ -vf "scale=1200:630:force_original_aspect_ratio=increase,crop=1200:630" \ -q:v 4 /workspace/.grok/og.jpg.tmp node scripts/write-atomic.mjs /workspace/.grok/og.jpg.tmp public/og.jpg -
Tell the injector the card is custom — set
"card": "custom"insrc/lib/og/site.json, handed over the same way, and keeppublic/og.jpg:node scripts/write-atomic.mjs /workspace/.grok/site.json.tmp src/lib/og/site.jsonBake also infers custom from the file if the flag is missing, but brand-check still requires the field. The injector emits the absolute
https://${host}/og.jpgURL. Do not addog:imageto__root.tsx. -
Verify before finishing (Pillow is installed;
ffprobeis not):python3 -c " from PIL import Image; import os im = Image.open('public/og.jpg') kb = os.path.getsize('public/og.jpg') // 1024 print(im.size, f'{kb} KB')" # expect: (1200, 630) and under 600 KB (keeps X and other scrapers # reliable; target <= 300 KB — if over, bump -q:v up a step and re-encode)Read back the final
public/og.jpg, not the pre-crop raw — the crop is where clipping happens. This is a hard gate, not an impression: if any title glyph touches a frame edge or is visibly cut, the card is rejected — do not ship it, whatever else is right about it. Fix the ratio (step 1) or regenerate with the width bound restated; a shipped decapitated title is worse than the placeholder. Then confirm the card reads like this app at thumbnail size — clear subject, correctly spelled title (if any), comfortable margins.