Repository navigation
fix: Redesign all og images. (#103) - #110
Draft
TimMikeladze wants to merge 1 commit into
Draft
TimMikeladze wants to merge 1 commit into
TimMikeladze wants to merge 1 commit into
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #103
Opened by the
issue-to-prdagr 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.tsxfiles (homepage/twitter-image.tsx, and per-Pokémonpokemon/[id]/opengraph-image.tsx) previously looked like they belonged to twodifferent 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
monospacefont and full-pill badges. Neither matched the app's actualtheme, which is a strict black/white/no-radius, JetBrains-Mono, "field guide"
aesthetic (see
globals.css.darkandtype-badge.tsx).Added
src/app/og-shared.tsx— a shared module both images render from:.darkin
globals.css;OgFrame(root canvas + theOgBackdropgrid-and-accent-stripe) andogImageOptions()so the three renders share one frame and one options object;loadOgAssets(), which loads JetBrains Mono 400 / 700 / 400-italic by resolvingthe TTF URLs through Google's CSS endpoint (no pinned file hashes), and reads the
canonical brand artwork
public/icons/logo-app.svgforDexMark, so the OG markcan 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;
OgFooterbrand mark andogFetchInit()— a bounded (5s), cached (one week)fetchconfig for the font and sprite requests.Helpers the OG route had re-implemented now live in one place:
getStatColor, thevariant name helpers and the dex-number → region lookup are in
src/lib/pkmn.ts(the region comes from a new
regionfield onGEN_RANGES, labelled through theexisting
REGION_LABELS), and the short stat labels areSTAT_ABBREVIATIONSinsrc/lib/dex-pokemon.ts.pokemon/[id]/page.tsx,pokemon/[id]/client.tsx,pokemon-card.tsxandstat-bar.tsximport these instead of carrying their owncopies.
ONE_WEEK_SECONDSinsrc/lib/utils.tsreplaces the eight604800literalsin
pokeapi.ts.twitter-image.tsxno longer pins the edge runtime, so every OG routeruns on the default Node runtime.
Rebuilt both images on top of it:
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.
#0a0a0aas the homepage instead ofpure black; font switched from generic
monospaceto JetBrains Mono, using only the400/700/italic faces that are actually loaded; type badges now match the real
TypeBadgecomponent (small radius, translucent tinted background, type-coloredtext) 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" })soregional/Mega/Gmax formes get their real Showdown sprite instead of the "?"
placeholder; the
DexMarklogo sits in the footer and in the "Pokémon not found"fallback so every OG image carries the same brand mark.
Assumptions
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.
.darkinglobals.css) as the shared OG palettefor 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.
now read from
public/icons/logo-app.svgrather than re-drawn by hand.stat-bar track alone — those mirror
stat-bar.tsx's own rounded-full styling, whichis a deliberate exception to the site's global
--radius: 0.Verification
Could not run
bun run check(tsc/biome) ornext build—bun,node <script>,node_modules/.bin/*andgitall require an approval step this non-interactivesession has no user to grant. The round-0 lint failures recorded in
.agent/verify.log(import ordering, two formatter complaints) were fixed by handfollowing 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/ogsource (adivwith an element child needsdisplay: flex; an emptyfontsarray disables the default font), and confirmed no other file references theOG routes or
og-shared.tsx. Still needed before merging:bun run check, and avisual check of
/opengraph-imageand a long-name page such as/pokemon/urshifurapidstrike/opengraph-imagein a running dev server — nothing inthis branch has been rendered yet.
Self-review
Fix log — round 0
loadOgFontinsrc/app/og-shared.tsxnow checksres.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 passfonts: font ? [font] : []so a failed fetch degrades to the default font instead of throwing.pokemon/[id]/opengraph-image.tsxnow filters empty strings out ofuniqueTextsup front and branches onfitting.length > 0/uniqueTexts.length > 0instead of the falsy!quotecheck.OG_CARD/OG_BORDERframing, andOgFooterbranding as the rest of the redesign.OgFooter): fixed. Replaced the hand-rollednationaldex.applabel insrc/app/opengraph-image.tsxwith<OgFooter />.getStatColor): fixed. Moved tosrc/lib/pkmn.tsasgetStatColor(percentage);stat-bar.tsxandpokemon/[id]/opengraph-image.tsxboth import it (call sites updated to pass a percentage instead of a raw stat value).VARIANT_SUFFIXES,VARIANT_DISPLAY_NAMES,getVariantFromName,getBaseNamemoved tosrc/lib/pkmn.ts;pokemon-card.tsxandpokemon/[id]/opengraph-image.tsximport from there instead of redefining.GEN_RANGES/getGenerationByPokemonIdas suggested rather than hardcoding dex-number boundaries a third time. NewgetRegionFromDexNumberlives insrc/lib/pkmn.ts; both call sites import it. (Note: this introduces aRegiontype inpkmn.tsdistinct from the pre-existing lowercaseRegiontype insrc/types/pokemon.ts, which serves a different domain — location-encounter filtering with a different value set including "hisui" — so no merge was attempted there.)export const runtime = "edge"from the homepageopengraph-image.tsxso 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— thebunbinary 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
loadOgFontinsrc/app/og-shared.tsxnow clearsfontPromiseinside 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 newogFetchInit()(5sAbortSignal.timeout+next: { revalidate: 604800 }).ogFetchInit()toog-shared.tsxand passed it to theplay.pokemonshowdown.comsprite fetch inpokemon/[id]/opengraph-image.tsx, matching the one-week revalidate windowsrc/lib/pokeapi.tsuses; a slow upstream now aborts into the existingcatchand renders the "?" placeholder instead of stalling the response. It is a function rather than a shared constant because anAbortSignalis single-use and its timer starts on creation.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 ofgetStatColor, 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 diffare blocked in this sandbox. By construction it should be:getStatColorkeeps stat-bar's percentage thresholds (>75 / >50),getRegionFromDexNumberis built on the pre-existingGEN_RANGES(same 1–151 / 152–251 / … boundaries), and the variant helpers moved verbatim. Counted as open so the maintainer decides.OgBackdropintoog-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-unusedOG_ACCENTimport from the per-Pokémon route.verify.log: fixed the round-0 lint failures by hand — import ordering inopengraph-image.tsx,pokemon/[id]/opengraph-image.tsxandpokemon-card.tsx, and the two formatter complaints (template literal on one line; theFIELD GUIDEdiv broken across lines).export const runtime = "edge"fromsrc/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,biomeortsc—bun,node <script>,node_modules/.bin/*,gitandcurlall require approval in this non-interactive sandbox (onlyls/cat/greprun). Confirmed by grep thatRequestInit.nextis declared innode_modules/next/types/global.d.tsandAbortSignal.timeoutinlib.dom.d.ts, that noOG_ACCENT/runtimereferences dangle, and re-read every edited file. Import order follows the case-insensitive ordering Biome printed inverify.log. Lint/tsc still need to be run before merge.Fix log — round 2
<OgFooter />wrapper divs withoutdisplay: flexthrow in Satori): fixed. The wrapper divs are gone —OgFooternow takes astyleprop merged onto its own (alreadydisplay: flex) root, so the homepage passesmarginLeft: "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.fonts: []disables next/og's default font): fixed. NewogImageOptions(fonts)inog-shared.tsxpassesfonts: undefinedwhen nothing loaded, sooptions.fonts || defaultFontsfalls through to the bundled font instead of Satori throwing "No fonts are loaded".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 servesformat('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.pokemonSprite(name, { set: "gen5" })fromsrc/lib/sprites.ts, which goes through@pkmn/imgand knows Showdown's forme file names.alignSelf: flex-start,flexShrink: 0,maxWidth: 360), the name column isflex: 1, the name row wraps, andnameFontSize()steps the name down (56/44/36/30) by length so the longest formes ingenerateStaticParams(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.getVariantFromName,getRegionFromDexNumber,getBaseName,VARIANT_DISPLAY_NAMESfrom@/lib/pkmn; the local copies,STAT_LABELS,STAT_ORDERand client.tsx's privateRegiontype are deleted. The short stat labels moved tosrc/lib/dex-pokemon.tsasSTAT_ABBREVIATIONS(keyed byStatKey) and bothpage.tsxand the OG route iterateSTAT_KEYS.Regiontype + third region-name list): fixed as suggested. EachGEN_RANGESentry now carries a lowercaseregion(the existingRegionfromsrc/types/pokemon.ts),getRegionFromDexNumberreturnsREGION_LABELS[gen.region]asstring | null, the newRegiontype andREGION_BY_GEN_IDare deleted, andRegionBadgetakes astring.GEN_RANGESentries are now one property per line because the added field pushes each past 80 columns.DexMarkhand-transcribed): fixed.og-shared.tsxreadspublic/icons/logo-app.svgonce (memoized,readFile(join(process.cwd(), …))— the path form Next traces into the function bundle) andDexMarkrenders it as<img src="data:image/svg+xml;base64,…">. Satori's data-URI path parses theviewBoxfor sizing and hands the SVG to resvg, which is the same route it already used for the inline JSX<svg>. A failed read resolves tonulland drops the mark rather than the image. README's logo paragraph notes that the OG images read the file at render time.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 oneFOOTER_CORNERstyle.labelprop onOgFooter): fixed. Hardcoded; the prop is gone (replaced by themark/styleprops the callers actually use).ONE_WEEK_SECONDSlives insrc/lib/utils.tsand is used by the eightpokeapi.tscall sites andogFetchInit(). Not exported frompokeapi.tsbecause that module imports the 2.8 MBpokedex-entries.json, which the homepage OG route would then bundle for the sake of a number.padding: "48px 56px"and percentage widths stay strings, as in the shared module)."#6D6C54"literal): fixed. The sprite frame falls back toOG_MUTEDlike the badge loop.verify.log: theOgFooter/ogFetchInitimport order is now the one Biome asked for;.agent/round.jsonis written in the formatter's shape ({ "open": 1 }) since Biome checks that file too.Verification (round 2):
bun run lint/bun run tscstill cannot run here (bun,awk, heredocs and quantified-regexgrepall 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 toloadOgFont,REGION_BY_GEN_ID,STAT_ORDER, the deletedRegiontype or the604800literal; 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 />wrapperdivs have an element child but nodisplay: flex.src/app/opengraph-image.tsx:161(<div style={{ marginLeft: "auto" }}><OgFooter /></div>),src/app/pokemon/[id]/opengraph-image.tsx:106(not-found branch) andsrc/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) istype === "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, sosatorithrowsExpected <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 ~1300generateStaticParamsimages are rendered at build time, it will also failnext 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 wrapperdivs' 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:110and:433—fonts: font ? [font] : [].ImageResponsedoesfonts: options.fonts || defaultFonts(index.node.js:19868); an empty array is truthy, so Satori receives zero fonts and throwsNo fonts are loaded. At least one font is required to calculate the layout.(index.node.js:17242). The whole point ofloadOgFont()resolving tonull(timeout,fonts.gstatic.comdown, 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] : undefinedat 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(singleweight: 700, style: "normal"face) vssrc/app/pokemon/[id]/opengraph-image.tsx:255(fontWeight: 400dex 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 genericmonospacefamily, where those weights existed.Smallest fix: either fetch the 400 face too (
JetBrainsMonoregular TTF) and return[regular, bold]fromloadOgFont, 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)turnsRaichu-Alolaintoraichu-alolanandCharizard-Mega-Xintocharizard-mega-x, butplay.pokemonshowdown.com/sprites/gen5/usesraichu-alola.png/charizard-megax.png. The fetch returns 404,res.okis false, and the card silently shows?for all Alolan/Galarian/Hisuian/Paldean/Mega/Gmax entries (~200 of thegenerateStaticParamsset).Smallest fix:
import { Sprites } from "@pkmn/img"(already a dependency used insrc/lib/sprites.ts) and useSprites.getPokemon(name, { gen: "gen5" }).urlforspriteUrl.Spec review — #103 "Redesign all og images." (round 2)
Note:
git status/git diff HEADare blocked in this sandbox, so the reviewreads 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 fromarithmetic on the declared sizes.
Closed from last round: the extraction of the variant/region/stat-colour helpers
into
src/lib/pkmn.tsis behaviour-preserving. The untouched local copies insrc/app/pokemon/[id]/client.tsx:100-163andsrc/app/pokemon/[id]/page.tsx:27-69are byte-for-byte the same suffix list, display-name map and 1–151 / 152–251 / …
region boundaries that
pkmn.ts:341-419now exports, andgetStatColor(
pkmn.ts:422-426) keeps the >75 / >50 percentage thresholdsstat-bar.tsx:24-26feeds 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-19fetches oneJetBrains Mono file and
og-shared.tsx:62-67registers it as the only face,weight: 700.src/app/pokemon/[id]/opengraph-image.tsxthen asks forfontWeight: 400(line 255, the dex number),600(lines 277, 329, 384, typebadges and stat labels),
500(line 297, variant/region badges) andfontStyle: "italic"(line 419, the Pokédex quote). Satori picks the nearestregistered 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
loadOgFontand pass both tofonts, and either register an italic face ordrop
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-426the quote sits at
top: 48px; right: 56px; maxWidth: 420px, so its left edgeis 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 andis 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
#NNNnumber, anything over ~10 characters runs under the quote:Fletchinder,Corviknight,Centiskorch,Crabominable, and every formewhose suffix is not in
VARIANT_SUFFIXESand therefore keeps its full name(
Urshifu-Rapid-Strike,Necrozma-Dusk-Mane,Ogerpon-Wellspring,Tauros-Paldea-Combat,Calyrex-Shadow). Smallest fix: take the quote out ofposition: absoluteand make it the last child of the header flex row atopengraph-image.tsx:191withmarginLeft: "auto"; flexShrink: 0; alignSelf: "flex-start", and give the name column amaxWidthso Satori wrapsor 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."
generateStaticParamsatsrc/app/pokemon/[id]/opengraph-image.tsx:34-37includes formes, butopengraph-image.tsx:158-159builds the sprite URL frompokemonDbSlug(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'sfiles are
raichu-alola,charizard-megax,meowth-galar, so the fetch404s,
res.okis false, and the redesigned card ships with the grey "?" boxat
opengraph-image.tsx:215-232for every regional, Mega and Gmax page. Thebug 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/imgand alreadyknows Showdown's forme naming.
Standards review — round 2
Note:
git diff/git statusare blocked in this sandbox; reviewed by reading every file in the working-tree status plus the newsrc/app/og-shared.tsx.Dedup stopped one directory short: the route's own
page.tsxandclient.tsxstill hand-roll the helpers that were just moved intosrc/lib/pkmn.ts.src/app/pokemon/[id]/page.tsx:27-78andsrc/app/pokemon/[id]/client.tsx:100-166each redefineVARIANT_SUFFIXES,VARIANT_DISPLAY_NAMES,getVariantFromName,getBaseName,getRegionFromDexNumber(with the dex-numberifladderpkmn.tswas written to replace) and a localRegiontype.page.tsx:71-80also has the sameSTAT_LABELS+STAT_ORDERassrc/app/pokemon/[id]/opengraph-image.tsx:39-48, andSTAT_ORDERisSTAT_KEYSfromsrc/lib/dex-pokemon.ts:39under another name. Smallest fix: delete the local copies inpage.tsx/client.tsxand import from@/lib/pkmn; move the short-label map and order intosrc/lib/dex-pokemon.tsnext toSTAT_KEYS(named e.g.STAT_ABBREVIATIONS, since that file already exports a differentSTAT_LABELS) and import it in bothpage.tsxand the OG route; dropSTAT_ORDERin favour ofSTAT_KEYS.Two exported
Regiontypes with different value sets, plus a third copy of the region-name list.src/lib/pkmn.ts:390-399addsexport type Region = "Kanto" | …whilesrc/types/pokemon.ts:308-318already exportsRegion = "kanto" | …;src/components/pokemon/pokemon-card.tsx:18now imports the new one, soimport { Region }means different things per file.REGION_BY_GEN_ID(pkmn.ts:401-411) re-lists the display namesREGION_LABELS(types/pokemon.ts:391-402) already holds. Smallest fix: add aregionfield (typed with the existing lowercaseRegion) to eachGEN_RANGESentry inpkmn.ts:316-326, havegetRegionFromDexNumberreturnREGION_LABELS[gen.region](typestring | null), delete the newRegiontype andREGION_BY_GEN_ID, and typeRegionBadge's prop asstring. (types/pokemon.tsonly type-imports fromlib/learnsets, so importingREGION_LABELSintopkmn.tscreates no runtime cycle.) If merging is out of scope, at minimum rename the new type (OriginRegion) so the collision goes away.DexMarkis a hand-transcribed copy of the logo artwork, against the README's single-source rule.src/app/og-shared.tsx:121-159inlines every path of the mark as JSX;README.md:9-11says the canonical artwork ispublic/icons/logo-app.svgand "every brand surface references that same device mark", withscripts/generate-icons.ts:10deriving the other assets from it. The comment atog-shared.tsx:121also points at the derivedsrc/app/icon.svgrather than the canonical file. Editing the logo now silently leaves the OG images behind. Smallest fix: readpublic/icons/logo-app.svgonce (memoized likeloadOgFont, vianode:fs/promisesreadFile) and render it inDexMarkas<img src="data:image/svg+xml;base64,…" width height />; if that is rejected, point the comment atlogo-app.svgand 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+:433each repeat the same rootdivstyle (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:106and:429. Smallest fix: addOgFrame({ children, style? })toog-shared.tsxthat renders the rootdiv+OgBackdrop, and anogImageOptions(font)helper returning{ ...OG_SIZE, fonts }; use them in all three renders.Speculative prop on
OgFooter.src/app/og-shared.tsx:161takeslabel = "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:24addsOG_FETCH_REVALIDATE_SECONDS = 604800with a comment saying it mirrorssrc/lib/pokeapi.ts, which uses the bare literal eight times (pokeapi.ts:107,258,270,…);src/lib/tcg.ts:37spells its window as60 * 60 * 24. Smallest fix: export onePOKEAPI_REVALIDATE_SECONDSfrompokeapi.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-108and all ofog-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:121falls back to"#6D6C54"(which isTYPE_TEXT_COLORS.Normal) for the sprite frame, while the type-badge loop at:264falls back toOG_MUTEDfor the same "unknown type" case. Smallest fix: useOG_MUTEDin 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.