- Remove legacyBehavior prop and nested <a> tags from Link components
- Modernize to Next.js 13+ Link API with className directly on Link
- Convert external links to plain <a> tags (LinkItem, Discord link)
- Remove unnecessary passHref props from Header components
- Fix tab switching by mapping string values to GridType enum
The tab switching issue was caused by trying to parse string values
("characters", "weapons", "summons") as integers when they needed to
be mapped to GridType enum values.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
4.8 KiB
4.8 KiB
PRD: Fix App Version Fetching with Server Prefetch + Hydrator
Problem
- The app shows a missing translation key for
common.toasts.update.description.becauseappState.version.update_typeis empty. - Root cause: The app never fetches or populates
appState.versionwith backend data.
Current State (validated)
- Backend API and internal route exist and work:
/app/api/version/route.tscallsfetchFromApi('/version'). - Client util exists but is unused:
utils/fetchLatestVersion.tsx. - Global state shape:
utils/appState.tsxinitializesversionas{ version: '0.0', update_type: '', updated_at: '' }. UpdateToastClientreadsappState.versionbut never triggers a fetch and only checks on mount.
Goals
- Populate
appState.versionreliably on app load without extra client latency. - Show the update toast exactly when appropriate (within time window and not already seen).
- Avoid redundant network requests and race conditions.
Solution Overview
- Server-prefetch the version in the localized layout (
app/[locale]/layout.tsx). - Hydrate global state on the client via a tiny
VersionHydratorclient component. - Make
UpdateToastClientreactive to version changes using Valtio snapshots. - Ensure
update_typemaps to valid i18n keys; add a small fallback mapping.
Implementation Plan
- Server Prefetch in
app/[locale]/layout.tsx
- Fetch version data on the server and pass to the client for hydration.
- Use either of:
- Direct helper:
const version = await fetchFromApi('/version')(fromapp/lib/api-utils.ts), or - Next fetch:
const res = await fetch('/api/version', { cache: 'no-store' }); const version = await res.json();
- Direct helper:
- Do this inside the default exported async layout function.
- Handle errors gracefully (wrap in try/catch and set
version = null).
- Add
VersionHydrator(client)
- File:
app/components/VersionHydrator.tsx - Behavior: On mount and when
versionprop changes, setappState.version.
Example:
- 'use client'
- import { useEffect } from 'react'
- import { appState } from '~/utils/appState'
- export default function VersionHydrator({ version }: { version: AppUpdate | null }) { useEffect(() => { if (version && version.updated_at) { appState.version = version } }, [version]) return null }
- Wire Hydrator in Layout
- In
app/[locale]/layout.tsx, render<VersionHydrator version={version} />alongside<Header />and<UpdateToastClient />. - Order suggestion (not strict): Hydrator before UpdateToastClient.
- Make
UpdateToastClientreactive
- Import
useSnapshotfromvaltioand observeappState.version. - Change the
useEffectdependency to run whenversion?.updated_atbecomes available. - This ensures the toast can open even if hydration happens after mount.
Sketch:
- import { useSnapshot } from 'valtio'
- const { version } = useSnapshot(appState)
- useEffect(() => { if (version && version.updated_at) { const cookie = getToastCookie(version.updated_at) const now = new Date() const updatedAt = new Date(version.updated_at) const validUntil = add(updatedAt, { days: 7 }) if (now < validUntil && !cookie.seen) setUpdateToastOpen(true) } }, [version?.updated_at])
- Validate i18n key mapping for
update_type
- Current keys:
common.toasts.update.description.contentand...feature. - Ensure API returns only these (or map unknown types to a default):
- const typeKey = ['content', 'feature'].includes(version.update_type) ? version.update_type : 'content'
- appState.version.update_type = typeKey
- Optional: Pages Router compatibility (temporary)
- If the Pages Router
pages/_app.tsxstill needs server-unavailable behavior, add a small client hydrator there too or remove coupling toappState.version.
Files to Update
app/[locale]/layout.tsx(server prefetch + render Hydrator)app/components/VersionHydrator.tsx(new)app/components/UpdateToastClient.tsx(valtio snapshot + effect dependency)- Optional: Add a small mapping for
update_typebefore setting state
Testing
- With API up:
appState.versionis populated on first render; toast opens if within 7 days and unseen. - With API down: No crash; state remains initial; toast doesn’t open.
- Verify both locales; i18n keys resolve correctly for
update_type. - Confirm no duplicate requests across navigations.
Risks & Mitigations
- Race between hydrator and toast effect: mitigated by making toast reactive to
version?.updated_at. - Caching behavior: using
no-storeavoids stale data; can switch torevalidateif desired. - API type mismatch: handle unknown
update_typewith a default mapping.
Rollout Steps
- Implement server prefetch and hydrator.
- Update
UpdateToastClientreactivity. - Ship behind no feature flags (safe, read-only state).
- Observe logs and confirm behavior in both locales.