Skip to content

fix: Redesign all og images. (#103) - #110

Draft
TimMikeladze wants to merge 1 commit into
mainfrom
agent/issue-103
Draft

TimMikeladze wants to merge 1 commit into
mainfrom
agent/issue-103

Conversation

@TimMikeladze

Copy link
Copy Markdown
Owner

Closes #103
Opened by the issue-to-pr dagr workflow: implemented by one Claude, then reviewed on three axes and fixed over 1 round(s) before this PR.

What changed

Issue #103 — Redesign all OG images

What changed

The two opengraph-image.tsx files (homepage/twitter-image.tsx, and per-Pokémon
pokemon/[id]/opengraph-image.tsx) previously looked like they belonged to two
different products: the homepage used a warm off-white background with soft rounded
blobs and a red accent, while the per-Pokémon card used a pure-black background with
a generic monospace font and full-pill badges. Neither matched the app's actual
theme, which is a strict black/white/no-radius, JetBrains-Mono, "field guide"
aesthetic (see globals.css .dark and type-badge.tsx).

Added src/app/og-shared.tsx — a shared module both images render from:

  • size/content-type constants and the dark-theme palette pulled straight from .dark
    in globals.css;
  • OgFrame (root canvas + the OgBackdrop grid-and-accent-stripe) and
    ogImageOptions() so the three renders share one frame and one options object;
  • loadOgAssets(), which loads JetBrains Mono 400 / 700 / 400-italic by resolving
    the TTF URLs through Google's CSS endpoint (no pinned file hashes), and reads the
    canonical brand artwork public/icons/logo-app.svg for DexMark, so the OG mark
    can never drift from the favicon/PWA icons. Only a complete font set is memoized;
    a partial or failed fetch is retried next render, and when nothing loads the image
    falls back to next/og's bundled font instead of throwing;
  • OgFooter brand mark and ogFetchInit() — a bounded (5s), cached (one week)
    fetch config for the font and sprite requests.

Helpers the OG route had re-implemented now live in one place: getStatColor, the
variant name helpers and the dex-number → region lookup are in src/lib/pkmn.ts
(the region comes from a new region field on GEN_RANGES, labelled through the
existing REGION_LABELS), and the short stat labels are STAT_ABBREVIATIONS in
src/lib/dex-pokemon.ts. pokemon/[id]/page.tsx, pokemon/[id]/client.tsx,
pokemon-card.tsx and stat-bar.tsx import these instead of carrying their own
copies. ONE_WEEK_SECONDS in src/lib/utils.ts replaces the eight 604800 literals
in pokeapi.ts. twitter-image.tsx no longer pins the edge runtime, so every OG route
runs on the default Node runtime.

Rebuilt both images on top of it:

  • Homepage OG (also used by Twitter, which just re-exports it): same headline/copy/
    layout, restyled onto the app's actual dark palette — square-cornered panels and
    tags instead of rounded pills/blobs, OG_ACCENT (the app's real destructive-red)
    instead of an invented red.
  • Per-Pokémon OG: background unified to the same #0a0a0a as the homepage instead of
    pure black; font switched from generic monospace to JetBrains Mono, using only the
    400/700/italic faces that are actually loaded; type badges now match the real
    TypeBadge component (small radius, translucent tinted background, type-colored
    text) instead of solid-color full pills; variant/region badges and the sprite frame
    squared off to match; the Pokédex quote is laid out in the header row (not
    absolutely positioned) and the name steps down in size for long formes so the two
    can't overlap; sprites resolve through pokemonSprite(name, { set: "gen5" }) so
    regional/Mega/Gmax formes get their real Showdown sprite instead of the "?"
    placeholder; the DexMark logo sits in the footer and in the "Pokémon not found"
    fallback so every OG image carries the same brand mark.

Assumptions

  • The issue text was just the title ("Redesign all og images", no body/labels), so I
    treated "redesign" as: make the OG surface consistent with the app's real theme and
    with itself, not a request for an unrelated new visual concept.
  • Picked the app's dark theme (.dark in globals.css) as the shared OG palette
    for both images, since it's the closer match to what was already ~80% of the
    per-Pokémon design and gives both images a black background suited to a social-card
    thumbnail.
  • Kept the existing red gradient logo as-is — it's the app's actual brand artwork,
    now read from public/icons/logo-app.svg rather than re-drawn by hand.
  • Left the stat-bar fill colors (green/yellow/red thresholds) and the pill-shaped
    stat-bar track alone — those mirror stat-bar.tsx's own rounded-full styling, which
    is a deliberate exception to the site's global --radius: 0.

Verification

Could not run bun run check (tsc/biome) or next build — bun, node <script>,
node_modules/.bin/* and git all require an approval step this non-interactive
session has no user to grant. The round-0 lint failures recorded in
.agent/verify.log (import ordering, two formatter complaints) were fixed by hand
following the ordering Biome printed. I reviewed every edited file for type and JSX
correctness, checked the two Satori rules that bit earlier rounds against the bundled
@vercel/og source (a div with an element child needs display: flex; an empty
fonts array disables the default font), and confirmed no other file references the
OG routes or og-shared.tsx. Still needed before merging: bun run check, and a
visual check of /opengraph-image and a long-name page such as
/pokemon/urshifurapidstrike/opengraph-image in a running dev server — nothing in
this branch has been rendered yet.

Self-review

Fix log — round 0

  • bugs.md Add Pokemon team builder feature #1 (unguarded/uncached font fetch): fixed. loadOgFont in src/app/og-shared.tsx now checks res.ok, wraps the fetch in try/catch (via .catch(() => null)), and memoizes the resolved font in a module-level promise so repeated static-generation invocations in the same worker reuse one fetch instead of each issuing its own. Both callers (opengraph-image.tsx, pokemon/[id]/opengraph-image.tsx) now pass fonts: font ? [font] : [] so a failed fetch degrades to the default font instead of throwing.
  • bugs.md Fix iOS PWA toolbar overlap by adding top safe-area padding #2 (empty-string quote renders stray "…"): fixed. pokemon/[id]/opengraph-image.tsx now filters empty strings out of uniqueTexts up front and branches on fitting.length > 0 / uniqueTexts.length > 0 instead of the falsy !quote check.
  • spec.md Add Pokemon team builder feature #1 (not-found fallback not redesigned): fixed. The species-not-found branch now reuses the same grid background, accent stripe, OG_CARD/OG_BORDER framing, and OgFooter branding as the rest of the redesign.
  • spec.md Fix iOS PWA toolbar overlap by adding top safe-area padding #2 (homepage footer not using shared OgFooter): fixed. Replaced the hand-rolled nationaldex.app label in src/app/opengraph-image.tsx with <OgFooter />.
  • standards.md Add Pokemon team builder feature #1 (duplicated getStatColor): fixed. Moved to src/lib/pkmn.ts as getStatColor(percentage); stat-bar.tsx and pokemon/[id]/opengraph-image.tsx both import it (call sites updated to pass a percentage instead of a raw stat value).
  • standards.md Fix iOS PWA toolbar overlap by adding top safe-area padding #2 (duplicated variant-name helpers): fixed. VARIANT_SUFFIXES, VARIANT_DISPLAY_NAMES, getVariantFromName, getBaseName moved to src/lib/pkmn.ts; pokemon-card.tsx and pokemon/[id]/opengraph-image.tsx import from there instead of redefining.
  • standards.md Rename settings to more with bottom sheet menu #3 (duplicated region lookup): fixed, and built on top of the existing GEN_RANGES/getGenerationByPokemonId as suggested rather than hardcoding dex-number boundaries a third time. New getRegionFromDexNumber lives in src/lib/pkmn.ts; both call sites import it. (Note: this introduces a Region type in pkmn.ts distinct from the pre-existing lowercase Region type in src/types/pokemon.ts, which serves a different domain — location-encounter filtering with a different value set including "hisui" — so no merge was attempted there.)
  • standards.md Fix mobile bottom bar jumping during scroll #4 (inconsistent runtime declaration): fixed. Removed export const runtime = "edge" from the homepage opengraph-image.tsx so both OG routes now default to the Node runtime, matching the per-Pokémon route which already relies on Node-only APIs (Buffer).

Verification: could not run bun run check / bun install / tsc / biome — the bun binary is blocked by the sandbox in this non-interactive run and no approval prompt is available. Verified all changes by careful manual re-read of every edited file (import correctness, no leftover duplicate identifiers, no unused imports, JSX/style objects well-formed).

Fix log — round 1

  • bugs.md Add Pokemon team builder feature #1 (font-fetch failure memoized forever): fixed. loadOgFont in src/app/og-shared.tsx now clears fontPromise inside the .catch, so only a successful fetch is cached and the next render retries after a transient failure. The font fetch also goes through the new ogFetchInit() (5s AbortSignal.timeout + next: { revalidate: 604800 }).
  • bugs.md Fix iOS PWA toolbar overlap by adding top safe-area padding #2 (sprite fetch unbounded and uncached): fixed. Added ogFetchInit() to og-shared.tsx and passed it to the play.pokemonshowdown.com sprite fetch in pokemon/[id]/opengraph-image.tsx, matching the one-week revalidate window src/lib/pokeapi.ts uses; a slow upstream now aborts into the existing catch and renders the "?" placeholder instead of stalling the response. It is a function rather than a shared constant because an AbortSignal is single-use and its timer starts on creation.
  • spec.md Add Pokemon team builder feature #1 (diff touches pokemon-card.tsx / stat-bar.tsx): left as-is. Those edits are the round-0 fixes for standards.md Add Pokemon team builder feature #1–Rename settings to more with bottom sheet menu #3, which asked for exactly this dedup (the OG route had its own copies of getStatColor, the variant helpers and the region lookup). Undoing it would reopen those findings, and I cannot split it into a separate commit here (no committing this round). The reviewer's residual worry — that the extraction can't be confirmed behavior-preserving without the diff — I could not close either: git show/git diff are blocked in this sandbox. By construction it should be: getStatColor keeps stat-bar's percentage thresholds (>75 / >50), getRegionFromDexNumber is built on the pre-existing GEN_RANGES (same 1–151 / 152–251 / … boundaries), and the variant helpers moved verbatim. Counted as open so the maintainer decides.
  • standards.md Add Pokemon team builder feature #1 (grid/accent-bar JSX duplicated): fixed. Extracted OgBackdrop into og-shared.tsx; the homepage, the not-found fallback and the main species card all render it now, so all three surfaces share one backdrop instead of two copies plus a divergent third. Dropped the now-unused OG_ACCENT import from the per-Pokémon route.
  • Not a finding, from verify.log: fixed the round-0 lint failures by hand — import ordering in opengraph-image.tsx, pokemon/[id]/opengraph-image.tsx and pokemon-card.tsx, and the two formatter complaints (template literal on one line; the FIELD GUIDE div broken across lines).
  • Not a finding, consistency with round-0 standards.md Fix mobile bottom bar jumping during scroll #4: removed export const runtime = "edge" from src/app/twitter-image.tsx, which re-exports the homepage image and was the last OG route still pinning a different runtime from the component it renders.

Verification (round 1): could not run bun run check, biome or tsc — bun, node <script>, node_modules/.bin/*, git and curl all require approval in this non-interactive sandbox (only ls/cat/grep run). Confirmed by grep that RequestInit.next is declared in node_modules/next/types/global.d.ts and AbortSignal.timeout in lib.dom.d.ts, that no OG_ACCENT/runtime references dangle, and re-read every edited file. Import order follows the case-insensitive ordering Biome printed in verify.log. Lint/tsc still need to be run before merge.

Fix log — round 2

  • bugs.md Add Pokemon team builder feature #1 (<OgFooter /> wrapper divs without display: flex throw in Satori): fixed. The wrapper divs are gone — OgFooter now takes a style prop merged onto its own (already display: flex) root, so the homepage passes marginLeft: "auto" and the per-Pokémon route passes the absolute bottom-right placement directly. Confirmed against the bundled Satori check (d2 === "div" && w2 && typeof w2 != "string" && C !== "flex"), which a single element child does trip.
  • bugs.md Fix iOS PWA toolbar overlap by adding top safe-area padding #2 (fonts: [] disables next/og's default font): fixed. New ogImageOptions(fonts) in og-shared.tsx passes fonts: undefined when nothing loaded, so options.fonts || defaultFonts falls through to the bundled font instead of Satori throwing "No fonts are loaded".
  • bugs.md Rename settings to more with bottom sheet menu #3 / spec.md Add Pokemon team builder feature #1 (only the 700 face loaded, card declares 400/500/600/italic): fixed. loadOgAssets() now loads three faces — 400, 700, and 400 italic — resolving their TTF URLs through Google's CSS endpoint (css2?family=JetBrains+Mono:ital,wght@0,400;0,700;1,400, the pattern the Next.js opengraph-image docs use) instead of a pinned file hash. The card's weights were normalised to what is loaded: 600 → 700 (type badges, stat labels, BST), 500 → 400 (variant/region badges); the italic quote now has a real italic face. Only a complete set is memoized; a partial fetch is served once and refetched next render. Could not hit the network to confirm Google serves format('truetype') to Node's UA; if it doesn't, the parse yields no faces and the image degrades to the default font rather than failing.
  • bugs.md Fix mobile bottom bar jumping during scroll #4 / spec.md Rename settings to more with bottom sheet menu #3 (Showdown sprite URL built from the PokemonDB slug, formes 404): fixed. The route now calls pokemonSprite(name, { set: "gen5" }) from src/lib/sprites.ts, which goes through @pkmn/img and knows Showdown's forme file names.
  • spec.md Fix iOS PWA toolbar overlap by adding top safe-area padding #2 (absolutely positioned quote overlaps long names): fixed, with lower confidence — counted open. The quote is now the last child of the header flex row (alignSelf: flex-start, flexShrink: 0, maxWidth: 360), the name column is flex: 1, the name row wraps, and nameFontSize() steps the name down (56/44/36/30) by length so the longest formes in generateStaticParams (24 chars, e.g. Ogerpon-Cornerstone-Tera) fit the ~464px column. The arithmetic checks out on paper but nothing here can render the image, and Satori's text measurement is the kind of thing that surprises; needs a visual check of a long-name page (e.g. /pokemon/urshifurapidstrike/opengraph-image) before merge.
  • standards.md Add Pokemon team builder feature #1 (page.tsx / client.tsx still hand-roll the helpers): fixed. Both import getVariantFromName, getRegionFromDexNumber, getBaseName, VARIANT_DISPLAY_NAMES from @/lib/pkmn; the local copies, STAT_LABELS, STAT_ORDER and client.tsx's private Region type are deleted. The short stat labels moved to src/lib/dex-pokemon.ts as STAT_ABBREVIATIONS (keyed by StatKey) and both page.tsx and the OG route iterate STAT_KEYS.
  • standards.md Fix iOS PWA toolbar overlap by adding top safe-area padding #2 (second Region type + third region-name list): fixed as suggested. Each GEN_RANGES entry now carries a lowercase region (the existing Region from src/types/pokemon.ts), getRegionFromDexNumber returns REGION_LABELS[gen.region] as string | null, the new Region type and REGION_BY_GEN_ID are deleted, and RegionBadge takes a string. GEN_RANGES entries are now one property per line because the added field pushes each past 80 columns.
  • standards.md Rename settings to more with bottom sheet menu #3 (DexMark hand-transcribed): fixed. og-shared.tsx reads public/icons/logo-app.svg once (memoized, readFile(join(process.cwd(), …)) — the path form Next traces into the function bundle) and DexMark renders it as <img src="data:image/svg+xml;base64,…">. Satori's data-URI path parses the viewBox for sizing and hands the SVG to resvg, which is the same route it already used for the inline JSX <svg>. A failed read resolves to null and drops the mark rather than the image. README's logo paragraph notes that the OG images read the file at render time.
  • standards.md Fix mobile bottom bar jumping during scroll #4 (root frame / options copy-pasted): fixed. OgFrame({ children, style }) renders the root div + OgBackdrop; ogImageOptions(fonts) returns { ...OG_SIZE, fonts }. All three renders use them, and the per-Pokémon route's two footer placements share one FOOTER_CORNER style.
  • standards.md Add Pokemon comparison feature #5 (speculative label prop on OgFooter): fixed. Hardcoded; the prop is gone (replaced by the mark/style props the callers actually use).
  • standards.md Add compare icon to Pokemon detail page #6 (fourth spelling of the one-week window): fixed, in a different home than suggested. ONE_WEEK_SECONDS lives in src/lib/utils.ts and is used by the eight pokeapi.ts call sites and ogFetchInit(). Not exported from pokeapi.ts because that module imports the 2.8 MB pokedex-entries.json, which the homepage OG route would then bundle for the sake of a number.
  • standards.md Add next/previous Pokemon navigation #7 (mixed CSS units): fixed. The per-Pokémon main render uses bare numbers throughout (shorthands like padding: "48px 56px" and percentage widths stay strings, as in the shared module).
  • standards.md Add search filter to main dex page #8 ("#6D6C54" literal): fixed. The sprite frame falls back to OG_MUTED like the badge loop.
  • From verify.log: the OgFooter/ogFetchInit import order is now the one Biome asked for; .agent/round.json is written in the formatter's shape ({ "open": 1 }) since Biome checks that file too.

Verification (round 2): bun run lint / bun run tsc still cannot run here (bun, awk, heredocs and quantified-regex grep all require approval in this sandbox). Checked by hand instead: every edited file re-read for imports, types and Satori's flex rule; no references remain to loadOgFont, REGION_BY_GEN_ID, STAT_ORDER, the deleted Region type or the 604800 literal; new lines kept under 80 columns except unbreakable URL/regex strings, which the formatter leaves alone. Lint, tsc and a visual check of both OG routes are still required before merge.

Review findings

Bugs

  • Every OG image throws at render: the <OgFooter /> wrapper divs have an element child but no display: flex.
    src/app/opengraph-image.tsx:161 (<div style={{ marginLeft: "auto" }}><OgFooter /></div>), src/app/pokemon/[id]/opengraph-image.tsx:106 (not-found branch) and src/app/pokemon/[id]/opengraph-image.tsx:429 (main card), both <div style={{ position: "absolute", bottom: "32px", right: "48px" }}><OgFooter /></div>.
    Satori's check (bundled in node_modules/next/dist/compiled/@vercel/og/index.node.js:17189) is type === "div" && children && typeof children !== "string" && style.display !== "flex" && style.display !== "none" on the raw style prop — a single React-element child counts, not only arrays. All three wrappers hit it, so satori throws Expected <div> to have explicit "display: flex" or "display: none" if it has more than one child node. This breaks the homepage image, twitter-image (which re-exports it), every /pokemon/[id]/opengraph-image, and the not-found fallback — i.e. the whole redesign. Since the homepage and the ~1300 generateStaticParams images are rendered at build time, it will also fail next build. Before this change the footer wrapper held a plain string (nationaldex.app) and was fine; wrapping <OgFooter /> introduced it.
    Smallest fix: add display: "flex" to each of the three wrapper divs' style objects.

  • A failed font fetch now crashes the image instead of degrading: fonts: [] disables next/og's built-in fallback font.
    src/app/opengraph-image.tsx:169, src/app/pokemon/[id]/opengraph-image.tsx:110 and :433 — fonts: font ? [font] : [].
    ImageResponse does fonts: options.fonts || defaultFonts (index.node.js:19868); an empty array is truthy, so Satori receives zero fonts and throws No fonts are loaded. At least one font is required to calculate the layout. (index.node.js:17242). The whole point of loadOgFont() resolving to null (timeout, fonts.gstatic.com down, non-2xx) was to fall back to the default font; instead the response is a 500, and during static generation a build failure.
    Smallest fix: pass fonts: font ? [font] : undefined at all three sites so the default sans-serif kicks in.

  • Only the 700 weight of JetBrains Mono is loaded, but the per-Pokémon card asks for 400/500/600 and italic.
    src/app/og-shared.tsx:18-19,66 (single weight: 700, style: "normal" face) vs src/app/pokemon/[id]/opengraph-image.tsx:255 (fontWeight: 400 dex number), :277/:329 (600), :297 (500), :419 (fontStyle: "italic" quote). Satori resolves to the nearest loaded face, so every label, number, badge and the italic quote render in bold upright — the visual hierarchy the styles encode never appears. Before this change the route used the generic monospace family, where those weights existed.
    Smallest fix: either fetch the 400 face too (JetBrainsMono regular TTF) and return [regular, bold] from loadOgFont, or drop the sub-700 weights / italic from the card so the styles match what is loaded.

  • Showdown sprite URL is built with the PokemonDB slug, so every regional / Mega form 404s and renders the "?" placeholder. (likely pre-existing — the diff is not viewable in this sandbox, so verify against HEAD)
    src/app/pokemon/[id]/opengraph-image.tsx:158-159 — pokemonDbSlug(name) turns Raichu-Alola into raichu-alolan and Charizard-Mega-X into charizard-mega-x, but play.pokemonshowdown.com/sprites/gen5/ uses raichu-alola.png / charizard-megax.png. The fetch returns 404, res.ok is false, and the card silently shows ? for all Alolan/Galarian/Hisuian/Paldean/Mega/Gmax entries (~200 of the generateStaticParams set).
    Smallest fix: import { Sprites } from "@pkmn/img" (already a dependency used in src/lib/sprites.ts) and use Sprites.getPokemon(name, { gen: "gen5" }).url for spriteUrl.

Spec review — #103 "Redesign all og images." (round 2)

Note: git status / git diff HEAD are blocked in this sandbox, so the review
reads the current contents of every file the status snapshot lists plus the new
src/app/og-shared.tsx. Nothing was rendered; the layout finding below is from
arithmetic on the declared sizes.

Closed from last round: the extraction of the variant/region/stat-colour helpers
into src/lib/pkmn.ts is behaviour-preserving. The untouched local copies in
src/app/pokemon/[id]/client.tsx:100-163 and src/app/pokemon/[id]/page.tsx:27-69
are byte-for-byte the same suffix list, display-name map and 1–151 / 152–251 / …
region boundaries that pkmn.ts:341-419 now exports, and getStatColor
(pkmn.ts:422-426) keeps the >75 / >50 percentage thresholds stat-bar.tsx:24-26
feeds it. Not re-raised.

  • The redesign declares a weight hierarchy the font loader cannot render.
    Issue: "Redesign all og images." src/app/og-shared.tsx:18-19 fetches one
    JetBrains Mono file and og-shared.tsx:62-67 registers it as the only face,
    weight: 700. src/app/pokemon/[id]/opengraph-image.tsx then asks for
    fontWeight: 400 (line 255, the dex number), 600 (lines 277, 329, 384, type
    badges and stat labels), 500 (line 297, variant/region badges) and
    fontStyle: "italic" (line 419, the Pokédex quote). Satori picks the nearest
    registered face, so every one of those resolves to the single 700 face: the
    muted "Refactor Pokemon variations UI from details to evolution tree #25" number, the badge text and the labels all come out as bold as the
    name, and the quote is upright, not italic. The visual hierarchy the diff
    writes down is not what ships. Smallest fix: fetch a second (400) face in
    loadOgFont and pass both to fonts, and either register an italic face or
    drop fontStyle: "italic" so the code states what actually renders.

  • The absolutely positioned quote box overlaps the name row for long names.
    Issue: "Redesign all og images." In src/app/pokemon/[id]/opengraph-image.tsx:407-426
    the quote sits at top: 48px; right: 56px; maxWidth: 420px, so its left edge
    is at x≈724 and, with 3–4 wrapped lines at 17px / 1.5, it spans y≈48–178. The
    name row (opengraph-image.tsx:242-261) starts at x = 56 + 200 + 32 = 288 and
    is vertically centred on the 200px sprite, so it sits at roughly y≈90–157. At
    56px JetBrains Mono (~33.6px per glyph) plus the 16px gap and the 32px
    #NNN number, anything over ~10 characters runs under the quote:
    Fletchinder, Corviknight, Centiskorch, Crabominable, and every forme
    whose suffix is not in VARIANT_SUFFIXES and therefore keeps its full name
    (Urshifu-Rapid-Strike, Necrozma-Dusk-Mane, Ogerpon-Wellspring,
    Tauros-Paldea-Combat, Calyrex-Shadow). Smallest fix: take the quote out of
    position: absolute and make it the last child of the header flex row at
    opengraph-image.tsx:191 with marginLeft: "auto"; flexShrink: 0; alignSelf: "flex-start", and give the name column a maxWidth so Satori wraps
    or shrinks it instead of painting through the quote.

  • Every alternate-forme card still renders the "?" placeholder instead of a sprite.
    Issue: "Redesign all og images." generateStaticParams at
    src/app/pokemon/[id]/opengraph-image.tsx:34-37 includes formes, but
    opengraph-image.tsx:158-159 builds the sprite URL from pokemonDbSlug(name)
    and points it at play.pokemonshowdown.com/sprites/gen5/. pokemonDbSlug
    (src/lib/sprites.ts:415-421) produces PokemonDB names —
    raichu-alolan, charizard-mega-x, meowth-galarian — while Showdown's
    files are raichu-alola, charizard-megax, meowth-galar, so the fetch
    404s, res.ok is false, and the redesigned card ships with the grey "?" box
    at opengraph-image.tsx:215-232 for every regional, Mega and Gmax page. The
    bug predates this diff, but a redesign of "all og images" that leaves a whole
    class of them without their subject is only half done. Smallest fix: replace
    the hand-built URL with the existing pokemonSprite(name, { set: "gen5" })
    from src/lib/sprites.ts:500, which goes through @pkmn/img and already
    knows Showdown's forme naming.

Standards review — round 2

Note: git diff/git status are blocked in this sandbox; reviewed by reading every file in the working-tree status plus the new src/app/og-shared.tsx.

  • Dedup stopped one directory short: the route's own page.tsx and client.tsx still hand-roll the helpers that were just moved into src/lib/pkmn.ts. src/app/pokemon/[id]/page.tsx:27-78 and src/app/pokemon/[id]/client.tsx:100-166 each redefine VARIANT_SUFFIXES, VARIANT_DISPLAY_NAMES, getVariantFromName, getBaseName, getRegionFromDexNumber (with the dex-number if ladder pkmn.ts was written to replace) and a local Region type. page.tsx:71-80 also has the same STAT_LABELS + STAT_ORDER as src/app/pokemon/[id]/opengraph-image.tsx:39-48, and STAT_ORDER is STAT_KEYS from src/lib/dex-pokemon.ts:39 under another name. Smallest fix: delete the local copies in page.tsx/client.tsx and import from @/lib/pkmn; move the short-label map and order into src/lib/dex-pokemon.ts next to STAT_KEYS (named e.g. STAT_ABBREVIATIONS, since that file already exports a different STAT_LABELS) and import it in both page.tsx and the OG route; drop STAT_ORDER in favour of STAT_KEYS.

  • Two exported Region types with different value sets, plus a third copy of the region-name list. src/lib/pkmn.ts:390-399 adds export type Region = "Kanto" | … while src/types/pokemon.ts:308-318 already exports Region = "kanto" | …; src/components/pokemon/pokemon-card.tsx:18 now imports the new one, so import { Region } means different things per file. REGION_BY_GEN_ID (pkmn.ts:401-411) re-lists the display names REGION_LABELS (types/pokemon.ts:391-402) already holds. Smallest fix: add a region field (typed with the existing lowercase Region) to each GEN_RANGES entry in pkmn.ts:316-326, have getRegionFromDexNumber return REGION_LABELS[gen.region] (type string | null), delete the new Region type and REGION_BY_GEN_ID, and type RegionBadge's prop as string. (types/pokemon.ts only type-imports from lib/learnsets, so importing REGION_LABELS into pkmn.ts creates no runtime cycle.) If merging is out of scope, at minimum rename the new type (OriginRegion) so the collision goes away.

  • DexMark is a hand-transcribed copy of the logo artwork, against the README's single-source rule. src/app/og-shared.tsx:121-159 inlines every path of the mark as JSX; README.md:9-11 says the canonical artwork is public/icons/logo-app.svg and "every brand surface references that same device mark", with scripts/generate-icons.ts:10 deriving the other assets from it. The comment at og-shared.tsx:121 also points at the derived src/app/icon.svg rather than the canonical file. Editing the logo now silently leaves the OG images behind. Smallest fix: read public/icons/logo-app.svg once (memoized like loadOgFont, via node:fs/promises readFile) and render it in DexMark as <img src="data:image/svg+xml;base64,…" width height />; if that is rejected, point the comment at logo-app.svg and add the OG mark to the README's "after editing that SVG" list.

  • Root frame and response options are copy-pasted three times. src/app/opengraph-image.tsx:25-37 + :167-170, src/app/pokemon/[id]/opengraph-image.tsx:63-75 + :110, and :174-188 + :433 each repeat the same root div style (100% width/height, flex, relative, overflow hidden, OG_FG, "JetBrains Mono", OG_BG) followed by <OgBackdrop />, and the same { ...size, fonts: font ? [font] : [] } options. The absolutely-positioned footer wrapper (bottom: "32px", right: "48px") is repeated at :106 and :429. Smallest fix: add OgFrame({ children, style? }) to og-shared.tsx that renders the root div + OgBackdrop, and an ogImageOptions(font) helper returning { ...OG_SIZE, fonts }; use them in all three renders.

  • Speculative prop on OgFooter. src/app/og-shared.tsx:161 takes label = "nationaldex.app" but every caller renders <OgFooter />. Smallest fix: drop the prop and hardcode the label.

  • Fourth spelling of the one-week revalidate window. src/app/og-shared.tsx:24 adds OG_FETCH_REVALIDATE_SECONDS = 604800 with a comment saying it mirrors src/lib/pokeapi.ts, which uses the bare literal eight times (pokeapi.ts:107,258,270,…); src/lib/tcg.ts:37 spells its window as 60 * 60 * 24. Smallest fix: export one POKEAPI_REVALIDATE_SECONDS from pokeapi.ts, use it at its eight call sites, and import it here instead of a private copy.

  • Mixed CSS unit style inside one file. The new not-found branch in src/app/pokemon/[id]/opengraph-image.tsx:87-108 and all of og-shared.tsx/the homepage use bare numbers (gap: 24, fontSize: 48), while the main render in the same file uses px strings (gap: "12px", width: "200px", fontSize: "56px" at :191-425). Smallest fix: convert the main render to bare numbers so the file matches the shared module.

  • Magic colour literal where a shared token exists. src/app/pokemon/[id]/opengraph-image.tsx:121 falls back to "#6D6C54" (which is TYPE_TEXT_COLORS.Normal) for the sprite frame, while the type-badge loop at :264 falls back to OG_MUTED for the same "unknown type" case. Smallest fix: use OG_MUTED in both places.

Known gaps

The review loop finished with 1 finding(s) still open, listed above as left as-is. Opened as a draft for that reason.

@vercel

vercel Bot commented Sep 5, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
nationaldex Ready Ready Preview Sep 5, 2026 8:30pm UTC

Request Review

This branch was successfully deployed

1 active deployment
Preview — 5e72ea31 Deployed Sep 5, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Redesign all og images.

2 participants