Next.js の root layout で Supabase Auth を Server Component にしたら全ページ Edge cache が死んだ話
結論:cookies() を呼ぶ Server Component を root layout に置くと全ページが dynamic になる
3 行サマリー:
createClient() + cookies() + supabase.auth.getUser()を Server Component で実行すると Next.js が「ユーザー固有コンテンツ」と判定する- root layout に常駐する
AuthButtonがこれをやっていたため、配下の全ページがcache-control: private, no-cache, no-storeになった AuthButtonを 35 行 client component 化するだけで全 99 記事が Edge cache 対象に戻った
今すぐ確認するチェックリスト:
-
layout.tsx直下にasyncな Server Component があってcookies()を読んでいないか - Vercel の build output で記事 / LP ページが
●(SSR Function)になっていないか -
cache-control: privateが返ってきていないか(curl -I https://your-site.com/articles/xxx)
何が起きたか
masatoman.net を Vercel にデプロイして数週間後のことです。
Vercel の Usage ページを開いたら、Fluid Active CPU が 当日分をほぼ使い切っている状態を発見しました。13 プロジェクトを同じ team で管理していますが、消費しているのは masatoman.net のみ。他 12 プロジェクトの合計よりも多い。
「ブログサイトがなぜこんなに CPU を使うのか?」
Vercel のログを確認すると、攻撃 bot(/wp-admin/install.php 系)とクローラーが 1 秒 30 リクエスト近いペースで叩いていました。ただし bot 対策の前に「なぜ静的 HTML を返さずに毎回 SSR しているのか」を先に解決すべきだと気づきました。
curl -I https://www.masatoman.net/articles/stripe-webhook-naked-www-redirect-silent-failure-2026 を打つと:
cache-control: private, no-cache, no-store, max-age=0, must-revalidate
ブログ記事に private, no-store が返っています。これは CDN も Edge も一切キャッシュしない設定です。
根本原因の特定
Next.js の App Router では、cookies() / headers() を呼ぶ Server Component が 1 つでも含まれると、そのルートのレンダリングが dynamic になります。
問題は「root layout に常駐する AuthButton が Server Component で cookies を読んでいた」ことでした。
// 修正前 — src/components/AuthButton.tsx(問題のあるパターン)
// Server Component として動作していた
import { createClient } from '@/lib/supabase/server'
export default async function AuthButton() {
const supabase = await createClient() // cookies() を内部で呼ぶ
const { data: { user } } = await supabase.auth.getUser()
return user ? (
<span>ログイン中: {user.email}</span>
) : (
<a href="/login">ログイン</a>
)
}
createClient() の中で @supabase/ssr の createServerClient が cookies() を呼んでいます。これにより:
root layout (Server)
└── Header (Server)
└── AuthButton (Server) ← cookies() を呼ぶ
Next.js のキャッシュ判定:
cookies()を含む → 「リクエストごとに異なる可能性」と判断- root layout に伝播 → 配下の全ルートが dynamic になる
Build output 確認:
npm run build
Route (app) Size First Load JS
┌ ○ / ...
├ ● /articles/[slug] ← ●(SSR Function)に!
├ ● /articles/stripe-webhook-...
...
○ (Static) prerendered as static content
● (Dynamic) server-rendered on demand
99 本の記事ページが全部 ● になっていました。generateStaticParams で ISR 対象にしていたはずの記事も全滅。
修正:AuthButton を Client Component 化
Server Component で認証状態を取る必要はありません。ログイン状態の確認は client side から Supabase Auth の onAuthStateChange か getSession() で十分です。
// 修正後 — src/components/AuthButton.tsx
'use client'
import { useEffect, useState } from 'react'
import { createBrowserClient } from '@supabase/ssr'
import type { User } from '@supabase/supabase-js'
export default function AuthButton() {
const [user, setUser] = useState<User | null>(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
const supabase = createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
supabase.auth.getSession().then(({ data: { session } }) => {
setUser(session?.user ?? null)
setLoading(false)
})
const { data: { subscription } } = supabase.auth.onAuthStateChange(
(_event, session) => {
setUser(session?.user ?? null)
}
)
return () => subscription.unsubscribe()
}, [])
if (loading) return null
return user ? (
<span className="text-sm text-gray-600">ログイン中</span>
) : (
<a href="/login" className="text-sm text-blue-600 hover:underline">
ログイン
</a>
)
}
変更点:
'use client'を追加(35 行程度のファイル全体)- Server 側の
createClient()→createBrowserClient()に変更 async/await→useEffect+useStateに変更
修正後の確認
Build output:
Route (app) Size First Load JS
┌ ○ / ...
├ ○ /articles/[slug] ← ○(Static)に!
├ ○ /articles/stripe-webhook-...
...
全記事ページが ○(Static)になりました。
本番デプロイ後のレスポンスヘッダー確認:
curl -I https://www.masatoman.net/articles/stripe-webhook-naked-www-redirect-silent-failure-2026
cache-control: public, max-age=0, must-revalidate
age: 249
x-vercel-cache: HIT
age: 249 — 249 秒前にキャッシュされたレスポンスが返っています。x-vercel-cache: HIT で Edge から配信されています。
なぜ気づきにくいか
この問題が厄介なのは、ローカル開発では症状が出ないことです。
npm run dev では Next.js がキャッシュ最適化を一部スキップするため、cache-control: private は出ません。npm run build && npm run start でも確認しにくい。
症状が顕在化するのは:
- Vercel にデプロイして CPU 使用量が跳ね上がる
curl -Iで本番のcache-controlヘッダーを確認するnpm run buildの出力で●が増えていることに気づく
3 番が最も早い気づき方です。新しいコンポーネントを追加したときは、必ず build output の ○/● を確認する習慣をつけると防げます。
同じ罠のパターン一覧
よくある「知らずに dynamic を広げるパターン」:
| コンポーネント | 問題 | 対策 |
|---|---|---|
AuthButton(Server) | cookies() で dynamic | Client Component 化 |
ThemeToggle(Server) | cookies() でテーマ読み込み | Client Component 化 |
LocaleSwitcher(Server) | headers() で Accept-Language | Client Component 化 |
Analytics(Server) | headers() で IP 取得 | Client Component 化 or middleware |
| Supabase RealTime(Server) | WebSocket でレンダリング強制 | Client Component 化 |
原則: root layout やその直下 Header/Footer に cookies() / headers() を読む Server Component を置かない。
読む必要があるなら Middleware(middleware.ts)で処理して、コンポーネントには最終結果だけを渡す設計にします。
今日やること(3 つ)
- build output を確認する:
npm run buildして●のページを全部洗い出す - root layout のコンポーネントを確認する:
cookies()/headers()を呼んでいる Server Component がないかチェック - 本番の
cache-controlを確認する:curl -I https://your-domain.com/articles/xxxでprivateが返っていたら要修正
masatoman.net は現在 Phase 0(最初の読者ベース構築中)。Next.js 16 + Supabase + Vercel 構成で運用中。今回の AuthButton 修正を実施し、全 99 記事ページが Edge cache 対象になったことを build output と本番ヘッダーで確認済み。GA4 と Search Console 連携済みでベースライン計測中。
で、どう稼ぐ?
パフォーマンスを改善した記事は SEO で上位表示されやすくなります(Google は Core Web Vitals + TTFB を評価します)。
masatoman.net のようなブログ型個人開発サイトでは:
- 記事が Edge cache されると: bot / クローラーへの CPU 消費が激減 → Vercel Free プランで継続運用可能
- 記事ページの TTFB が改善: Google のクロール効率が上がり、インデックス速度が改善
- インデックス速度が改善: 新記事への有料記事 CTA への流入が増える
今回の修正は「コスト削減」と「SEO 改善」を同時に実現します。無料プランで運用を続けながら有料記事収益を積み上げるための基盤です。
Supabase Auth + Next.js の実装詳細(有料コンテンツゲート・サブスク管理との組み合わせ)は Lab で解説予定です。
masatoman のメルマガ — 毎週月曜の朝に手紙を 1 通
masatoman.net の今週の記事 1 本を、読者目線で深掘りした手紙が毎週月曜 9:00 に届きます。「これ自分のことだ」が見つかる予告編。登録特典に「個人開発の収益化チェックリスト 15 項目」。
masatoman のメルマガ — 毎週月曜の朝に 1 通
masatoman.net で今週公開した記事の中から 1 本を、読者目線で深掘りした手紙が届きます。「自分も同じことやってる」「ここで詰まってた」が見つかる予告編。
Next Step
次に読むならこの導線です
【第12回】夜寝てる間に Claude Code が記事を書き上げる構成 — 月 ¥5K で動く全コード
Claude Codeラボ全12話の集大成。Skills/MCP/サブエージェント/Hooks/リモート運用を統合した「自走する Claude 自動化」を、月 ¥5K の実コストで動かす全構成を公開。寝てる間に競合調査・記事下書き・PR まで自動化する 6 層アーキテクチャの完成版。
【第10回】Claude Code × Supabase で管理画面を30分で生成する Skill 実装ガイド
Supabase のテーブルを参照して管理画面を自動生成する Skill を、実コード付きで解説。Claude Code が DB スキーマから Next.js 管理画面を30分で生成する仕組みを公開します。
【第12回】夜寝てる間に Claude Code が記事を書き上げる構成 — 月 ¥5K で動く全コード
Claude Codeラボ全12話の集大成。Skills/MCP/サブエージェント/Hooks/リモート運用を統合した「自走する Claude 自動化」を、月 ¥5K の実コストで動かす全構成を公開。寝てる間に競合調査・記事下書き・PR まで自動化する 6 層アーキテクチャの完成版。
Claude Code で 0→MVP を1日で作る全記録 — recipe-ai Build in Public
Claude Codeを使い、YouTube料理動画からレシピを自動抽出するAIアプリ「recipe-ai」を0からMVPまで1日で構築した全記録。CLAUDE.md設計、API実装、Supabase連携、Vercelデプロイ、Stripe課金導入までの工程を時系列で公開。
次の実験記録も追う
Claude Code × 個人開発の実験ログ、失敗、判断変更をまとめて追いたい人向けに、月次でLab Freeを届けます。
masatoman のメルマガ — 毎週月曜の朝に 1 通
masatoman.net で今週公開した記事の中から 1 本を、読者目線で深掘りした手紙が届きます。「自分も同じことやってる」「ここで詰まってた」が見つかる予告編。
この記事が役に立ったらシェア