背景
業種別のランディングページ(LP)を複数業種ごとに作っている。もともとSSR(サーバーサイドレンダリング)で動かす前提で設計していたが、今日SSG(静的サイト生成)に切り替えた。
なぜSSRにしていたかというと、サブドメインで業種を判定する仕組み(industryA.example.com → 業種A向けコンテンツ)を使っていて、サブドメインの読み取りにはサーバー側の処理が必要だったから。ローカル開発でも localhost ひとつ立ち上げれば複数業種を切り替えて確認できる、という開発上の利便性もあった。
SSGに切り替えた理由
SEO的にはSSRもSSGも差がない。どちらもHTMLが事前に生成されるので、クローラーから見れば同じこと。
SSRを続ける場合、Cloudflare Workers上でNuxt Contentを動かすためにD1(CloudflareのSQLiteサービス)が必要になる。LPサイトは更新頻度が低い静的コンテンツが中心なので、わざわざD1をセットアップして維持する意味がない。
判断のポイントはシンプルさだった。D1のバインディング設定、Workers Paidプランの管理、データベースのマイグレーション -- これらの運用コストを考えると、SSGで静的HTMLを生成してCloudflare Pagesに置くだけの方がずっとシンプル。複数業種分のビルドをワンコマンドで回せるなら、開発体験も変わらない。
実装の変更点
変更は実質3箇所だけだった。
1. useSubdomain composableの環境変数フォールバック
SSRではリクエストのHostヘッダーからサブドメインを読み取っていた。SSGではビルド時にHostヘッダーが存在しないので、環境変数 NUXT_PUBLIC_INDUSTRY から業種を取得するフォールバックを追加した。
// composables/useSubdomain.ts(修正後のイメージ)
export const useSubdomain = () => {
const config = useRuntimeConfig()
const event = useRequestEvent()
// 1. 環境変数(SSGビルド時に使われる)
const envIndustry = config.public.industry as string
// 2. サーバーミドルウェアが設定したサブドメイン
const subdomainFromEvent = event?.context?.subdomain as string
// 3. フォールバック順序: 環境変数 → サブドメイン → デフォルト
const industry = envIndustry || subdomainFromEvent || 'industryA'
return { industry }
}
ここで重要だったのは ??(Nullish Coalescing)ではなく ||(OR)を使うこと。?? は null と undefined だけをスキップするが、空文字列 "" はそのまま通してしまう。環境変数が空文字列で設定されるケースがあるため、|| で falsy な値すべてをスキップする必要があった。
2. nuxt.config.tsのpreset変更
// nuxt.config.ts
export default defineNuxtConfig({
nitro: {
// SSR時: cloudflare_pages(Workers Runtime)
// SSG時: cloudflare_pages_static(静的ファイルのみ)
preset: 'cloudflare_pages_static'
},
runtimeConfig: {
public: {
industry: '' // NUXT_PUBLIC_INDUSTRY 環境変数から自動で上書きされる
}
}
})
cloudflare_pages から cloudflare_pages_static に変えるだけ。Nuxtの規約で、runtimeConfig.public.industry は環境変数 NUXT_PUBLIC_INDUSTRY から自動的にマッピングされるので、追加の設定は不要。
3. デプロイスクリプトの作成
各業種それぞれで環境変数を切り替えてビルドし、対応するCloudflare Pagesプロジェクトにデプロイするスクリプトを作った。PowerShellとBashの両方を用意した。
# scripts/deploy.ps1(簡略版)
param(
[string]$Industry = ""
)
$industries = @(
@{ name = "industryA"; project = "tax-lp-industryA" },
@{ name = "industryB"; project = "tax-lp-industryB" },
@{ name = "industryC"; project = "tax-lp-industryC" }
# ... 各業種分を定義
)
# 引数があれば1業種だけ、なければ全業種
if ($Industry) {
$targets = $industries | Where-Object { $_.name -eq $Industry }
} else {
$targets = $industries
}
foreach ($ind in $targets) {
Write-Host "[Build] $($ind.name)..."
$env:NUXT_PUBLIC_INDUSTRY = $ind.name
# キャッシュ削除 → ビルド → デプロイ
Remove-Item -Recurse -Force .nuxt, .data, dist -ErrorAction SilentlyContinue
npx nuxi generate
npx wrangler pages deploy dist --project-name $ind.project --branch main
}
使い方は2パターン。
# 1業種だけ
powershell -ExecutionPolicy Bypass -File scripts/deploy.ps1 -Industry industryA
# 全業種まとめて
powershell -ExecutionPolicy Bypass -File scripts/deploy.ps1
Bash版も同じ構造で作った。
#!/bin/bash
# scripts/deploy.sh
INDUSTRIES=("industryA" "industryB" "industryC") # 各業種名を定義
for ind in "${INDUSTRIES[@]}"; do
echo "[Build] $ind..."
rm -rf .nuxt .data dist
NUXT_PUBLIC_INDUSTRY=$ind npx nuxi generate
npx wrangler pages deploy dist --project-name "tax-lp-$ind" --branch main
done
Cloudflare Pagesプロジェクトの作成
複数のプロジェクトをCLIから一括で作成した。
for ind in industryA industryB industryC; do
npx wrangler pages project create "tax-lp-$ind" --production-branch main
done
GitHubとの連携は使わない方針にした。ローカルでビルドして wrangler pages deploy で直接デプロイする。理由は、GitHub Actionsのビルド設定を業種ごとに管理するよりも、手元で deploy.ps1 を叩く方がシンプルだから。LPサイトは頻繁に更新するものではないし、ビルド時間も短い。
ハマったところ
SSGプリレンダリング時のサブドメイン判定バグ
全業種を一通りデプロイした後、全サイトが業種Aのコンテンツになっていた。NUXT_PUBLIC_INDUSTRY=industryB でビルドしても結果は同じ。
原因は server/middleware/subdomain.ts にあった。SSGの nuxt generate 実行時にもサーバーミドルウェアが動作する。プリレンダリング時のHostヘッダーは localhost なので、ミドルウェアのフォールバックロジックが常にデフォルト業種を返していた。
そして useSubdomain.ts 側では、ミドルウェアが設定した値が環境変数より先に評価されていた。結果として、環境変数で別の業種を指定していても、ミドルウェアが先にデフォルト業種を入れてしまい、そちらが使われていた。
修正は2点。
useSubdomain.tsの優先順位を変更 -- 環境変数を最優先にした??を||に変更 -- 空文字列もフォールバックの対象にした
この修正後、各業種のコンテンツが正しく生成されるようになった。
--branch main の指定漏れ
Cloudflare Pagesでは、プロジェクト作成時に --production-branch main を指定しても、デプロイ時に --branch main を付けないとプレビューデプロイ扱いになる。本番URL(tax-lp-{業種名}.pages.dev)が404を返し、個別のデプロイURL(d1936697.tax-lp-{業種名}.pages.dev)は200を返すという状況だった。
wrangler pages deploy dist --project-name tax-lp-{業種名} --branch main のように、デプロイコマンドにも --branch main を明示する必要がある。
PowerShellからのwrangler実行問題
PowerShellから npx wrangler を実行すると Could not determine Node.js install directory というエラーが出ることがあった。Volta(Node.jsバージョン管理ツール)経由の wrangler がPowerShell環境でうまく動かないケースがある。
Git Bashからは問題なく動作したので、最終的にはBash版のデプロイスクリプトも用意して対応した。PowerShell版も $ErrorActionPreference = "Stop" を削除することで、stderrの出力を致命的エラーとして扱わないようにして改善した。
SQLiteロック
業種を連続でビルドする際、前のビルドプロセスが .data/contents.sqlite をロックしたままになることがあった。次の業種のビルドが「データベースがロックされている」とエラーを出す。
対策として、各ビルドの前に .nuxt と .data ディレクトリを削除するステップを入れた。これはmdx-playgroundのデプロイスクリプトでも同様の対策をしていたので、同じアプローチを採用した。
最終的な構成
Cloudflare Pages プロジェクト:
tax-lp-{業種名} → {業種名}.example.com (業種ごとにプロジェクトを作成)
デプロイフロー:
ローカル → NUXT_PUBLIC_INDUSTRY=xxx nuxi generate → wrangler pages deploy → Cloudflare
GitHub連携なし。ローカルビルド、直接デプロイ。サーバーサイドの処理はゼロ。D1も不要。
今日の判断のまとめ
SSR → SSGの移行は「SSGでできるならSSGにしておく」という方針に基づいている。LPサイトに動的なサーバー処理は必要ない。環境変数でテナントを切り替え、業種ごとにビルドして静的ファイルをデプロイするだけ。
変更量は小さかった(composable 1ファイル、nuxt.config.ts 2行、デプロイスクリプト新規作成)が、SSGプリレンダリング時のミドルウェア動作という落とし穴があった。SSRで動いていたコードをSSGに持っていくときは、ビルド時にもサーバーミドルウェアが走ることを意識しておく必要がある。