Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
3e961ec
Enable Cache Components and stream the root provider tree
lucasdoell Jun 26, 2026
4330e0b
Remove route segment configs and migrate unstable_cache to use cache
lucasdoell Jun 26, 2026
34954df
Defer sidebar skeleton random width to after mount
lucasdoell Jun 26, 2026
5a9c03d
Mount the Vercel Toolbar on the client only
lucasdoell Jun 26, 2026
b7bff57
Add a cookie-free metadata translator for Cache Components
lucasdoell Jun 26, 2026
0e6f73f
Resolve route metadata in the default locale (Cache Components)
lucasdoell Jun 26, 2026
88e0040
Skip OpenTelemetry setup during the build phase
lucasdoell Jun 26, 2026
fff2bce
Cache the map hero trends page data
lucasdoell Jun 26, 2026
2e8932d
Render the map hero trends page at request time
lucasdoell Jun 26, 2026
911363b
Clean up metadata migration types and lint
lucasdoell Jun 26, 2026
94fcaa3
Tighten Cache Components migration comments
lucasdoell Jun 26, 2026
cf33844
Upgrade apps/web to Next.js 16.3 canary
lucasdoell Jun 26, 2026
fe432fb
Enable partial prefetching and stream the map trends page
lucasdoell Jun 26, 2026
5ddb127
Cache immutable per-map reads aggressively
lucasdoell Jun 26, 2026
b095d86
Make the cached map readers async
lucasdoell Jun 26, 2026
23fd2b6
Cache scrim aggregates and team stats with tag invalidation
lucasdoell Jun 26, 2026
d0a6964
Drop redundant comments on cache invalidation calls
lucasdoell Jun 26, 2026
b9a3683
Use BatchSpanProcessor so telemetry never blocks prerendering
lucasdoell Jun 26, 2026
4db1e85
Hold partialPrefetching until authed routes are instant-ready
lucasdoell Jun 26, 2026
8889845
Stream auth-gated content on the scrim and map layouts
lucasdoell Jun 26, 2026
f970a34
Stream auth-gated chrome in shared and section layouts
lucasdoell Jun 26, 2026
acfcfd5
Add segment loading shells for instant navigation
lucasdoell Jun 26, 2026
236cf09
Add loading shells for the remaining auth/i18n marketing pages
lucasdoell Jun 26, 2026
1a26dbd
Hoist next-themes into the static shell to stop the light-mode flash
lucasdoell Jun 26, 2026
329913c
Show an app-chrome skeleton while the root providers stream
lucasdoell Jun 26, 2026
529e440
Match loading skeletons to their routes and fix the dashboard tour in…
lucasdoell Jun 26, 2026
d9ab3b7
Give every route a bespoke loading skeleton matching its content
lucasdoell Jun 26, 2026
7b2c294
Merge pull request #80 from luxdotdev/feat/cache-components-migration
lucasdoell Jun 26, 2026
19a0cf6
Update loading skeletons
lucasdoell Jun 28, 2026
5755122
Fix tank damage blocked showing 0th percentile in scrim overview
lucasdoell Jun 28, 2026
22e5fe8
Run map ingestion event inserts sequentially within the transaction
lucasdoell Jun 30, 2026
9ef18bd
Fall back to the raw map name when it isn't in the maps catalog
lucasdoell Jun 30, 2026
376cbe0
Guard blank map parses and add structured client upload logging
lucasdoell Jun 30, 2026
8e10de5
Reformat
lucasdoell Jun 30, 2026
2cb8f30
Inline footer copyright year to fix dynamic "use cache" rejection
lucasdoell Jun 30, 2026
251d331
Force request-time footer render to stop dynamic "use cache" rejection
lucasdoell Jun 30, 2026
7909884
Guard team-stats layout flag reads with connection()
lucasdoell Jul 1, 2026
e621a04
Order flag identify teams to stabilize precompute cache key
lucasdoell Jul 1, 2026
1374e5a
Day-align team-stats date range to stabilize cache key
lucasdoell Jul 1, 2026
a9ce9b4
Pass resolved positionalData flag into map overview
lucasdoell Jul 1, 2026
2f5ed7f
Order MapData resolvers so cached map args are deterministic
lucasdoell Jul 1, 2026
cb5c9d9
Document Cache Components determinism + request-time rules
lucasdoell Jul 1, 2026
086d1d0
Cache the WP artifact via use cache to stop map-route hanging promise
lucasdoell Jul 1, 2026
2397089
Harden remaining nondeterministic cache inputs from the audit
lucasdoell Jul 1, 2026
d1c8255
Add precompute flag group and code-header constant
lucasdoell Jul 4, 2026
58f2518
Add precomputed flag readers with deterministic fallback
lucasdoell Jul 4, 2026
526f6ef
Precompute page flags once per request in the proxy
lucasdoell Jul 4, 2026
f5db02a
Read scrim and map page flags from the precomputed code
lucasdoell Jul 4, 2026
6e84bd4
Read page flags from the precomputed code across the app
lucasdoell Jul 4, 2026
d53eb52
Read shared-chrome flags from the precomputed code
lucasdoell Jul 4, 2026
ccfd2bb
Document the precomputed-flags rule for Cache Components
lucasdoell Jul 4, 2026
bbd13dd
Prerender the root provider tree; stream request data as islands
lucasdoell Jul 4, 2026
f11fca2
Prerender the marketing page into the static shell
lucasdoell Jul 4, 2026
7208821
Collapse the dashboard loading cascade into one stable skeleton
lucasdoell Jul 4, 2026
a69f5fb
Document the static-shell loading-state rules
lucasdoell Jul 4, 2026
6dfcf8b
Roll out the single-boundary loading pattern across all routes
lucasdoell Jul 4, 2026
49b8586
Mount the scrim page chrome once; gate auth inside the content boundary
lucasdoell Jul 4, 2026
4c368a1
Document the chrome-mounts-once and content-boundary auth-gate rules
lucasdoell Jul 4, 2026
a978e15
Make map tab switches optimistic with cached revisits and a dot-matri…
lucasdoell Jul 4, 2026
cdeca62
Runtime-prefetch map tabs by caching the whole tab render path
lucasdoell Jul 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 35 additions & 2 deletions apps/web/AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
<!-- NEXT-AGENTS-MD-START -->[Next.js Docs Index]|root: ./.next-docs|STOP. What you remember about Next.js is WRONG for this project. Always search docs and read before any task.|If docs missing, run this command first: npx @next/codemod agents-md --output CLAUDE.md|01-app:{04-glossary.mdx}|01-app/01-getting-started:{01-installation.mdx,02-project-structure.mdx,03-layouts-and-pages.mdx,04-linking-and-navigating.mdx,05-server-and-client-components.mdx,06-fetching-data.mdx,07-mutating-data.mdx,08-caching.mdx,09-revalidating.mdx,10-error-handling.mdx,11-css.mdx,12-images.mdx,13-fonts.mdx,14-metadata-and-og-images.mdx,15-route-handlers.mdx,16-proxy.mdx,17-deploying.mdx,18-upgrading.mdx}|01-app/02-guides:{ai-agents.mdx,analytics.mdx,authentication.mdx,backend-for-frontend.mdx,caching-without-cache-components.mdx,ci-build-caching.mdx,content-security-policy.mdx,css-in-js.mdx,custom-server.mdx,data-security.mdx,debugging.mdx,draft-mode.mdx,environment-variables.mdx,forms.mdx,incremental-static-regeneration.mdx,instant-navigation.mdx,instrumentation.mdx,internationalization.mdx,json-ld.mdx,lazy-loading.mdx,local-development.mdx,mcp.mdx,mdx.mdx,memory-usage.mdx,migrating-to-cache-components.mdx,multi-tenant.mdx,multi-zones.mdx,open-telemetry.mdx,package-bundling.mdx,prefetching.mdx,preserving-ui-state.mdx,production-checklist.mdx,progressive-web-apps.mdx,public-static-pages.mdx,redirecting.mdx,sass.mdx,scripts.mdx,self-hosting.mdx,single-page-applications.mdx,static-exports.mdx,streaming.mdx,tailwind-v3-css.mdx,third-party-libraries.mdx,videos.mdx}|01-app/02-guides/migrating:{app-router-migration.mdx,from-create-react-app.mdx,from-vite.mdx}|01-app/02-guides/testing:{cypress.mdx,jest.mdx,playwright.mdx,vitest.mdx}|01-app/02-guides/upgrading:{codemods.mdx,version-14.mdx,version-15.mdx,version-16.mdx}|01-app/03-api-reference:{07-edge.mdx,08-turbopack.mdx}|01-app/03-api-reference/01-directives:{use-cache-private.mdx,use-cache-remote.mdx,use-cache.mdx,use-client.mdx,use-server.mdx}|01-app/03-api-reference/02-components:{font.mdx,form.mdx,image.mdx,link.mdx,script.mdx}|01-app/03-api-reference/03-file-conventions/01-metadata:{app-icons.mdx,manifest.mdx,opengraph-image.mdx,robots.mdx,sitemap.mdx}|01-app/03-api-reference/03-file-conventions/02-route-segment-config:{dynamicParams.mdx,instant.mdx,maxDuration.mdx,preferredRegion.mdx,runtime.mdx}|01-app/03-api-reference/03-file-conventions:{default.mdx,dynamic-routes.mdx,error.mdx,forbidden.mdx,instrumentation-client.mdx,instrumentation.mdx,intercepting-routes.mdx,layout.mdx,loading.mdx,mdx-components.mdx,not-found.mdx,page.mdx,parallel-routes.mdx,proxy.mdx,public-folder.mdx,route-groups.mdx,route.mdx,src-folder.mdx,template.mdx,unauthorized.mdx}|01-app/03-api-reference/04-functions:{after.mdx,cacheLife.mdx,cacheTag.mdx,catchError.mdx,connection.mdx,cookies.mdx,draft-mode.mdx,fetch.mdx,forbidden.mdx,generate-image-metadata.mdx,generate-metadata.mdx,generate-sitemaps.mdx,generate-static-params.mdx,generate-viewport.mdx,headers.mdx,image-response.mdx,next-request.mdx,next-response.mdx,not-found.mdx,permanentRedirect.mdx,redirect.mdx,refresh.mdx,revalidatePath.mdx,revalidateTag.mdx,unauthorized.mdx,unstable_cache.mdx,unstable_noStore.mdx,unstable_rethrow.mdx,updateTag.mdx,use-link-status.mdx,use-params.mdx,use-pathname.mdx,use-report-web-vitals.mdx,use-router.mdx,use-search-params.mdx,use-selected-layout-segment.mdx,use-selected-layout-segments.mdx,userAgent.mdx}|01-app/03-api-reference/05-config/01-next-config-js:{adapterPath.mdx,allowedDevOrigins.mdx,appDir.mdx,assetPrefix.mdx,authInterrupts.mdx,basePath.mdx,cacheComponents.mdx,cacheHandlers.mdx,cacheLife.mdx,compress.mdx,crossOrigin.mdx,cssChunking.mdx,deploymentId.mdx,devIndicators.mdx,distDir.mdx,env.mdx,expireTime.mdx,exportPathMap.mdx,generateBuildId.mdx,generateEtags.mdx,headers.mdx,htmlLimitedBots.mdx,httpAgentOptions.mdx,images.mdx,incrementalCacheHandlerPath.mdx,inlineCss.mdx,logging.mdx,mdxRs.mdx,onDemandEntries.mdx,optimizePackageImports.mdx,output.mdx,pageExtensions.mdx,poweredByHeader.mdx,productionBrowserSourceMaps.mdx,proxyClientMaxBodySize.mdx,reactCompiler.mdx,reactMaxHeadersLength.mdx,reactStrictMode.mdx,redirects.mdx,rewrites.mdx,sassOptions.mdx,serverActions.mdx,serverComponentsHmrCache.mdx,serverExternalPackages.mdx,staleTimes.mdx,staticGeneration.mdx,taint.mdx,trailingSlash.mdx,transpilePackages.mdx,turbopack.mdx,turbopackFileSystemCache.mdx,turbopackIgnoreIssue.mdx,typedRoutes.mdx,typescript.mdx,urlImports.mdx,useLightningcss.mdx,viewTransition.mdx,webVitalsAttribution.mdx,webpack.mdx}|01-app/03-api-reference/05-config:{02-typescript.mdx,03-eslint.mdx}|01-app/03-api-reference/06-cli:{create-next-app.mdx,next.mdx}|02-pages/01-getting-started:{01-installation.mdx,02-project-structure.mdx,04-images.mdx,05-fonts.mdx,06-css.mdx,11-deploying.mdx}|02-pages/02-guides:{analytics.mdx,authentication.mdx,babel.mdx,ci-build-caching.mdx,content-security-policy.mdx,css-in-js.mdx,custom-server.mdx,debugging.mdx,draft-mode.mdx,environment-variables.mdx,forms.mdx,incremental-static-regeneration.mdx,instrumentation.mdx,internationalization.mdx,lazy-loading.mdx,mdx.mdx,multi-zones.mdx,open-telemetry.mdx,package-bundling.mdx,post-css.mdx,preview-mode.mdx,production-checklist.mdx,redirecting.mdx,sass.mdx,scripts.mdx,self-hosting.mdx,static-exports.mdx,tailwind-v3-css.mdx,third-party-libraries.mdx}|02-pages/02-guides/migrating:{app-router-migration.mdx,from-create-react-app.mdx,from-vite.mdx}|02-pages/02-guides/testing:{cypress.mdx,jest.mdx,playwright.mdx,vitest.mdx}|02-pages/02-guides/upgrading:{codemods.mdx,version-10.mdx,version-11.mdx,version-12.mdx,version-13.mdx,version-14.mdx,version-9.mdx}|02-pages/03-building-your-application/01-routing:{01-pages-and-layouts.mdx,02-dynamic-routes.mdx,03-linking-and-navigating.mdx,05-custom-app.mdx,06-custom-document.mdx,07-api-routes.mdx,08-custom-error.mdx}|02-pages/03-building-your-application/02-rendering:{01-server-side-rendering.mdx,02-static-site-generation.mdx,04-automatic-static-optimization.mdx,05-client-side-rendering.mdx}|02-pages/03-building-your-application/03-data-fetching:{01-get-static-props.mdx,02-get-static-paths.mdx,03-forms-and-mutations.mdx,03-get-server-side-props.mdx,05-client-side.mdx}|02-pages/03-building-your-application/06-configuring:{12-error-handling.mdx}|02-pages/04-api-reference:{06-edge.mdx,08-turbopack.mdx}|02-pages/04-api-reference/01-components:{font.mdx,form.mdx,head.mdx,image-legacy.mdx,image.mdx,link.mdx,script.mdx}|02-pages/04-api-reference/02-file-conventions:{instrumentation.mdx,proxy.mdx,public-folder.mdx,src-folder.mdx}|02-pages/04-api-reference/03-functions:{get-initial-props.mdx,get-server-side-props.mdx,get-static-paths.mdx,get-static-props.mdx,next-request.mdx,next-response.mdx,use-params.mdx,use-report-web-vitals.mdx,use-router.mdx,use-search-params.mdx,userAgent.mdx}|02-pages/04-api-reference/04-config/01-next-config-js:{adapterPath.mdx,allowedDevOrigins.mdx,assetPrefix.mdx,basePath.mdx,bundlePagesRouterDependencies.mdx,compress.mdx,crossOrigin.mdx,deploymentId.mdx,devIndicators.mdx,distDir.mdx,env.mdx,exportPathMap.mdx,generateBuildId.mdx,generateEtags.mdx,headers.mdx,httpAgentOptions.mdx,images.mdx,logging.mdx,onDemandEntries.mdx,optimizePackageImports.mdx,output.mdx,pageExtensions.mdx,poweredByHeader.mdx,productionBrowserSourceMaps.mdx,proxyClientMaxBodySize.mdx,reactStrictMode.mdx,redirects.mdx,rewrites.mdx,serverExternalPackages.mdx,trailingSlash.mdx,transpilePackages.mdx,turbopack.mdx,typescript.mdx,urlImports.mdx,useLightningcss.mdx,webVitalsAttribution.mdx,webpack.mdx}|02-pages/04-api-reference/04-config:{01-typescript.mdx,02-eslint.mdx}|02-pages/04-api-reference/05-cli:{create-next-app.mdx,next.mdx}|03-architecture:{accessibility.mdx,fast-refresh.mdx,nextjs-compiler.mdx,supported-browsers.mdx}|04-community:{01-contribution-guide.mdx,02-rspack.mdx}<!-- NEXT-AGENTS-MD-END -->

