Next.js 静的エクスポート徹底解説:200+ ページの高性能ツールサイトの構築
静的エクスポートの仕組み
Next.js の output: "export" はアプリ全体を純粋な静的 HTML に事前レンダリングし、Node.js ランタイムは不要です。
next build (SSG)
↓
すべての generateStaticParams() を走査
↓
各 [locale] × [slug] の組み合わせで HTML を事前生成
↓
out/ ディレクトリへ出力
↓
OSS/CDN にアップロード
SSG vs SSR vs ISR
| モード | サーバー要否 | ビルド時レンダリング | 更新方法 | 向いている用途 |
|---|---|---|---|---|
| SSG (export) | 不要 | ✅ | 再ビルド&デプロイ | 内容が固定のツールサイト |
| SSR | 必要 | ❌ | リクエストごとにレンダリング | パーソナライズされたコンテンツ |
| ISR | 必要 | ✅ | バックグラウンドで段階的再生成 | 頻繁に更新されるコンテンツ |
ToolsKu が SSG を選んだ理由:
- 200+ のツールページは完全に静的で動的データが不要
- 阿里云 OSS + CDN にデプロイし、運用コストゼロ
- 初回 TTFB が極めて低い(CDN が HTML を直接返す)
- オフラインキャッシュに自然に適合
generateStaticParams の設計パターン
多言語 × ツールページの直積
// 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 は layout.generateStaticParams × page.generateStaticParams の直積を自動計算し、各 (locale, path) 組み合わせの HTML を生成します。
動的ルート:ブログ記事の列挙
// src/app/[locale]/blog/[slug]/page.tsx
export function generateStaticParams() {
return getStaticParamsForAllPosts().map(({ slug, locale }) => ({
locale,
slug,
}));
}
getStaticParamsForAllPosts() は src/content/blog/ をスキャンし、すべての slug × 利用可能な言語を列挙して、各記事の各言語版を事前レンダリングします。
ビルド性能の最適化
大規模サイトでのビルド課題
- 292 ツールページ × 4 言語 = 1168 HTML ページ
- ブログ・カテゴリページなどを含め 1300+ 静的ページ
- 各ページで
getTranslations()により i18n メッセージを読み込み
並行度の制御
// next.config.ts
experimental: {
staticGenerationRetryCount: 2,
...(isCi
? {
staticGenerationMaxConcurrency: 4, // CI: 4 並列
staticGenerationMinPagesPerWorker: 32, // worker あたり 32 ページ
}
: {
staticGenerationMaxConcurrency: 16, // ローカル: 16 並列
staticGenerationMinPagesPerWorker: 8,
}),
}
- CI 環境:GitHub Actions のメモリが限られるため、OOM 回避のため並行度を下げる
- ローカル開発:並行度を上げてビルドを高速化
タイムアウト設定
staticPageGenerationTimeout: 180, // 3 分
一部のツールページ(動画ツールなど)は WASM 依存が多く、事前レンダリングがデフォルトの 60 秒を超えることがあります。
多言語ルーティングアーキテクチャ
next-intl + App Router の統合
src/i18n/routing-config.ts
→ locales: ['zh-CN', 'zh-TW', 'en', 'ja']
→ localePrefix: 'always' ← すべての URL に言語プレフィックス
→ localeDetection: true ← Accept-Language による自動協調
URL 構造
https://www.toolsku.com/zh-CN/pdf/merge/ ← 簡体字中国語
https://www.toolsku.com/en/pdf/merge/ ← 英語
https://www.toolsku.com/zh-CN/blog/pdf-merge-guide/ ← ブログ
hreflang の実装
各ページに完全な hreflang タグを注入:
<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/" />
Sitemap の自動検出
ToolsKu の sitemap.ts はファイルシステムスキャンで全ページを自動検出します。
const pathEntries = discoverLocaleInnerPathsWithFilePath(appDir);
// src/app/[locale]/ 配下のすべての page.tsx を自動検出
動的セグメントのスキップ
discoverLocaleInnerPaths は設計上動的セグメント([slug] など)をスキップします。パラメータがないとレンダリングできないためです。
ブログ記事は sitemap.ts で別途処理します:
const blogSlugs = getAllBlogSlugs();
for (const slug of blogSlugs) {
entries.push({
url: absoluteLocalizedUrl(locale, `/blog/${slug}`),
lastModified: meta.updatedAt,
priority: 0.7,
alternates: { languages },
});
}
CDN デプロイアーキテクチャ
アセットプレフィックス (Asset Prefix)
// next.config.ts
assetPrefix: getAssetPrefix(),
// → 本番は https://toolsku.oss-cn-beijing.aliyuncs.com/public
ビルド成果物の _next/static/ 配下の JS/CSS は CDN を指し、HTML は OSS から直接配信されます。
デプロイフロー
next build → out/
↓
ossutil cp out/ oss://toolsku/ --recursive
↓
CDN キャッシュ無効化(または TTL 待ち)
↓
新バージョン公開
キャッシュ戦略
| リソース種別 | キャッシュ戦略 | 理由 |
|---|---|---|
_next/static/**/*.js |
Cache-Control: max-age=31536000, immutable | ファイル名に hash |
*.html |
Cache-Control: max-age=3600, s-maxage=86400 | ページは更新されうる |
*.wasm |
Cache-Control: max-age=31536000 | WASM バージョンは安定 |
i18n/*.json |
Cache-Control: max-age=86400 | 検索インデックスは定期更新 |
ビルドパイプライン
yarn build
→ generate-tools-search-index.mjs ← ツール検索インデックス生成
→ generate-tool-clients-public.mjs ← ツールクライアントデータ生成
→ next build ← 全ページを SSG 事前レンダリング
→ generateStaticParams() を走査
→ 各 (locale, path) をレンダリング
→ out/ に出力
インクリメンタルビルドの課題
Next.js の静的エクスポートは現時点でインクリメンタルビルド非対応です。next build のたびに全ページを再レンダリングします。1300+ ページではフルビルドに 3〜5 分かかります。
対策:
- CI では並行度を下げてメモリピークを抑制
- ローカル開発は
devモード(オンデマンドレンダリング、事前生成なし) - Turbopack で開発モードを高速化
まとめ
| 決定事項 | 選択 | 理由 |
|---|---|---|
| レンダリング | SSG (export) | 運用ゼロ、極低 TTFB |
| ルーティング | App Router | Server Components、優れたコード分割 |
| 国際化 | next-intl | App Router ネイティブ、充実したメッセージ |
| コンテンツ | リポジトリ (JSON/TS) | CMS 不要、Git でバージョン管理 |
| デプロイ | OSS + CDN | 中国国内で高速、サーバー運用不要 |
純粋な静的エクスポートはツール系サイトに最適です。内容が固定でアクセスが多く、初回表示速度が重要だからです。Next.js App Router + SSG により、ToolsKu はサーバー運用なしで高性能なデプロイを実現しています。
ブラウザローカルツールを無料で試す →