Share cards, favicon, and app icons
A deployed app (https://{name}.grok.me) unfurls with a 1200×630 card, and
every app (preview included) shows a favicon in the browser tab. Both are
wired in the root head() — AGENTS.md § "First scaffold" is the source of
truth for src/routes/__root.tsx. Extend __root.tsx; never replace it
wholesale (auth SSR, redesign, another skill's excerpt). Dropping og:image
or the favicon link ships a broken card / blank tab icon, and npm run dev /
build will not catch it.
Parallelize the brand-asset pass (default)
The identity bundle — public/og.jpg, public/favicon.svg, PWA icons when
installable — needs only the app's name, theme, and palette, and writes only
public/ plus a few head() / manifest lines. Start it as a background
task with the task tool early in the build (AGENTS.md § "Parallel work"),
right after the name and palette are settled: give the task this skill and
sole ownership of public/ brand assets, keep building the app, and
integrate its head() edits at the end (wait_tasks before the final
verify). Image generation is pure waiting — a build that runs the card pass
inline at the end serializes minutes of model latency behind an already
finished app.
A default, not a hard rule: stay sequential when the user is art-directing
the card, when the app art the card should reuse doesn't exist yet, or when
the task tool isn't available.
Which card an app gets
Default: generate a custom card. Any app with a face of its own gets one
built from its own art (below):
- Games — every kind and rendering tech. Canvas and WebGL, but equally
DOM-rendered board, card, word, puzzle, and quiz games. A tic-tac-toe grid
made of divs is still a game and still ships a custom card.
- Whimsical, playful, and toy apps — generators, virtual pets, screensavers.
- Creative tools — drawing, music, photo, design, anything whose output is
visual or expressive.
- Content- and brand-forward apps — landing pages, portfolios,
storefronts, anything presenting an identity to visitors.
When in doubt, make the custom card — a themed card is the better unfurl
for anything a user would show someone.
Plain utility apps only (converters, invoice/CRUD trackers, internal
dashboards, minimal notes/admin tools — apps whose face is the data, not a
theme) → keep the default og.grok.me card:
https://og.grok.me/v1/card.png?host={VITE_PUBLIC_HOSTNAME}&title={APP_NAME}
plus og:image:width / og:image:height of 1200 / 630. When the user
renames the app, update APP_NAME — it is both the document title and the
painted card title.
Either way: live preview has no VITE_PUBLIC_HOSTNAME, so emit no
og:image at all (text-only unfurl is expected). On publish the platform
injects the hostname — do not write a .env for it. Card pixels update on
the next deploy (the URL is baked into HTML at build time).
Custom card: generate public/og.jpg
For prompt-craft, composition, and blind read-back verification, follow the
imagine and game-asset-core skills — this skill owns the card-specific
contract only (size, lockup, wiring).
Get a true 2:1 canvas — generate_image alone cannot produce one.
Its only ratio knob is orientation, and landscape is hard-mapped to a
3:2 canvas (1176×784); there is no aspect_ratio parameter, and ratio
words in the prompt do not control the canvas. Cropping 3:2 down to
the card's 1200×630 trims ~21% vertically — straight through a
centered title. Two sanctioned paths to 2:1 (which renders 1408×704, so
the normalize below trims only ~2.5% per side and nothing vertical):
- Default — generate, then reframe:
generate_image
(orientation: "landscape") for the art + baked title, then one
edit_image pass with aspect_ratio: "2:1" 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". edit_image is the
documented ratio-change path. Check the output dimensions — if the
edit came back at the source ratio (some edit paths pin to the input),
switch to the direct path below instead of re-trying.
- Direct — one call, guaranteed ratio: the xAI Images API with
"aspect_ratio": "2:1" using the injected XAI_API_KEY (see the
xai-api skill; same generator underneath): POST https://api.x.ai/v1/images/generations with response_format: "b64_json". The API accepts 2:1 even where docs don't list it.
Build the prompt from the app's theme, palette, and characters. If the
app already has a key generated asset (hero sprite, title scene), feed it
to edit_image so the card matches in-game art — same 2:1 +
check-the-output rule applies.
Last resort only (both paths unavailable): stay on the 3:2 canvas 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.
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 2:1 canvas the normalize
below trims nothing vertically and only ~2.5% per side, so the width
bound above is the real protection. The crop only turns destructive
when the canvas is off-ratio (a 3:2 canvas that never got reframed, an
edit that pinned to its input's ratio, a model miss) — check the raw
canvas dimensions before cropping, and re-ratio first if it isn't 2:1.
Verify glyphs and layout on read-back (see imagine / game-asset-core
for the blind-describe loop). On a garble or layout miss, regenerate with a
corrected prompt — never try to move a logo with image_edit (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 a 2:1 canvas this shaves ~2.5% off each side and leaves the vertical
composition untouched). 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 public/og.jpg
Point og:image at the app's own asset — absolute URL, scrapers do not
resolve relative paths — keeping the no-host guard:
const ogImage = host ? `https://${host}/og.jpg` : undefined;
Keep og:image:width / og:image:height at 1200 / 630.
Verify before finishing (Pillow is installed; ffprobe is 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.
If neither generate_image/edit_image nor the xAI Images API is available,
fall back to the og.grok.me card — never ship a missing or broken
og:image URL.
Regenerate the card when the app's visual identity materially changes (theme
overhaul, new hero art) — and on rename: the title is baked into the pixels,
so update APP_NAME and regenerate the card with the new name. (A titleless
fallback card survives a rename without regeneration.)
Favicon: hand-author public/favicon.svg
Every app gets one, and it works in live preview immediately (no host needed).
Write the SVG by hand — never image_gen. It must stay crisp at 16px:
one bold glyph or shape, flat fills from the app's design tokens, a square
viewBox, a handful of elements at most. For whimsical apps an emoji-text
SVG is a fine quick win:
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<rect width="100" height="100" rx="20" fill="#1a1b26"/>
<text x="50" y="50" font-size="62" text-anchor="middle"
dominant-baseline="central">🥕</text>
</svg>
Wire it in the root head():
links: [
{ rel: "icon", type: "image/svg+xml", href: "/favicon.svg" },
{ rel: "stylesheet", href: appCss },
],
Verify it renders non-blank and legible small (browser tab in a preview
screenshot, or read back a rasterized copy). Update it when the app's theme
or name changes meaning — it is part of the app's identity, not a set-and-forget.
PWA icons: only for installable apps
When the app ships a web manifest (the user asked for installable / PWA /
home-screen behavior — do not invent a manifest just to have icons), add
raster icons derived from the favicon artwork so the identity stays
consistent:
public/icon-192.png and public/icon-512.png — the favicon's glyph on
its tile, rasterized at size. Playwright (baked into the sandbox) can
screenshot the served /favicon.svg at a 192/512 viewport; or redraw the
same mark as a flat PNG. Keep it bold and flat — no photographic detail.
- A maskable variant (
"purpose": "maskable") needs the glyph inside the
center ~80% safe zone so launcher shapes don't clip it.
- Wire the manifest
icons array plus theme_color / background_color
from the app's design tokens, and read the 192 back to confirm it stays
legible.
Not supported
No /api/og route, no runtime image renderer, no per-route cards, no runtime
og:* mutation. The card is one static site-wide image chosen at build time.
If you add robots.txt, never blanket Disallow: / — crawlers must fetch /
to read the meta tags.
1---2name: og-23description: Share-link previews and app identity for apps on *.grok.me: the og:image card in the root head, the SVG favicon, and PWA icons for installable apps. Use when scaffolding, renaming, or restyling the app — and for share / unfurl / OG / Twitter card questions. A custom 1200×630 card from the app's own art is the default — games of every kind (DOM board/word games included), whimsical apps, creative tools, and brand-forward pages; only plain utilities keep the placeholder. By default run the whole brand-asset pass in a parallel background task (the `task` tool) while the app builds. Triggers on "share", "rename", "app name", "OG", "Open Graph", "twitter card", "unfurl", "og:image", "link preview", "social card", "thumbnail", "preview image", "favicon", "app icon", "PWA", "manifest", "installable", "home screen", "SEO", "meta description".4---56# Share cards, favicon, and app icons78A deployed app (`https://{name}.grok.me`) unfurls with a 1200×630 card, and9every app (preview included) shows a favicon in the browser tab. Both are10wired in the root `head()` — AGENTS.md § "First scaffold" is the source of11truth for `src/routes/__root.tsx`. **Extend `__root.tsx`; never replace it12wholesale** (auth SSR, redesign, another skill's excerpt). Dropping `og:image`13or the favicon link ships a broken card / blank tab icon, and `npm run dev` /14`build` will not catch it.1516## Parallelize the brand-asset pass (default)1718The identity bundle — `public/og.jpg`, `public/favicon.svg`, PWA icons when19installable — needs only the app's name, theme, and palette, and writes only20`public/` plus a few `head()` / manifest lines. **Start it as a background21task with the `task` tool early in the build** (AGENTS.md § "Parallel work"),22right after the name and palette are settled: give the task this skill and23sole ownership of `public/` brand assets, keep building the app, and24integrate its `head()` edits at the end (`wait_tasks` before the final25verify). Image generation is pure waiting — a build that runs the card pass26inline at the end serializes minutes of model latency behind an already27finished app.2829A default, not a hard rule: stay sequential when the user is art-directing30the card, when the app art the card should reuse doesn't exist yet, or when31the `task` tool isn't available.3233## Which card an app gets3435**Default: generate a custom card.** Any app with a face of its own gets one36built from its own art (below):3738- **Games — every kind and rendering tech.** Canvas and WebGL, but equally39 DOM-rendered board, card, word, puzzle, and quiz games. A tic-tac-toe grid40 made of divs is still a game and still ships a custom card.41- **Whimsical, playful, and toy apps** — generators, virtual pets, screensavers.42- **Creative tools** — drawing, music, photo, design, anything whose output is43 visual or expressive.44- **Content- and brand-forward apps** — landing pages, portfolios,45 storefronts, anything presenting an identity to visitors.4647**When in doubt, make the custom card** — a themed card is the better unfurl48for anything a user would *show someone*.4950- **Plain utility apps only** (converters, invoice/CRUD trackers, internal51 dashboards, minimal notes/admin tools — apps whose face is the data, not a52 theme) → keep the default `og.grok.me` card:5354 ```55 https://og.grok.me/v1/card.png?host={VITE_PUBLIC_HOSTNAME}&title={APP_NAME}56 ```5758 plus `og:image:width` / `og:image:height` of `1200` / `630`. When the user59 renames the app, update `APP_NAME` — it is both the document title and the60 painted card title.6162Either way: **live preview has no `VITE_PUBLIC_HOSTNAME`, so emit no63`og:image` at all** (text-only unfurl is expected). On publish the platform64injects the hostname — do **not** write a `.env` for it. Card pixels update on65the **next deploy** (the URL is baked into HTML at build time).6667## Custom card: generate `public/og.jpg`6869For prompt-craft, composition, and blind read-back verification, follow the70`imagine` and `game-asset-core` skills — this skill owns the **card-specific**71contract only (size, lockup, wiring).72731. **Get a true 2:1 canvas — `generate_image` alone cannot produce one.**74 Its only ratio knob is `orientation`, and `landscape` is hard-mapped to a75 **3:2** canvas (1176×784); there is no `aspect_ratio` parameter, and ratio76 words in the prompt do **not** control the canvas. Cropping 3:2 down to77 the card's 1200×630 trims **~21% vertically** — straight through a78 centered title. Two sanctioned paths to 2:1 (which renders 1408×704, so79 the normalize below trims only ~2.5% per side and nothing vertical):8081 - **Default — generate, then reframe:** `generate_image`82 (`orientation: "landscape"`) for the art + baked title, then one83 `edit_image` pass with **`aspect_ratio: "2:1"`** and a prompt like84 "extend the scenery left and right into a wider frame; keep the title85 lettering and central subject exactly as they are". `edit_image` is the86 documented ratio-change path. **Check the output dimensions** — if the87 edit came back at the source ratio (some edit paths pin to the input),88 switch to the direct path below instead of re-trying.89 - **Direct — one call, guaranteed ratio:** the xAI Images API with90 `"aspect_ratio": "2:1"` using the injected `XAI_API_KEY` (see the91 `xai-api` skill; same generator underneath): `POST92 https://api.x.ai/v1/images/generations` with `response_format:93 "b64_json"`. The API accepts `2:1` even where docs don't list it.9495 Build the prompt from the app's theme, palette, and characters. If the96 app already has a key generated asset (hero sprite, title scene), feed it97 to `edit_image` so the card matches in-game art — same 2:1 +98 check-the-output rule applies.99 **Last resort only** (both paths unavailable): stay on the 3:2 canvas and100 keep the entire title block inside the **middle half** of the frame101 height, with the crop-clipping check in step 6 as the gate.1022. **Bake the title in like a game cover.** Store-page covers (Stardew Valley,103 Cuphead) lead with a short stylized logo-type title. Put the exact app name104 in quotes in the prompt; 1–3 strong words; optional short tagline under the105 title in smaller lettering (exact words in quotes).106 - **Stack multi-word titles** into a two-line lockup ("SKY" over "STRIKE").107 - **Center the block both ways** with generous margins — avoid "upper third"108 / percentage placement (models hug the edge).109 - **Bound the width**: lettering spans roughly half to two-thirds of the110 frame, never border to border.111 - **Keep comfortable margins anyway.** From a 2:1 canvas the normalize112 below trims nothing vertically and only ~2.5% per side, so the width113 bound above is the real protection. The crop only turns destructive114 when the canvas is off-ratio (a 3:2 canvas that never got reframed, an115 edit that pinned to its input's ratio, a model miss) — **check the raw116 canvas dimensions before cropping**, and re-ratio first if it isn't 2:1.1173. **Verify glyphs *and* layout on read-back** (see `imagine` / `game-asset-core`118 for the blind-describe loop). On a garble or layout miss, **regenerate with a119 corrected prompt** — never try to move a logo with `image_edit` (frame120 translation / seams). After two failed attempts, ship the card **artwork-only**121 (titleless).122123 **Intentional exception vs `imagine`'s "rebuild text with code" rule:** the124 share card is a single static PNG; there is no reliable in-sandbox path to125 composite crisp code-drawn lettering onto generative art for this asset, so126 a clean titleless card is the correct fallback after two glyph failures.1274. **Normalize to exactly 1200×630 JPEG** with the baked-in ffmpeg (cover-crop —128 from a 2:1 canvas this shaves ~2.5% off each side and leaves the vertical129 composition untouched). **JPEG, not PNG**: the card is130 photographic generative art, and a PNG of it lands at 1–2 MB — heavy131 enough that link scrapers (X card previews included) time out or skip the132 image, so the card silently fails to unfurl. JPEG at this quality is133 ~150–300 KB with no visible loss at unfurl size:134135 ```sh136 ffmpeg -y -i card-raw.jpg \137 -vf "scale=1200:630:force_original_aspect_ratio=increase,crop=1200:630" \138 -q:v 4 public/og.jpg139 ```1401415. **Point `og:image` at the app's own asset** — absolute URL, scrapers do not142 resolve relative paths — keeping the no-host guard:143144 ```tsx145 const ogImage = host ? `https://${host}/og.jpg` : undefined;146 ```147148 Keep `og:image:width` / `og:image:height` at `1200` / `630`.1496. **Verify before finishing** (Pillow is installed; `ffprobe` is **not**):150151 ```sh152 python3 -c "153 from PIL import Image; import os154 im = Image.open('public/og.jpg')155 kb = os.path.getsize('public/og.jpg') // 1024156 print(im.size, f'{kb} KB')"157 # expect: (1200, 630) and under 600 KB (keeps X and other scrapers158 # reliable; target <= 300 KB — if over, bump -q:v up a step and re-encode)159 ```160161 **Read back the final `public/og.jpg`, not the pre-crop raw** — the crop162 is where clipping happens. This is a **hard gate, not an impression**:163 if any title glyph touches a frame edge or is visibly cut, the card is164 **rejected** — do not ship it, whatever else is right about it. Fix the165 ratio (step 1) or regenerate with the width bound restated; a shipped166 decapitated title is worse than the placeholder. Then confirm the card167 reads like *this* app at thumbnail size — clear subject, correctly168 spelled title (if any), comfortable margins.169170If neither `generate_image`/`edit_image` nor the xAI Images API is available,171fall back to the `og.grok.me` card — never ship a missing or broken172`og:image` URL.173174Regenerate the card when the app's visual identity materially changes (theme175overhaul, new hero art) — and on rename: the title is baked into the pixels,176so update `APP_NAME` *and* regenerate the card with the new name. (A titleless177fallback card survives a rename without regeneration.)178179## Favicon: hand-author `public/favicon.svg`180181Every app gets one, and it works in live preview immediately (no host needed).182183- **Write the SVG by hand — never `image_gen`.** It must stay crisp at 16px:184 one bold glyph or shape, flat fills from the app's design tokens, a square185 `viewBox`, a handful of elements at most. For whimsical apps an emoji-text186 SVG is a fine quick win:187188 ```svg189 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">190 <rect width="100" height="100" rx="20" fill="#1a1b26"/>191 <text x="50" y="50" font-size="62" text-anchor="middle"192 dominant-baseline="central">🥕</text>193 </svg>194 ```195196- Wire it in the root `head()`:197198 ```tsx199 links: [200 { rel: "icon", type: "image/svg+xml", href: "/favicon.svg" },201 { rel: "stylesheet", href: appCss },202 ],203 ```204205- Verify it renders non-blank and legible small (browser tab in a preview206 screenshot, or read back a rasterized copy). Update it when the app's theme207 or name changes meaning — it is part of the app's identity, not a set-and-forget.208209## PWA icons: only for installable apps210211When the app ships a web manifest (the user asked for installable / PWA /212home-screen behavior — do **not** invent a manifest just to have icons), add213raster icons derived from the favicon artwork so the identity stays214consistent:215216- `public/icon-192.png` and `public/icon-512.png` — the favicon's glyph on217 its tile, rasterized at size. Playwright (baked into the sandbox) can218 screenshot the served `/favicon.svg` at a 192/512 viewport; or redraw the219 same mark as a flat PNG. Keep it bold and flat — no photographic detail.220- A maskable variant (`"purpose": "maskable"`) needs the glyph inside the221 center ~80% safe zone so launcher shapes don't clip it.222- Wire the manifest `icons` array plus `theme_color` / `background_color`223 from the app's design tokens, and read the 192 back to confirm it stays224 legible.225226## Not supported227228No `/api/og` route, no runtime image renderer, no per-route cards, no runtime229`og:*` mutation. The card is one static site-wide image chosen at build time.230231If you add `robots.txt`, never blanket `Disallow: /` — crawlers must fetch `/`232to read the meta tags.