Gotchas
The repo's
arsvine-realm/docs/GOTCHAS.mdlists 27 of the same gotchas. This page is a maintainer-priority subset, with a public-facing angle.
Protected posts
useBlogPostStateauth-probe effect must depend onstate.authState. Otherwise switching protected posts within the same group keeps the effect from re-running, and the page hangs at auth checking. Seehooks/useBlogPostState.ts.authResolvedmust clearactiveRequestKeyandloadingLocalein both branches (granted and required). Clearing only the required branch leaves a stale key, and the next legitimate fetch gets incorrectly deduped. Seelib/blog-post-state.ts.- Protected posts'
mdxSourceis alwaysnullfrom SSG. Do not place ciphertext or hidden MDX payloads in_next/data/.../...json. DirectGET /api/post-variant?slug=<protected>&locale=zh-CNwithout a cookie must return403.
Route transitions and navigation
- Internal navigation goes through
useTransition().navigateTo(url).router.push()skips the transition animation and breaks home / content / detail motion choreography. useRouteLoadingKinddecides the loading variant from the leaving path, not the target. Switching to target-based selection breaks either home → detail or detail → detail.- The route loading overlay lives inside
TransitionContext. Putting it at the end ofdocument.bodywould cover the HUD.
Custom cursor and avatar
- CustomCursor hover state must go through the reset helper. Direct manipulation of internal refs leaves residue after route changes, scrolling, blur, visibility changes, and DOM unmounts.
- Avatar parallax mousemove must use
style.setProperty('transform', value, 'important'). Directstyle.transform = ...is silently overridden by the avatar entry keyframe'sforwardsfinal transform.
MDX rendering
<Explain>must not live inside an animation container. The z-index and stacking context get covered and tooltips get trapped under following paragraphs.- After the blog reveal animation ends, transform must be set to
'none', not an empty string. Any non-nonetransform creates a stacking context that traps<Explain>tooltips. - Pass
uppercase={false}explicitly to<AnimatedTitleChars>. The shared component defaults to uppercase, and re-applyingtoUpperCase()to CJK or already-uppercase strings produces unpredictable rendering. <Term>and<Explain>semantics:<Term note="...">word</Term>— rubification annotation, proper nouns / abbreviations / short glosses<Explain note="...">phrase</Explain>— tooltip / mobile bottom sheet, longer sentence-level notes<Explain>is a focusable span (not a button) to avoid inline wrapping and centering issues; on mobile it becomes a fixed bottom panel
Typography
--font-displayis Latin-only (ZELDA Free). Do not use it for CJK, accented Latin, blog titles, translated strings, or user content. Use--font-hudfor HUD-safe headings and--font-readingfor long-form content.- Google Fonts variable-font deduplication is intentional. The same
.woff2may be referenced by multiple@font-faceblocks at differentfont-weightvalues. Do not rewritescripts/fetch-google-fonts.mjsto force one file per weight. - COS custom-header Value field is value-only. Pasting
Cache-Control: public, ...as the value produces a malformed header that Firefox rejects.
Music player
- Track click is an explicit play intent. Do not add a "only autoplay if already playing" guard.
useMusicPlayerStateuses a play-intent flag to chainaudio.load()→audio.play(). - The player must not auto-open on mobile. The auto-open guard lives in
MusicPlayer; removing it breaks the mobile entry experience.
Media / assets
- The
cdn.arsvine.combucket only allowsarsvine.comand*.arsvine.comReferer values. Localhost and empty Referer are rejected. Local development must usescripts/dev-host-setup.cmd. - COS traffic is billable. Traffic packages are not a hard limit; after the package is exhausted traffic continues and is charged by usage. Enable budget alerts.
- Large assets (high-res images, originals, audio) must not be committed to Git.
public/music/still allows local test files, but production audio must come fromcdn.arsvine.com/realm/audio/.... - Do not assume
/[locale]/gameexists.useLayoutRouteModeand prefetch matchers still include it as a legacy branch, butpages/[locale]/gameis not in the file tree. - GitHub content paths must be repo-relative only.
lib/content/github.tsalready implements this: it rejects absolute URLs, protocol-relative URLs, leading/, backslashes, query strings, hash fragments, traversal, and encoded traversal. It splits the path into segments and encodes each one explicitly. - External links must be parsed with
new URL(), not substring checks likeincludes('github.com').lib/safe-external-href.tsis the shared helper.
Locale and proxy
- Locale resolution is fixed at
NEXT_LOCALE cookie > Accept-Language > zh-CN. Do not infer language from IP or country. TheGEO_COUNTRYcookie is for UI micro-tuning only. proxy.tsalready handles "looks like a locale but unsupported" cases (e.g./fr/web/1). Do not add duplicate logic.
Commits and dependencies
- pnpm workspace settings live in
pnpm-workspace.yaml, not inpackage.json#pnpm. pnpm 11 ignores the package-level key. - New tests live under
tests/<area>/. Do not put.test.tsnext to source files.
Current test layout
Full coverage requires Node 24 and pnpm 11. CI runs pnpm check, which is lint + typecheck + test + build end to end.