## Internationalization

The Next.js app uses `next-intl` for internationalization, with `en.json`, `ko.json`, and `zh.json` files living at `apps/web/messages`. When building user-facing components, make sure to include proper translation support.
Expand All @@ -8,6 +6,33 @@ The Next.js app uses `next-intl` for internationalization, with `en.json`, `ko.j

Before committing, ensure that changes are formatted, linted, typechecked, and pass the test suite.

## Cache Components (Cache Components / PPR is enabled)

`cacheComponents: true` + `partialPrefetching: true` are on. PPR prerenders every request **twice** (a cache-warming pass, then the final prerender), so four rules prevent the recurring `HANGING_PROMISE_REJECTION` (`dynamic "use cache"`) and `Unexpected cache miss after cache warming phase` errors:

1. **`use cache` inputs must be deterministic across passes.** A cached function's arguments (and closure values) become its cache key. Only pass route params, scalar DB columns, or **fixed-order** constant arrays. Never pass:
- a raw `findFirst`/`findMany` result — add an explicit `orderBy` (an unordered query can return a different row/order between passes; e.g. `resolveScrimMapDataId`, flag `identify`'s team `idArray`).
- a `Set`→array, `Object.keys/values/entries`, or any array whose order isn't pinned.
- a `new Date()`/`Date.now()`/`Math.random()` value — quantize it (e.g. day-align, like `computeDateRange`) or don't key on it.

2. **Reads that reach `use cache` in a layout's static-shell region must be request-time.** `@vercel/edge-config`'s `get()` and every feature flag (`@/lib/flags`) compile to `use cache`. If such a read runs outside a page's dynamic boundary (e.g. in the `Footer`/a `layout.tsx`) without already being request-time, add `await connection()` as the first line (see `AuthedAppHeader`, `Footer`, the team-stats layout). Note: awaiting `getTranslations`/`getLocale` does **not** defer — only `connection()`/`headers()`/`cookies()`/`auth()` do.

3. **Flag reads in render code must use the precomputed code.** `proxy.ts` evaluates all `pageFlags` once per request and forwards a signed code on the `x-flags-code` request header; pages/layouts/server components MUST read flags via `getFlag`/`getAllFlags` (`@/lib/flags-helpers`), never by calling the flag function directly. A live flag call can transiently fail and silently fall back to `defaultValue` in only one of the two prerender passes — the passes then disagree about *which* `use cache` calls run, which is the classic cause of `HANGING_PROMISE_REJECTION`. Route handlers and server actions (never prerendered) still call flags live (`resolveAllFlags` or the flag itself). New flags must be appended to `pageFlags` in `src/lib/flags-precompute.ts`.

4. **The static shell must stay request-free — providers hydrate via islands, fallbacks are sync.** The root layout's provider tree is prerendered; request-derived values (locale override, flags, session extras) stream in as Suspense islands that hydrate stateful client providers (`IntlProvider`/`FeatureFlagsProvider`/`BrandThemeProvider` + their `*Hydrator` components in `layout.tsx`). Consequences:
- `loading.tsx` and Suspense fallbacks must be synchronous and cookie-free. For translated text in them use `getStaticTranslations` (`@/lib/metadata-i18n`) — `getTranslations` reads the LOCALE cookie and forces the shell dynamic. Same for client components that prerender: `useTranslations` works because the intl provider carries the default-locale catalog (plus explicit `timeZone`) — never remove those props or next-intl "environment-falls-back" into `cookies()`.
- A client provider above `{children}` must not read request data (`useSearchParams`, cookies): hold state and sync via a Suspense-wrapped child (see `TeamSwitcherProvider`).
- Prefer ONE content boundary per page whose fallback mirrors the real content's own pending layout (see `/dashboard`'s `ScrimListSkeleton` mirroring `ScrimPagination`), instead of stacking route `loading.tsx` + layout skeleton + component skeletons — that's what causes multi-skeleton flashing.
- Page chrome (`DashboardLayout`/`AppHeader`) must render exactly once, in the page's static shell — never in both a `loading.tsx` and the page (the fallback→content swap remounts it and re-suspends the header), and never below a layout-level auth gate. Auth gates are the FIRST step of the page's Suspense-wrapped content component (`isAuthedToViewScrim`/`isAuthedToViewMap` → `<NoAuthCard />`, see the `[scrimId]` routes), so they resolve inside the page's single skeleton. Chrome that needs param-derived request data takes a thunk resolved inside the streamed header (`DashboardLayout`'s `guestModeSource`).

5. **Runtime prefetching (`prefetch = 'allow-runtime'`) requires the whole render path to be cache-safe.** The map page prefetches `?tab=` URLs on hover/trajectory (`MapTabs` + `prefetchRoute` in `use-predictive-prefetch.ts`); the server resolves the target tab ahead of the click only as far as the caching structure allows. Rules that make it work:
- Per-viewer request work (auth gate, session, user row, colorblind palette) lives in ONE `"use cache: private"` scope (`getMapViewerContext`) — an uncached session/DB read aborts the runtime prerender at the surrounding Suspense boundary, and in `generateMetadata` it fails the whole prefetch with E1370 ("couldn't prerender metadata"). `cacheLife` stale must be ≥ 30s for prefetching.
- Tab/server components on the prefetch path are `"use cache"` components: locale and feature flags come in as PROPS (request APIs are forbidden in public cache scopes) — translate with `getLocaleTranslations(locale, ns)` (`@/lib/metadata-i18n`), never `getTranslations`. Cached output embedding presigned URLs must cap `expire` below the URL's lifetime (see HeatmapTab/RoutesTab).
- `router.prefetch` can't issue runtime prefetches directly (Next internals); the standard prefetch is enough — the server's `allow-runtime` hint upgrades it. `typedRoutes` narrows `prefetch` to one arg, so use `prefetchRoute`.
- Testing gotcha: Next disables ALL prefetching for bot user agents — headless-Chrome UAs ("HeadlessChrome") silently no-op; pass a real UA when verifying with agent-browser, and use a production build (`next start`), never dev.

These surface only at request time on real `[param]` routes (not `next build`); check Vercel runtime logs after deploying cache-touching changes.

## Agent skills

### Issue tracker
Expand All @@ -21,3 +46,11 @@ Canonical defaults (`needs-triage`, `needs-info`, `ready-for-agent`, `ready-for-
### Domain docs

