Next.js Static Export Deep Dive: Building a 200+ Page High-Performance Tool Site
How Static Export Works
Next.js output: "export" pre-renders the entire app to pure static HTML with no Node.js runtime:
next build (SSG)
↓
Traverse all generateStaticParams()
↓
Pre-render HTML for each [locale] × [slug] combination
↓
Output to out/
↓
Upload to OSS/CDN
SSG vs SSR vs ISR
| Mode | Server required | Build-time render | Updates | Best for |
|---|---|---|---|---|
| SSG (export) | No | ✅ | Rebuild & redeploy | Fixed-content tool sites |
| SSR | Yes | ❌ | Render per request | Personalized content |
| ISR | Yes | ✅ | Background incremental revalidation | Frequently updated content |
Why ToolsKu chose SSG:
- 200+ tool pages are fully static—no dynamic data
- Deploy to Alibaba Cloud OSS + CDN with zero ops overhead
- Extremely low first-byte TTFB (CDN serves HTML directly)
- Natural fit for offline caching
generateStaticParams Design Patterns
Locale × tool page Cartesian product
// src/app/[locale]/layout.tsx
export function generateStaticParams() {
return [
{ locale: 'zh-CN' },
{ locale: 'zh-TW' },
{ locale: 'en' },
{ locale: 'ja' },
];
}
// src/app/[locale]/pdf/merge/page.tsx
export function generateStaticParams() {
return [
{ locale: 'zh-CN' },
{ locale: 'zh-TW' },
{ locale: 'en' },
{ locale: 'ja' },
];
}
Next.js automatically computes the Cartesian product of layout.generateStaticParams × page.generateStaticParams, generating HTML for every (locale, path) pair.
Dynamic routes: enumerating blog posts
// src/app/[locale]/blog/[slug]/page.tsx
export function generateStaticParams() {
return getStaticParamsForAllPosts().map(({ slug, locale }) => ({
locale,
slug,
}));
}
getStaticParamsForAllPosts() scans src/content/blog/, enumerating every slug × available locale so each article version is pre-rendered.
Build Performance Optimization
The build challenge at scale
- 292 tool pages × 4 locales = 1,168 HTML pages
- Plus blog, category pages, etc.—1,300+ static pages total
- Each page calls
getTranslations()to load i18n messages
Concurrency control
// next.config.ts
experimental: {
staticGenerationRetryCount: 2,
...(isCi
? {
staticGenerationMaxConcurrency: 4, // CI: 4 concurrent
staticGenerationMinPagesPerWorker: 32, // 32 pages per worker
}
: {
staticGenerationMaxConcurrency: 16, // Local: 16 concurrent
staticGenerationMinPagesPerWorker: 8,
}),
}
- CI: GitHub Actions has limited memory—lower concurrency to avoid OOM
- Local dev: Raise concurrency to speed up builds
Timeout settings
staticPageGenerationTimeout: 180, // 3 minutes
Some tool pages (e.g. video tools) load heavy WASM dependencies; pre-render can exceed the default 60s.
Multilingual Routing Architecture
next-intl + App Router integration
src/i18n/routing-config.ts
→ locales: ['zh-CN', 'zh-TW', 'en', 'ja']
→ localePrefix: 'always' ← All URLs include locale prefix
→ localeDetection: true ← Accept-Language negotiation
URL structure
https://www.toolsku.com/zh-CN/pdf/merge/ ← Simplified Chinese
https://www.toolsku.com/en/pdf/merge/ ← English
https://www.toolsku.com/zh-CN/blog/pdf-merge-guide/ ← Blog
hreflang implementation
Every page injects complete hreflang tags:
<link rel="alternate" hreflang="zh-CN" href="https://www.toolsku.com/zh-CN/pdf/merge/" />
<link rel="alternate" hreflang="zh-TW" href="https://www.toolsku.com/zh-TW/pdf/merge/" />
<link rel="alternate" hreflang="en" href="https://www.toolsku.com/en/pdf/merge/" />
<link rel="alternate" hreflang="ja" href="https://www.toolsku.com/ja/pdf/merge/" />
<link rel="alternate" hreflang="x-default" href="https://www.toolsku.com/zh-CN/pdf/merge/" />
Automatic Sitemap Discovery
ToolsKu's sitemap.ts uses filesystem scanning to discover all pages:
const pathEntries = discoverLocaleInnerPathsWithFilePath(appDir);
// Auto-discover all page.tsx under src/app/[locale]/
Skipping dynamic segments
discoverLocaleInnerPaths intentionally skips dynamic segments (e.g. [slug]) because they need parameters to render.
For blog posts, sitemap.ts handles them separately:
const blogSlugs = getAllBlogSlugs();
for (const slug of blogSlugs) {
entries.push({
url: absoluteLocalizedUrl(locale, `/blog/${slug}`),
lastModified: meta.updatedAt,
priority: 0.7,
alternates: { languages },
});
}
CDN Deployment Architecture
Asset prefix
// next.config.ts
assetPrefix: getAssetPrefix(),
// → Production points to https://toolsku.oss-cn-beijing.aliyuncs.com/public
Built assets under _next/static/ reference the CDN; HTML is served directly from OSS.
Deployment flow
next build → out/
↓
ossutil cp out/ oss://toolsku/ --recursive
↓
CDN invalidation (or wait for TTL)
↓
New version live
Caching strategy
| Resource type | Cache policy | Rationale |
|---|---|---|
_next/static/**/*.js |
Cache-Control: max-age=31536000, immutable | Filenames include hash |
*.html |
Cache-Control: max-age=3600, s-maxage=86400 | Pages may update |
*.wasm |
Cache-Control: max-age=31536000 | WASM versions are stable |
i18n/*.json |
Cache-Control: max-age=86400 | Search index updates periodically |
Build Pipeline
yarn build
→ generate-tools-search-index.mjs ← Generate tool search index
→ generate-tool-clients-public.mjs ← Generate tool client data
→ next build ← SSG pre-render all pages
→ Traverse generateStaticParams()
→ Render each (locale, path) pair
→ Output to out/
The incremental build gap
Next.js static export does not support incremental builds yet—every next build re-renders all pages. For 1,300+ pages, a full build takes 3–5 minutes.
Mitigations:
- Lower CI concurrency to reduce memory peaks
- Use
devmode locally (on-demand render, no pre-generation) - Turbopack speeds up development mode
Lessons Learned
| Decision | Choice | Rationale |
|---|---|---|
| Rendering | SSG (export) | Zero ops, minimal TTFB |
| Routing | App Router | Server Components, better code splitting |
| i18n | next-intl | Native App Router support, mature message system |
| Content | Repo (JSON/TS) | No CMS dependency, Git versioning |
| Hosting | OSS + CDN | Fast in China, zero server maintenance |
Pure static export is the best fit for tool sites—fixed content, high traffic, and a need for blazing first paint. Next.js App Router + SSG lets ToolsKu ship high performance with no server to operate.
Try these browser-local tools — no sign-up required →