Skip to content

Commit ee23395

Browse files
authored
Merge pull request #4826 from udecode/fix/vercel-caching-locale-paths
2 parents 9b2287d + eb9e1d7 commit ee23395

67 files changed

Lines changed: 6549 additions & 107 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 301 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,301 @@
1+
---
2+
title: Fix Vercel Caching - $50+ Monthly Overage
3+
type: fix
4+
date: 2026-01-23
5+
deepened: 2026-01-23
6+
---
7+
8+
# Fix Vercel Caching - $50+ Monthly Overage
9+
10+
## Enhancement Summary
11+
12+
**Deepened on:** 2026-01-23
13+
**Sources:** Vercel React Best Practices, 3 reviewer agents (DHH, Kieran, Simplicity)
14+
15+
### Key Improvements
16+
1. Expanded scope to include ALL locale-dependent files (18 files, not 3)
17+
2. Added Vercel-specific caching guidance (LRU vs React.cache)
18+
3. Fixed `generateStaticParams` issues for CN route
19+
4. Added client-side utility migrations
20+
21+
### Critical Findings from Reviewers
22+
- Home page ALSO uses `searchParams` - must fix
23+
- `useLocale.ts` uses `useSearchParams()` - needs migration to `usePathname()`
24+
- Dual encoding (`/cn/...?locale=cn`) is redundant - remove query params entirely
25+
26+
---
27+
28+
## Overview
29+
30+
Docs pages have **0% cache hit rate** causing $50+ Vercel overage. Every request hits origin server, running expensive SSR (code highlighting, file tree generation).
31+
32+
## Problem Statement
33+
34+
**Symptoms from Vercel dashboard:**
35+
- 0% cache hit rate on `/docs/[[...slug]]`
36+
- $22.84 Fluid Active CPU (178 hours)
37+
- $20.43 Fast Origin Transfer (338 GB)
38+
- 99.5% traffic from single region (iad1)
39+
- Spike started ~Jan 7
40+
41+
**Root Cause:**
42+
```tsx
43+
// apps/www/src/app/(app)/docs/[[...slug]]/page.tsx:35-37
44+
type DocPageProps = {
45+
searchParams: Promise<{ locale: string }>; // <-- THIS FORCES DYNAMIC RENDERING
46+
};
47+
```
48+
49+
Using `searchParams` in Next.js App Router **forces dynamic rendering** - pages cannot be statically generated or cached, even with `generateStaticParams()`.
50+
51+
### Vercel Best Practices Context
52+
53+
From Vercel's React Best Practices guide (Section 3.3 - Cross-Request LRU Caching):
54+
55+
> "React.cache() only works within one request. For data shared across sequential requests, use an LRU cache."
56+
57+
However, for **static documentation sites**, the better approach is:
58+
1. **Static Generation** via `generateStaticParams()` - already exists but bypassed
59+
2. **Remove dynamic triggers** - `searchParams` forces dynamic mode
60+
3. **Vercel Edge CDN** will cache static pages automatically
61+
62+
The current `React.cache()` calls in [registry-cache.ts](apps/www/src/lib/registry-cache.ts) only deduplicate within a single request - they don't help with CDN caching.
63+
64+
## Proposed Solution
65+
66+
Move locale from query param (`?locale=cn`) to path segment (`/cn/docs/...`).
67+
68+
**Why this approach:**
69+
- Enables full static generation + CDN caching
70+
- Better SEO (separate URLs for each locale)
71+
- Follows Next.js i18n best practices
72+
- No runtime locale detection overhead
73+
74+
## Technical Approach
75+
76+
### Files to Modify (Complete List)
77+
78+
| File | Change |
79+
|------|--------|
80+
| `apps/www/src/app/(app)/docs/[[...slug]]/page.tsx` | Remove `searchParams`, add locale prop |
81+
| `apps/www/src/app/(app)/page.tsx` | Remove `searchParams` (home page!) |
82+
| `apps/www/src/app/cn/docs/[[...slug]]/page.tsx` | NEW - CN docs route |
83+
| `apps/www/src/app/cn/page.tsx` | NEW - CN home page |
84+
| `apps/www/src/hooks/useLocale.ts` | Use `usePathname()` not `useSearchParams()` |
85+
| `apps/www/src/lib/withLocale.ts` | Remove `?locale=cn` suffix |
86+
| `apps/www/src/components/languages-dropdown-menu.tsx` | Remove query param setting |
87+
| `apps/www/next.config.ts` | Replace rewrites with redirects |
88+
89+
### Step 1: Fix English Docs Page
90+
91+
Remove `searchParams` from page props:
92+
93+
```tsx
94+
// apps/www/src/app/(app)/docs/[[...slug]]/page.tsx
95+
96+
// BEFORE
97+
type DocPageProps = {
98+
params: Promise<{ slug: string[] }>;
99+
searchParams: Promise<{ locale: string }>; // DELETE THIS
100+
};
101+
102+
// AFTER
103+
type DocPageProps = {
104+
params: Promise<{ slug: string[] }>;
105+
locale?: 'en' | 'cn'; // Optional prop, defaults to 'en'
106+
};
107+
108+
export const dynamic = 'force-static';
109+
110+
// Update getDocFromParams
111+
async function getDocFromParams({ params, locale = 'en' }: DocPageProps) {
112+
const slugParam = (await params).slug;
113+
// Use locale prop instead of searchParams
114+
if (locale === 'cn') {
115+
// Chinese logic...
116+
}
117+
// ...
118+
}
119+
```
120+
121+
### Step 2: Create CN Docs Route
122+
123+
```tsx
124+
// apps/www/src/app/cn/docs/[[...slug]]/page.tsx
125+
import { DocContent } from '@/app/(app)/docs/[[...slug]]/doc-content';
126+
import { allDocs } from 'contentlayer/generated';
127+
// ... other imports from main page
128+
129+
export const dynamic = 'force-static';
130+
131+
// IMPORTANT: Generate params for CN docs specifically
132+
export function generateStaticParams() {
133+
return allDocs
134+
.filter((doc) => doc._raw.sourceFileName?.endsWith('.cn.mdx'))
135+
.map((doc) => ({
136+
slug: doc.slugAsParams.replace(/\.cn$/, '').split('/').slice(1),
137+
}));
138+
}
139+
140+
export default async function CNDocPage({ params }: { params: Promise<{ slug: string[] }> }) {
141+
// Render with locale='cn'
142+
// ... (copy rendering logic with locale hardcoded to 'cn')
143+
}
144+
```
145+
146+
### Step 3: Fix Client-Side Locale Detection
147+
148+
```tsx
149+
// apps/www/src/hooks/useLocale.ts
150+
151+
// BEFORE - forces client-side hydration issues
152+
import { useSearchParams } from 'next/navigation';
153+
154+
export const useLocale = () => {
155+
const searchParams = useSearchParams();
156+
const locale = searchParams?.get('locale') || 'en';
157+
return locale;
158+
};
159+
160+
// AFTER - derive from pathname
161+
import { usePathname } from 'next/navigation';
162+
163+
export const useLocale = () => {
164+
const pathname = usePathname();
165+
return pathname?.startsWith('/cn') ? 'cn' : 'en';
166+
};
167+
```
168+
169+
### Step 4: Fix Locale Link Helper
170+
171+
```tsx
172+
// apps/www/src/lib/withLocale.ts
173+
174+
// BEFORE - adds redundant query param
175+
export const hrefWithLocale = (href: string, locale: string) => {
176+
if (locale === 'cn') {
177+
return `/cn${href}?locale=${locale}`; // Redundant!
178+
}
179+
return href;
180+
};
181+
182+
// AFTER - path only
183+
export const hrefWithLocale = (href: string, locale: string) => {
184+
if (locale === 'cn') {
185+
return `/cn${href}`;
186+
}
187+
return href;
188+
};
189+
```
190+
191+
### Step 5: Update Next.js Config
192+
193+
```ts
194+
// apps/www/next.config.ts
195+
196+
// REMOVE these rewrites:
197+
rewrites: async () => {
198+
return [
199+
{ source: '/cn', destination: '/?locale=cn' }, // DELETE
200+
{ source: '/cn/:path*', destination: '/:path*?locale=cn' }, // DELETE
201+
];
202+
},
203+
204+
// ADD these redirects for old URLs:
205+
redirects: async () => {
206+
return [
207+
// ...existing redirects...
208+
209+
// Redirect old ?locale=cn URLs to /cn/* paths
210+
{
211+
source: '/',
212+
has: [{ type: 'query', key: 'locale', value: 'cn' }],
213+
destination: '/cn',
214+
permanent: true,
215+
},
216+
{
217+
source: '/docs/:path*',
218+
has: [{ type: 'query', key: 'locale', value: 'cn' }],
219+
destination: '/cn/docs/:path*',
220+
permanent: true,
221+
},
222+
];
223+
},
224+
```
225+
226+
### Step 6: Fix Home Page (Also Uses searchParams!)
227+
228+
```tsx
229+
// apps/www/src/app/(app)/page.tsx
230+
231+
// BEFORE
232+
export default async function IndexPage({
233+
searchParams,
234+
}: {
235+
searchParams: SearchParams;
236+
}) {
237+
const locale = ((await searchParams).locale || 'en') as keyof typeof i18n;
238+
// ...
239+
}
240+
241+
// AFTER - remove searchParams, default to 'en'
242+
export const dynamic = 'force-static';
243+
244+
export default async function IndexPage() {
245+
const locale = 'en'; // English home page
246+
// ...
247+
}
248+
249+
// Create separate /cn/page.tsx for Chinese home
250+
```
251+
252+
## Acceptance Criteria
253+
254+
- [x] Remove `searchParams` from docs page props
255+
- [x] Remove `searchParams` from home page props
256+
- [x] Create `/cn/docs/[[...slug]]/page.tsx` with proper `generateStaticParams`
257+
- [x] Create `/cn/page.tsx` for Chinese home
258+
- [x] Update `useLocale.ts` to use `usePathname()`
259+
- [x] Update `withLocale.ts` to remove query params
260+
- [x] Update language dropdown to use path-only navigation
261+
- [x] Replace rewrites with redirects in next.config.ts
262+
- [ ] Verify `x-vercel-cache: HIT` header after deploy
263+
- [ ] Verify cache hit rate > 90% in Vercel dashboard
264+
- [ ] Verify costs return to ~$20/month baseline
265+
266+
## Testing Checklist
267+
268+
- [x] Verify build output shows pages as "Static" not "Dynamic"
269+
- [ ] Test language switcher navigates to `/cn/*` without query params
270+
- [ ] Test internal doc links preserve locale context
271+
- [ ] Verify 301 redirects work for old `?locale=cn` URLs
272+
- [ ] Check no hydration warnings in browser console
273+
274+
## Alternative Approaches Considered
275+
276+
| Approach | Pros | Cons |
277+
|----------|------|------|
278+
| Path segments (chosen) | Full caching, SEO-friendly | Route restructuring needed |
279+
| Cookies for locale | No URL changes | Still dynamic, no caching |
280+
| ISR with short TTL | Quick fix | Still hits origin frequently |
281+
| Remove CN support | Simplest | Loses Chinese users |
282+
283+
## Risk Analysis
284+
285+
- **Low risk:** Route restructuring is well-documented Next.js pattern
286+
- **Migration:** Old `?locale=cn` URLs need 301 redirects to preserve SEO
287+
- **Testing:** Verify both locales render correctly before deploy
288+
289+
## References
290+
291+
### Internal Files
292+
- [apps/www/src/app/(app)/docs/[[...slug]]/page.tsx](apps/www/src/app/(app)/docs/[[...slug]]/page.tsx) - Docs page
293+
- [apps/www/src/app/(app)/page.tsx](apps/www/src/app/(app)/page.tsx) - Home page
294+
- [apps/www/src/hooks/useLocale.ts](apps/www/src/hooks/useLocale.ts) - Client locale hook
295+
- [apps/www/src/lib/withLocale.ts](apps/www/src/lib/withLocale.ts) - Locale link helper
296+
- [apps/www/next.config.ts](apps/www/next.config.ts) - Next.js config
297+
298+
### External Documentation
299+
- [Next.js i18n Routing](https://nextjs.org/docs/app/building-your-application/routing/internationalization)
300+
- [Vercel React Best Practices - Server Caching](/.claude/rules/vercel-react-best-practices/AGENTS.md#33-cross-request-lru-caching)
301+
- [Next.js Static Generation](https://nextjs.org/docs/app/building-your-application/rendering/server-components#static-rendering-default)

0 commit comments

Comments
 (0)