Single-context: one `CONTEXT.md` + `docs/adr/` at the repo root (created lazily by `/grill-with-docs`). See `docs/agents/domain.md`.

<!-- BEGIN:nextjs-agent-rules -->
# This is NOT the Next.js you know

This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices.

**Keep this block, including in commits.** It is part of the project's agent setup, maintained by `next dev` for every agent that works here. If it appears as an uncommitted change, that is intentional — commit it as-is. Do not remove it to clean up a diff; it will be regenerated.
<!-- END:nextjs-agent-rules -->
1 change: 1 addition & 0 deletions apps/web/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -2187,6 +2187,7 @@
"back": "Back to scrim overview",
"dashboard": "Dashboard",
"tabsLabel": "Map sections",
"loadingTab": "Loading tab data",
"mobileBanner": {
"message": "For the best experience, we recommend using a larger screen.",
"dismiss": "Dismiss"
Expand Down
1 change: 1 addition & 0 deletions apps/web/messages/ko.json
Original file line number Diff line number Diff line change
Expand Up @@ -2187,6 +2187,7 @@
"back": "스크림 개요로 돌아가기",
"dashboard": "대시보드",
"tabsLabel": "맵 섹션",
"loadingTab": "탭 데이터 불러오는 중",
"mobileBanner": {
"message": "최상의 경험을 위해 더 큰 화면 사용을 권장합니다.",
"dismiss": "닫기"
Expand Down
1 change: 1 addition & 0 deletions apps/web/messages/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -2187,6 +2187,7 @@
"back": "返回比赛概览",
"dashboard": "仪表板",
"tabsLabel": "地图分区",
"loadingTab": "正在加载标签页数据",
"mobileBanner": {
"message": "为了获得最佳体验,建议使用更大的屏幕。",
"dismiss": "关闭"
Expand Down
2 changes: 2 additions & 0 deletions apps/web/next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ const cspHeader = `
`;

const nextConfig: NextConfig = {
cacheComponents: true,
partialPrefetching: true,
reactCompiler: true,
typedRoutes: true,
transpilePackages: ["@parsertime/transactional"],
Expand Down
2 changes: 1 addition & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@
"media-chrome": "^4.18.2",
"motion": "^12.23.26",
"nanoid": "^5.1.5",
"next": "16.2.9",
"next": "16.3.0-preview.5",
"next-axiom": "^1.9.2",
"next-intl": "^4.9.2",
"next-seo": "^7.2.0",
Expand Down
6 changes: 3 additions & 3 deletions apps/web/src/app/(auth)/auth-error/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { Footer } from "@/components/footer";
import { getMetadataTranslations } from "@/lib/metadata-i18n";
import type { Metadata } from "next";
import { getTranslations } from "next-intl/server";

export async function generateMetadata(): Promise<Metadata> {
const t = await getTranslations("authError.metadata");
export function generateMetadata(): Metadata {
const t = getMetadataTranslations("authError.metadata");
return { title: t("title"), description: t("description") };
}

Expand Down
52 changes: 45 additions & 7 deletions apps/web/src/app/(auth)/auth-error/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { getStaticTranslations } from "@/lib/metadata-i18n";
import { getTranslations } from "next-intl/server";
import Link from "next/link";
import { Suspense } from "react";

type Error =
| "Configuration"
Expand All @@ -10,9 +13,31 @@ type Error =
| "AdapterError"
| "Default";

export default async function AuthErrorPage(props: PageProps<"/auth-error">) {
const searchParams = await props.searchParams;
const t = await getTranslations("authError");
// Static shell: the page frame and heading prerender (default-locale title via
// the cookie-free translator); the searchParams-derived error message streams
// into ONE boundary whose fallback mirrors the loaded content's layout.
export default function AuthErrorPage(props: PageProps<"/auth-error">) {
const t = getStaticTranslations("authError");

return (
<div className="flex h-[90vh] flex-col items-center justify-center space-y-6 p-6 text-center">
<h1 className="text-3xl font-bold">{t("title")}</h1>
<Suspense fallback={<AuthErrorSkeleton />}>
<AuthErrorContent searchParams={props.searchParams} />
</Suspense>
</div>
);
}

async function AuthErrorContent({
searchParams,
}: {
searchParams: PageProps<"/auth-error">["searchParams"];
}) {
const [params, t] = await Promise.all([
searchParams,
getTranslations("authError"),
]);

const errorMessages: Record<Error, string> = {
Configuration: t("errors.configuration"),
Expand All @@ -22,15 +47,14 @@ export default async function AuthErrorPage(props: PageProps<"/auth-error">) {
AdapterError: t("errors.adapterError"),
Default: t("errors.default"),
};
const rawError = searchParams.error;
const rawError = params.error;
const error: Error =
typeof rawError === "string" && Object.hasOwn(errorMessages, rawError)
? (rawError as Error)
: "Default";

return (
<div className="flex h-[90vh] flex-col items-center justify-center space-y-6 p-6 text-center">
<h1 className="text-3xl font-bold">{t("title")}</h1>
<>
<p className="max-w-[600px] text-gray-500 dark:text-gray-400">
<span className="font-bold">{t("error")}</span>{" "}
{errorMessages[error] ?? errorMessages.Default}
Expand All @@ -52,6 +76,20 @@ export default async function AuthErrorPage(props: PageProps<"/auth-error">) {
<Link href="/sign-in">{t("signIn")}</Link>
</Button>
</div>
</div>
</>
);
}

// Mirrors the loaded content: two message lines and the button row.
function AuthErrorSkeleton() {
return (
<>
<Skeleton className="h-4 w-[500px] max-w-full" />
<Skeleton className="h-4 w-[440px] max-w-full" />
<div className="flex space-x-4">
<Skeleton className="h-10 w-24" />
<Skeleton className="h-10 w-24" />
</div>
</>
);
}
Loading
Loading