DB型サイトを「一覧だけ会員限定・個別ページは残す」に切り替える指示書(Next.js App Router)
自社DBで集客していたサイトが競合のリスト抜き取りに気づいた時の改修手順。名前が並ぶバルクな一覧だけを会員限定にし、個別ページはtitle/H1とCTAを残す。ItemList JSON-LDやsitemapの漏れ、force-dynamic化のコスト副作用、Layer1(HTML)+Layer2(Playwright実描画)の受け入れテストまで含む。
約17.8万トークンの節約 (API料金換算で約270円分)。 要件定義・技術調査・試行錯誤ぶんのトークンがまるごと不要になります。※ 出品者申告とレビューに基づく推定値。モデル・タスク内容により変動します。
この巻物について
「DB型サイトを「一覧だけ会員限定・個別ページは残す」に切り替える指示書(Next.js App Router)」は、Web開発カテゴリのAI指示書(MDファイル)です。自社DBで集客していたサイトが競合のリスト抜き取りに気づいた時の改修手順。名前が並ぶバルクな一覧だけを会員限定にし、個別ページはtitle/H1とCTAを残す。ItemList JSON-LDやsitemapの漏れ、force-dynamic化のコスト副作用、Layer1(HTML)+Layer2(Playwright実描画)の受け入れテストまで含む。この巻物をAIに読み込ませると、ゼロから設計・調査する場合に比べて 約17.8万トークン(API料金換算で約270円)・85%のトークンを節約できます。
- カテゴリ
- Web開発
- 対応AI
- claude-code、cursor、codex-cli
- ライセンス
- 商用利用可 (再販不可)
- 価格
- 無料
- ゼロから開発時
- 約21万トークン
- この巻物使用時
- 約3.2万トークン
- 節約量
- 約17.8万トークン (約270円)
- 更新日
- 2026-08-20
使い方 (AIに渡す3つの方法)
いちばん簡単なのはワンライナー。Claude Code のターミナルに貼るだけです。
claude "https://makimono-md.vercel.app/api/v1/files/db-next-js-app-router/raw を読み込んで、この指示書どおりに実装して"
中身
データベース型サイトを「一覧だけ会員限定・個別ページは残す」に切り替える指示書(Next.js App Router)
自社DBを公開して集客していたサイトが、競合にリストを丸ごと抜かれるリスクに気づいた時の改修手順。 遮断するのは「名前が並ぶバルクな一覧」だけにして、個別ページは title/H1 と CTA を残す。 これにより「リストは守るが、検索流入の受け皿は捨てない」を両立させる。
対象: Next.js App Router + Postgres 系(Supabase 等)+ Cookie セッション認証。 所要: 実装 1〜2 時間 + 検証 30 分。
0. 前提の確認(ここを飛ばすと的を外す)
改修前に必ず次を実測する。「SEO資産を守る」前提が既に無効化されているケースがある。
curl -s https://<本番ドメイン>/robots.txt
curl -s https://<本番ドメイン>/sitemap.xml | grep -c '<loc>'
curl -s https://<本番ドメイン>/sitemap.xml | grep -c '/<個別ページのパス>/'
- robots.txt で既に個別ページ配下が
Disallowされている、または sitemap から除外済みなら、 その領域の検索流入はすでに意図的に捨てられている。その場合「SEOと両立させる」設計は不要で、 素直に全面ログイン必須にした方が単純。 - コンバージョン実績(問い合わせ件数・会員登録数)も確認する。流入があっても転換ゼロなら 「守るべきSEO資産」の価値はゼロなので、判断が変わる。
1. 会員判定を1箇所に集約する
src/lib/member.ts:
import { createServerClient } from '@/lib/db/server';
/**
* 承認済み会員かどうか。競合としてマークされた会員は false を返す。
* cookie を読むので、これを呼ぶページは動的レンダリングになる。
*/
export async function isApprovedMember(): Promise<boolean> {
const server = await createServerClient();
const { data: { user } } = await server.auth.getUser();
if (!user) return false;
const { data: member } = await server
.from('members')
.select('status, is_competitor')
.eq('auth_user_id', user.id)
.maybeSingle();
return member?.status === 'approved' && member?.is_competitor !== true;
}
既存コードに同等のインライン判定が散っているなら、この関数の呼び出しに置き換える(挙動は変えない)。
2. ゲート表示コンポーネントを1つ作る
src/components/ListMemberGate.tsx(server component で可)。props は
{ redirect: string; count?: number | null; label?: string }。
内容:
- 見出し「<対象>の一覧・検索は会員限定です」
- 件数だけは出す(「該当 1,234 件」)。件数は価値の訴求になり、抜き取りには使えない
- 「無料で会員登録」→
/auth/sign-up?redirect=<redirect> - 「すでに会員の方はログイン」→
/auth/sign-in?redirect=<redirect> - 「登録せずに相談したい方はこちら」→ 問い合わせページ(登録を挟まない逃げ道を必ず用意する)
3. 一覧を出しているページを全部洗い出してゲートする
grep -rl "<一覧カードのコンポーネント名>" src/app
典型的な漏れどころ:
| 場所 | 対応 |
|---|---|
| トップの検索フォーム + facet + 結果 + ページネーション | 会員のみ表示。件数(総掲載数)は残す |
| 属性別ハブ(地域別・カテゴリ別・月別など) | カード一覧のみ差し替え。H1・導入文・内部リンクは残す |
| 締切・新着などの横断ハブ | 同上 |
ItemList JSON-LD | 非会員には出さない(構造化データに名前が並ぶ=一覧の流出) |
sitemap.xml | 個別ページを残すか外すかを明示的に決める |
実装パターン(非会員には名前を取るクエリ自体を実行しないのが重要):
const member = await isApprovedMember();
const { data, count } = member
? await db.from('items').select('id, name, …', { count: 'exact' })
.order('start_date').limit(100)
: await db.from('items').select('id', { count: 'exact', head: true }); // 件数のみ
const items = (data ?? []) as Item[];
- ORM の型が
select(member ? 'a, b, c' : 'id')のような動的文字列を解釈できないことがある (型パーサが union を扱えずParserError型になる)。その場合は三項演算子でクエリごと分岐させる。 head: trueの時dataはnull。if (!data) notFound()のような既存ガードはcountを見る形に直す(直さないと非会員に 404 を返してしまう)。
4. 個別ページは「中身ゼロ・枠は残す」
会員向け表示は一切変えない。非会員に対してだけ落とす。
残すもの: <title> / <h1>(対象の名前)/ 大分類タグ / 問い合わせ CTA / パンくずのトップ階層
落とすもの:
- 属性フィールド(時期・場所・形式・規模など)を丸ごと会員限定セクションへ
- ヘッダのチップ類のうち属性由来のもの
- パンくず・関連リンクのうち属性別ページへのリンク(それ自体が属性の露出になる)
generateMetadataのdescriptionから属性を除去し、名前だけの汎用文にする- 構造化データ(
Event等)はname/url/statusのみ。startDate/location/sameAs/audienceは出さない - FAQ 回答に個別の数値を埋めていたら、一般論+「詳細は会員登録後」に固定
会員限定案内の文言に、何が会員限定なのかを漏れなく列挙する(落とした項目を全部書く)。 登録せずに進める導線(問い合わせ)も必ず併記する。
5. レンダリング方式の変更に伴う副作用を認識する
cookie を読むため、対象ページは ISR / 静的生成が使えず export const dynamic = 'force-dynamic' になる。
revalidate/dynamic = 'force-static'の export を削除するdynamicParams = falseを消し忘れると、generateStaticParamsを外した瞬間に全パラメータが 404 になる。未定義パラメータは本文側のnotFound()で弾く- コスト影響: 個別ページが N 件あると、クローラのアクセスが毎回サーバレンダリングになる。
middleware で「未ログインなら CDN キャッシュ可」ヘッダを付ける案は、
フレームワークが dynamic ルートに付ける
private, no-cacheに上書きされて効かない(実測)。 効かせたいなら「会員だけ middleware rewrite で別ルートへ送り、非会員版は静的に戻す」まで踏み込む必要がある。 そこまでやらないなら、効かない対策コードは入れずに残課題として明記する。
6. 受け入れテスト(2層で、実際に走らせる)
Layer1: HTML を直接検証(test/list-gating.test.mjs)
next build && next start -p <port> の上で fetch し、未ログインの出力に名前が並んでいないことを機械チェックする。
const { status, html } = await get('/');
check('ゲート表示がある', html.includes('会員限定です'));
check('facet が出ていない', !html.includes('<絞り込みボタンの文言>'));
const links = html.match(/href="\/items\/[0-9a-f-]{8}[^"]*"/g) ?? [];
check('個別ページへのリンクが並んでいない', links.length === 0);
check('ItemList JSON-LD が無い', !html.includes('"@type":"ItemList"'));
check('H1 は残っている', /<h1[\s>]/.test(html));
個別ページは sitemap から実在 ID を1件拾って検証する(テストにIDをハードコードしない)。
startDate / location が JSON-LD に出ていないことも見る。
Layer2: 実描画(Playwright)
スマホ幅(390px)でゲート見出し・登録ボタン・問い合わせCTAが isVisible() であること、
a[href^="/items/"] の数が 0 であること、document.documentElement.scrollWidth > innerWidth
でないこと(横スクロールが出ていないこと)を確認する。
検証の落とし穴
- 会員ログイン状態の実描画まで検証しないと「会員側を壊していない」ことは言えない。
時間がないなら「未検証」と明記する(
member ? 既存 : ゲートの分岐で既存パス無改変・build 通過、まで) - 本番反映後に本番URLでも同じ確認をする。
sitemap.xmlは ISR で古い版が返ることがあるため、 値がおかしい時は数分置いて再取得する
7. 手数料率など「対外配布済みの数字」を触る時
内部DBのデフォルト値と、既に配布した資料(PDF・LP・提案書)の記載は必ず一致させる。
- 現在値は人に聞かず配布物とDBから確認する。人の記憶は古い決定を答えることがある
- 変更したら read-back verify する:
select column_default from information_schema.columns where table_name='…' and column_name='…' - 既存行の値は変えない(
alter column … set defaultはデフォルトのみ変更)。 既存行も変えるならupdateを別途明示的に流し、影響行数を確認する
完了条件
tsc --noEmitとnext buildが通る- Layer1 / Layer2 が全 PASS し、本番URLでも同じ結果
- 会員側の表示・機能が落ちていない(検証したか、未検証かを明記)
- レンダリング方式変更のコスト影響を、対策済みか残課題かのどちらかで明記した
よくある質問
+「DB型サイトを「一覧だけ会員限定・個別ページは残す」に切り替える指示書(Next.js App Router)」とは何ですか?
自社DBで集客していたサイトが競合のリスト抜き取りに気づいた時の改修手順。名前が並ぶバルクな一覧だけを会員限定にし、個別ページはtitle/H1とCTAを残す。ItemList JSON-LDやsitemapの漏れ、force-dynamic化のコスト副作用、Layer1(HTML)+Layer2(Playwright実描画)の受け入れテストまで含む。
+どれくらいトークン(費用)を節約できますか?
ゼロから開発すると約21万トークンかかりますが、この巻物を使えば約3.2万トークンで済みます。差し引き約17.8万トークン(API料金換算で約270円)・85%の節約です。
+どうやって使いますか?
無料です。MDファイルを Claude Code などのAIに読み込ませるだけ。ワンライナーをターミナルに貼れば実装が始まります。要件定義や技術調査を省いて実装だけにトークンを使えます。
+どのAIツールに対応していますか?
claude-code、cursor、codex-cli に対応しています。
+商用利用できますか?
ライセンスは「商用利用可 (再販不可)」です。
🤝 自分でAIを動かすのは、まだ不安…という方へ
この巻物の内容を、AIを使うプロに丸ごと任せることもできます。姉妹サービスAI代行堂なら「LINEで頼むだけで、仕事が完成」。
関連する巻物
Next.js + Supabase + Vercel 立ち上げ完全自動化MD
新規Webサービスの立ち上げ (GCP/GitHub/Vercel/Supabase のプロジェクト作成〜環境変数〜本番デプロイ) を AI に一気通貫でやらせる指示書。人間の作業はログイン1回だけ。
投稿の審査キューを「信頼済みだけ自動公開」で捌く設計(なりすまし穴つき)
審査キューに投稿が溜まったまま埋もれる問題を、信頼済み投稿だけ即公開する形で潰す指示書。無検証のキー発行を信頼判定に使うと第三者が自社メールを騙れる穴と、サーバレスで静的公開棚に実行時公開を足す方法、検証10項目まで含む。
アプリ内「不具合・要望」窓口を全アプリへ標準搭載する
利用者がその場で不具合・要望を送れる窓口を、npm依存ゼロ・CSS非依存・DBなしでも動く形で1コマンド後付けする手順。公開サイト向けの濫用対策、2段検証、配布物として崩してはいけない点まで含む。
この巻物、誰かのトークンも救えます
𝕏 で節約レシートをシェア