テンプレートページ生成:プログラマティック SEO の技術実装パス
先週、友人がプログラマティック SEO について相談してきました。キーワードマトリクスは用意でき、2,000以上のロングテール語を Excel に整理したものの、次の一歩で詰まったと言うのです。
「ロジックは分かる。でもいざ手を動かすと問題が山ほど出てくる。Next.js と Astro のどっち? データベースは何を選ぶ? URL のパスはどう設計する? それに、5,000ページを一気に生成して、サーバーは耐えられるのか?」
これらの問題には、私も2年前に悩まされました。当時は弁護士サービスのディレクトリサイトを作っていて、プログラマティック SEO で3,000の都市ページを用意するつもりでした。結果はどうだったか。最初のバージョンを公開するまで2か月もかかり、踏んだ落とし穴は今思い出しても頭が痛くなります。
この記事は、そうした落とし穴を埋めるためのものです。静的生成・動的レンダリング・ハイブリッドという3つの完全な技術パスを示します。各パスにはコードの考え方、適した場面、実例を添えてあります。読み終えれば、すぐ手を動かせます。少なくとも、私が当時のようにゼロから手探りすることはなくなります。
まず確認:あなたはどのパスに向いている?
技術実装は、やみくもにフレームワークを選ぶことではありません。まず自分のデータの特性を見極める必要があります。
静的生成(SSG)はあなた向き?
データの更新頻度が低い場合、たとえば1週間や1か月に1度しか更新しないなら、静的生成が最も安定します。
例を挙げましょう。以前、友人の旅行ガイドサイトを手伝ったとき、各都市のガイドページの内容はほぼ固定でした。観光地の紹介、交通情報、グルメのおすすめです。これらの情報は半年に1度しか更新しません。私たちは Astro の Content Collections を使い、2,000都市のデータを JSON ファイルとして保存し、静的ページを一括生成しました。ビルド時間は約10分でしたが、公開後の TTFB(最初のバイトが届くまでの時間)は80ms前後で安定し、CDN のキャッシュヒット率は95%でした。
メリットは明確です。ページの読み込みが速く、SEO に本質的に有利で、サーバー負荷も小さい。ただし限界もあります。データを更新するにはサイト全体を再ビルドする必要があり、5,000ページ以上ではビルド時間がかなり長くなります。
動的レンダリング(SSR)はいつ使う?
データがリアルタイムに変わる場面では、静的生成では対応できません。
Wise(あの海外送金ツール)がまさに典型例です。彼らの通貨換算ページでは、為替レートが毎分変わります。静的生成にすると、ユーザーが見るレートは10分前のものかもしれません。これは送金の判断に大きく影響します。そこで彼らは Next.js の SSR(サーバーサイドレンダリング)を採用し、リクエストごとに API から最新レートを取得しています。
ただし動的レンダリングの代償はサーバー負荷です。Wise には毎日100万回以上の通貨換算クエリがあり、サーバーコストは小さくありません。さらに TTFB はやや遅くなり、通常は 200-500ms です。
ハイブリッドは折衷案
低頻度データと高頻度データの両方がある場合、ハイブリッドが最適かもしれません。
Zapier の連携ページはこのやり方です。「App A + App B」の連携ページが5,000以上あり、たとえば「Slack と Gmail の連携」です。これらの連携の基本情報(機能紹介、設定手順)は静的ですが、ユーザーの実際の連携状態(接続済みかどうか、最近の同期時刻)は動的です。
Zapier は Next.js の ISR(インクリメンタル静的再生成)を使っています。ページの初回読み込みは静的で、裏側に定期的に更新する仕組みがあります。ユーザーが見る内容は、速くて正確です。
私のおすすめ:まず次の3つの質問に答えてから、技術パスを選んでください。
- データはどのくらいの頻度で更新する?(毎日? 毎時? リアルタイム?)
- ページ規模はどのくらい?(5,000未満? 5,000〜2万? 2万以上?)
- SEO 性能の要求はどのくらい高い?(TTFB は100ms未満必須? それとも300msでも許容できる?)
この3点をはっきりさせれば、技術選定は明確になります。
静的生成の方式:Astro 実装の考え方
静的生成にすると決めたなら、私は Astro をおすすめします。なぜか。Astro はそもそも静的サイトのために設計されており、Content Collections 機能がプログラマティック SEO にとても適しているからです。
データ構造の設計
まずデータ構造を定義します。弁護士サービスのディレクトリを作り、都市ごとに1ページとすると仮定しましょう。データは次のように整理できます。
// src/content/config.ts
import { defineCollection, z } from 'astro:content';
const lawyersCollection = defineCollection({
type: 'content',
schema: z.object({
city: z.string(),
citySlug: z.string(),
province: z.string(),
lawyerCount: z.number(),
topFirms: z.array(z.string()),
avgPrice: z.string(),
specialties: z.array(z.string()),
}),
});
export const collections = {
'lawyers': lawyersCollection,
};
そして src/content/lawyers/ ディレクトリにデータファイルを作成します。都市ごとに1つの JSON です。
// src/content/lawyers/beijing.json
{
"city": "北京",
"citySlug": "beijing",
"province": "北京市",
"lawyerCount": 12500,
"topFirms": ["金杜律師事務所", "中倫律師事務所", "大成律師事務所"],
"avgPrice": "2000-5000元/時間",
"specialties": ["刑事", "民事", "商事", "知的財産"]
}
2,000都市なら、2,000の JSON ファイルです。手間がかかるように見えますが、実はスクリプトで自動生成できます。私はたいてい Python か Node.js でデータベースからデータをエクスポートし、JSON ファイルへ一括で書き込みます。
動的ルートのテンプレート
データが揃ったら、次はテンプレートです。Astro の動的ルートはとても柔軟です。
// src/pages/[citySlug].astro
---
import { getCollection } from 'astro:content';
export async function getStaticPaths() {
const lawyers = await getCollection('lawyers');
return lawyers.map(lawyer => ({
params: { citySlug: lawyer.data.citySlug },
props: { lawyer },
}));
}
const { lawyer } = Astro.props;
---
<!DOCTYPE html>
<html>
<head>
<title>{lawyer.data.city}弁護士サービスガイド | 法律事務所のおすすめと料金基準</title>
<meta name="description" content={`${lawyer.data.city}の弁護士サービス完全ガイド。${lawyer.data.specialties.join('、')}などの分野を網羅し、${lawyer.data.topFirms.join('、')}などのトップ事務所を紹介。平均料金基準は${lawyer.data.avgPrice}`} />
</head>
<body>
<h1>{lawyer.data.city}弁護士サービスガイド</h1>
<section>
<h2>主要データ</h2>
<p>弁護士数:{lawyer.data.lawyerCount}名</p>
<p>主な分野:{lawyer.data.specialties.join('、')}</p>
<p>平均料金:{lawyer.data.avgPrice}</p>
</section>
<section>
<h2>おすすめ事務所</h2>
<ul>
{lawyer.data.topFirms.map(firm => <li>{firm}</li>)}
</ul>
</section>
<!-- 内部リンク:関連都市 -->
<section>
<h2>周辺都市の弁護士サービス</h2>
<!-- ここは省や地理的な位置に応じておすすめできる -->
</section>
</body>
</html>
getStaticPaths 関数が全都市の静的ページを自動生成します。2,000都市なら、2,000の HTML ファイルです。
ビルドとデプロイ
Astro のビルドコマンドはとても簡単です。
npm run build
ビルドが完了すると、dist/ ディレクトリの中身がすべての静的 HTML ファイルです。そのまま Cloudflare Pages や Vercel にデプロイすれば、CDN が自動でキャッシュします。
以前に作った旅行ガイドサイトでは、2,000ページのビルド時間は約10分でした。Astro のビルド速度は確かに速く、Next.js の静的生成よりもかなり効率的です。
最適化のコツ:ページが5,000を超えるなら、バッチに分けてビルドすることをおすすめします。Astro はインクリメンタルビルドに対応しており、新しいデータに対応するページだけを生成でき、毎回サイト全体を再ビルドする必要はありません。
動的レンダリングの方式:Next.js SSR 実践
データがリアルタイムに変わる場合、静的生成では対応できません。そのときは Next.js の SSR を使います。
基本設定
Next.js SSR の核心は getServerSideProps です。
// pages/currency/[pair].tsx
import { GetServerSideProps } from 'next';
export const getServerSideProps: GetServerSideProps = async (context) => {
const { pair } = context.params;
const [from, to] = pair.split('-to-');
// リアルタイムで為替レートを取得
const exchangeRate = await fetchExchangeRate(from, to);
return {
props: {
from,
to,
rate: exchangeRate.rate,
lastUpdate: exchangeRate.timestamp,
},
};
};
export default function CurrencyPage({ from, to, rate, lastUpdate }) {
return (
<div>
<h1>{from} to {to} 通貨換算</h1>
<p>現在のレート:{rate}</p>
<p>更新時刻:{new Date(lastUpdate).toLocaleString()}</p>
{/* 換算計算機 */}
<input type="number" placeholder="金額を入力" />
<button>換算</button>
{/* 過去の推移グラフ */}
<div>過去30日間のレート推移</div>
</div>
);
}
ユーザーが /currency/usd-to-eur にアクセスするたびに、サーバーは為替レート API から最新データを取得します。ページ内容は常に最新です。
キャッシュ戦略:サーバーを酷使しない
動的レンダリングの問題はサーバー負荷です。Wise は毎日100万回以上のクエリがあり、もし毎回リアルタイムでデータを取得すれば、サーバーコストは爆発します。
解決策はキャッシュです。ただしキャッシュには戦略が必要です。
stale-while-revalidate は良い手法です。ユーザーのリクエスト時に、まずキャッシュ済みのデータ(数分前に期限切れかもしれない)を返し、同時に裏側でこっそり更新します。次にユーザーが来たときには、新しいデータが見られます。
Next.js はこの戦略をサポートしています。
export const getServerSideProps: GetServerSideProps = async (context) => {
const { pair } = context.params;
// キャッシュを確認
const cached = await checkCache(pair);
if (cached && !isExpired(cached)) {
return { props: cached.data };
}
// キャッシュ期限切れ、裏側で更新
fetchExchangeRate(pair).then(data => updateCache(pair, data));
// まず古いデータを返す
return { props: cached?.data || await fetchExchangeRate(pair) };
};
これでユーザーは毎回すばやくコンテンツを見られ、サーバーも毎回外部 API を呼ぶ必要がなくなります。
構造化データの動的注入
動的レンダリングのページでは、構造化データも動的に生成する必要があります。
// ページコンポーネント内で JSON-LD を生成
const jsonLd = {
"@context": "https://schema.org",
"@type": "FinancialService",
"name": `${from} to ${to} Currency Conversion`,
"offers": {
"@type": "Offer",
"price": rate,
"priceCurrency": to,
},
};
// HTML に注入
<script type="application/ld+json">
{JSON.stringify(jsonLd)}
</script>
これで各ページの構造化データはリアルタイムで正確になり、Google の検索結果にも最新レートを表示できます。
ハイブリッドの方式:Next.js ISR 実践
「一部は静的、一部は動的」という場面なら、ISR(インクリメンタル静的再生成)が最良の選択です。
ISR の中核ロジック
ISR の原理はこうです。ページの初回生成時は静的ですが、「有効期限」を設定できます。期限が切れると、次のアクセスで裏側の再生成がトリガーされます。
// pages/integrations/[app1]-and-[app2].tsx
export async function getStaticPaths() {
const integrations = await fetchAllIntegrations();
return integrations.map(int => ({
params: { app1: int.app1, app2: int.app2 },
}));
}
export async function getStaticProps({ params }) {
const integration = await fetchIntegration(params.app1, params.app2);
return {
props: integration,
revalidate: 3600, // 1時間後に期限切れ
};
}
revalidate: 3600 の意味は、ページ生成後、1時間以内のアクセスはすべて静的コンテンツを返すということです。1時間後の最初のアクセスでは、ユーザーはまだ古いページを見ますが、裏側で再生成が行われます。2回目のアクセスでは、新しいページになります。
オンデマンド更新:on-demand revalidation
自然な期限切れを待つのに向かない場面もあります。たとえば Zapier のある連携が突然壊れ、ユーザーからの報告を受けたら、1時間待つのではなく、すぐにページの状態を更新したいはずです。
Next.js はオンデマンド更新をサポートしています。
// API パス:更新をトリガー
// pages/api/revalidate.ts
export default async function handler(req, res) {
const { app1, app2 } = req.query;
try {
await res.revalidate(`/integrations/${app1}-and-${app2}`);
return res.json({ revalidated: true });
} catch (err) {
return res.status(500).send('Error revalidating');
}
}
監視スクリプトを設定し、連携状態を定期的にチェックできます。問題を見つけたら、この API を呼んでページ更新をトリガーします。
ISR が適用できる境界
ISR は万能ではありません。データが本当にアクセスごとにリアルタイム更新を要する場合(たとえば株価、フラッシュセール)は、やはり SSR が必要です。
ISR が適しているのは、データの更新頻度が高くなく(毎時・毎日)、それでも更新時にはすばやく反映したい場面です。Zapier の連携ページはまさに完璧な例です。連携情報は数か月に1度しか変わりませんが、いったん変わったら、ユーザーはできるだけ早く見たいのです。
URL 構造の設計:SEO の土台
技術選定が決まったら、次は URL 構造です。多くの人が見落としがちですが、URL 設計は SEO 効果に直接影響します。
SEO に優しい URL の3原則
第一に、ターゲットキーワードを含める。URL は Google のランキング要因の1つであり、キーワードを自然に埋め込むと加点になります。
たとえば「北京 離婚弁護士」というページなら、URL は /beijing/divorce-lawyer にできます。キーワードの「北京」「離婚弁護士」がどちらも自然にパスに現れます。
第二に、階層を3階層以内にする。深すぎるパスはユーザーにもクローラーにも不親切です。
/service/legal/lawyer/divorce/beijing のような6階層のパスは、ユーザーがひと目で目が回ります。Google も低品質ページと見なすかもしれません。
第三に、パラメータではなくハイフンで区切る。
/lawyer?type=divorce&city=beijing のようなパラメータ URL は、SEO 効果が /beijing/divorce-lawyer に遠く及びません。パラメータ URL はクローラーに動的ページと誤判定されやすく、インデックス効率が低くなります。
よくある3つの URL パターン
私が見てきた主流のパターンは3つで、それぞれ適した場面があります。
パターン1:コア語が前
/lawyer/beijing/divorce
ブランド志向のサイトに向きます。コア語の「lawyer」が前に来て、ブランド認知を強めます。
パターン2:地理語が前
/beijing/divorce-lawyer
ローカルサービス系のサイトに向きます。ユーザーが「北京 離婚弁護士」を検索すると、URL が検索語と完全に一致し、ランキングの優位性が明確です。
パターン3:フラット化
/beijing-divorce-lawyer
ページ数が膨大なサイトに向きます。パスが1階層だけで、ビルドも管理も簡単です。
私のおすすめは、ユーザーがどう検索するかを見ることです。検索の大半が「都市 + サービス」なら、パターン2を使います。検索語がばらけているなら、パターン3のほうが柔軟です。
内部リンクの自動化
URL が決まったら、次は内部リンクです。プログラマティック SEO の強みの1つは、内部リンクのネットワークを自動で構築できることです。
弁護士ディレクトリサイトがあり、3,000の都市ページがあるとしましょう。各ページは関連都市にリンクすべきです。どうするか。
地理階層に基づく:北京ページから「河北省の弁護士」「天津の弁護士」(周辺都市)へリンク。
サービス種別に基づく:北京の離婚弁護士ページから「北京の刑事弁護士」「北京の民事弁護士」(同じ都市の別サービス)へリンク。
コード実装はとても簡単です。
---
// ページテンプレート内で
const { lawyer } = Astro.props;
const nearbyCities = await getNearbyCities(lawyer.data.province);
const relatedSpecialties = lawyer.data.specialties;
---
<section>
<h2>周辺都市の弁護士</h2>
{nearbyCities.map(city => (
<a href={`/${city.slug}/${lawyer.data.specialties[0]}-lawyer`}>
{city.name}{lawyer.data.specialties[0]}弁護士
</a>
))}
</section>
<section>
<h2>{lawyer.data.city}のその他の法律サービス</h2>
{relatedSpecialties.map(spec => (
<a href={`/${lawyer.data.citySlug}/${spec}-lawyer`}>
{lawyer.data.city}{spec}弁護士
</a>
))}
</section>
これで各ページが数十個の内部リンクを持ち、サイト全体がメッシュ状の構造になります。クローラーのクロール効率が高く、ユーザーも関連コンテンツをすばやく見つけられます。
データベース選定:ここで悩みすぎない
データ構造を設計したら、次はどこに保存するか。多くの人がここで長く悩みますが、実はそれほど複雑ではありません。
4つの選択肢、それぞれに適した場面
PostgreSQL:構造化データ、複雑なクエリ向け。
データのフィールドが固定で、複雑なクエリが必要なら(たとえば「北京市で料金が3,000未満の離婚弁護士をすべて探す」)、PostgreSQL が最も安定します。ACID 保証、トランザクション対応、全文検索がすべて使えます。
MongoDB:柔軟なデータ構造、高速なイテレーション向け。
データ構造がまだ変わるなら、MongoDB のほうが柔軟です。事前にスキーマを定義する必要がなく、いつでもフィールドを追加できます。私も初期のプロジェクトでは、データ構造を頻繁に調整するため MongoDB をよく使いました。
Airtable/Google Sheets:小規模、共同作業のニーズ向け。
データが数十〜数百件だけで、チームの複数人で共同作業するなら、Airtable や Google Sheets は実は使い勝手がよいです。ビジュアル編集、リアルタイム共同作業ができ、非エンジニアでも操作できます。友人の小規模プロジェクトでは Airtable を使い、データ量は200件で、保守コストはとても低かったです。
CSV/JSON ファイル:純粋な静的生成の場面向け。
データが完全に静的で、ページ数が1,000未満なら、CSV や JSON ファイルをそのまま使えば十分です。データベースを保守する必要がなく、ビルド時にファイルを直接読み込みます。Astro の Content Collections はまさにこの設計です。
私のおすすめ:まずデータ規模をはっきりさせる
データ量が1,000件未満:CSV/JSON ファイルか Airtable。
データ量が1,000〜1万件:PostgreSQL か MongoDB。
データ量が1万件以上:PostgreSQL + Redis キャッシュ。
長く悩まず、使えるものを1つ選べば十分です。後でデータ構造が変わったら移行すればよく、大した工事ではありません。
テンプレート開発の鍵:コンテンツの差別化
技術アーキテクチャを組んだら、テンプレート開発こそが本当の難関です。多くのプログラマティック SEO プロジェクトが失敗するのは、テンプレートの内容が似通いすぎているからです。
落とし穴回避:キーワードを差し替えるだけのテンプレートは必ず死ぬ
ある失敗例を見たことがあります。あるサイトが2万の「都市 + ホテル」ページを作り、各ページは都市名を差し替えただけで、他の内容は完全に同じでした。「北京 ホテルおすすめ」「上海 ホテルおすすめ」「広州 ホテルおすすめ」……本文はすべて同じテンプレートでした。
結果はどうだったか。Google のアルゴリズムにペナルティを受けました。トラフィックは70%落ち、回復に8か月かかりました。
問題の根源:各ページに固有の価値がないことです。ユーザーが「北京 ホテル」を検索しても、見える内容は「上海 ホテル」とほぼ同じで、これは明らかに一括生成されたゴミコンテンツです。
差別化の3つの方法
方法1:動的データ注入
各ページには必ず固有データが必要です。弁護士ディレクトリサイトなら、各都市の弁護士数、事務所一覧、平均料金はそれぞれ異なります。これらのデータは直接データベースから来るので、各ページが自然に差別化されます。
方法2:UGC 統合
ユーザー生成コンテンツは最良の差別化素材です。TripAdvisor のホテルページでは、各ホテルに固有のユーザーレビューがあります。これらのレビューはテンプレート生成ではなく、本物のユーザーが書いたものです。
UGC データがあるなら、必ずページに統合しましょう。たとえば弁護士ディレクトリサイトなら、「ユーザー評価」モジュールを追加できます。
<section>
<h2>ユーザー評価</h2>
{lawyer.data.reviews.map(review => (
<div>
<p>{review.content}</p>
<span>評価:{review.rating}/5</span>
</div>
))}
</section>
方法3:AI による拡張
動的データも UGC もない場合は、AI を使って差別化コンテンツを生成できます。ただし注意してください。AI が生成した内容は必ず人手でレビューする必要があり、説明的な段落にだけ使えます。コアコンテンツにはできません。
以前ある実験をしました。弁護士ディレクトリサイトで、コアデータ(弁護士数、事務所一覧)はデータベースのものですが、各都市の「法律サービスの特徴紹介」は AI で補助生成しました。たとえば「北京の弁護士サービスの特徴:商事紛争の比率が高い、知的財産のニーズが大きい……」といった説明的な段落です。
鍵:AI による補助はあくまで拡張であり、置き換えではありません。各ページには必ず本物の固有データが必要で、AI は花を添えるだけです。
構造化データの自動化
各ページには JSON-LD の構造化データが必要です。これは完全に自動化できます。
const jsonLd = {
"@context": "https://schema.org",
"@type": "LegalService",
"name": `${lawyer.data.city}弁護士サービス`,
"areaServed": {
"@type": "City",
"name": lawyer.data.city,
},
"provider": lawyer.data.topFirms.map(firm => ({
"@type": "Organization",
"name": firm,
})),
};
各ページの構造化データは本物のデータに基づいて生成され、Google の検索結果に事務所一覧や都市情報を表示でき、クリック率もかなり高くなります。
性能最適化:ユーザーを待たせすぎない
プログラマティック SEO はページ数が多いため、性能最適化を無視できません。
3つの重要指標
TTFB(最初のバイトが届くまでの時間):サーバーの応答速度。
静的生成は通常 < 100ms、動的レンダリングは 200-500ms です。TTFB が500msを超えると、ユーザーは内容を見る前にページを閉じてしまうかもしれません。
LCP(最大コンテンツの描画時間):主要コンテンツが現れる時間。
目標は < 2.5秒です。画像や複雑なコンポーネントが多いページでは、LCP がとても遅くなることがあります。
FID(初回入力遅延):操作への応答速度。
目標は < 100ms です。ページの JavaScript が多すぎると、ユーザーがボタンを押しても反応しないことがあります。
CDN 設定:静的ページのアクセラレータ
静的生成のページでは、CDN キャッシュが鍵です。Cloudflare Pages、Vercel はどちらも CDN を内蔵しており、設定はとても簡単です。
// astro.config.mjs
export default defineConfig({
output: 'static',
build: {
assets: 'assets/',
},
vite: {
build: {
rollupOptions: {
output: {
assetFileNames: 'assets/[hash][extname]',
},
},
},
},
});
これでビルド後の静的ファイルにはすべて一意の hash が付き、CDN のキャッシュ効率が高くなります。
画像最適化:画像でページを遅くしない
各ページのヒーロー画像は、もとの JPG をそのまま使うと数 MB になることがあります。読み込み時間がとても遅くなります。
Astro は画像最適化を内蔵しています。
---
import { Image } from 'astro:assets';
import heroImage from '../images/lawyer-hero.jpg';
---
<Image src={heroImage} alt="弁護士サービス" width={1200} height={675} />
Astro は自動で WebP 形式に変換し、適切なサイズに圧縮します。もとは 2MB の画像が、最適化後は 200KB ほどになることもあります。
クロールバジェット管理:ページが多いときの必修科目
ページが5,000を超えると、Google のクローラーがクロールしきれないことがあります。これを「クロールバジェットの浪費」と呼びます。
解決策:
第一に、sitemap.xml の分割。5,000ページを1つの sitemap に入れず、複数のファイルに分けます。
// sitemap-index.xml
<sitemapindex>
<sitemap><loc>https://example.com/sitemap-1.xml</loc></sitemap>
<sitemap><loc>https://example.com/sitemap-2.xml</loc></sitemap>
<sitemap><loc>https://example.com/sitemap-3.xml</loc></sitemap>
</sitemapindex>
各 sitemap ファイルは最大500ページにすると、クローラーのクロール効率が高くなります。
第二に、内部リンクの優先度。重要なページ(検索ボリュームの大きい都市)には、トップページのおすすめやナビゲーションバーへの掲載など、より多くの内部リンク入口を与えます。重要でないページは内部リンクを減らすと、クローラーは自然に重要ページを優先してクロールします。
実例の分解:他社のやり方を見てみる
理論は終わりです。実例を見てみましょう。これらのサイトの技術アーキテクチャは、多くのヒントを与えてくれます。
TripAdvisor:ハイブリッド構成の手本
TripAdvisor には数百万のホテルページがあります。どうやって実現したのか。
アーキテクチャ:静的生成 + 動的更新のハイブリッド。
ホテルの基本情報(名称、住所、設備)は静的生成です。ユーザーレビューや評価は動的に読み込まれます。各ホテルページには2つのデータソースがあり、静的データはデータベースのエクスポートから、動的データはレビュー API から来ます。
URL 構造:/hotel/[city]/[hotel-name]
たとえば /hotel/beijing/grand-hyatt。3階層のパスで、SEO に優しいです。
鍵となる技術:
- ユーザーレビューのリアルタイム更新(UGC 統合)
- 価格比較の動的読み込み(API 呼び出し)
- 構造化データの自動化(Review schema)
TripAdvisor の成功の鍵は UGC です。各ホテルページには数百件の本物のレビューがあり、内容が自然に差別化されます。UGC がなければ、純粋なテンプレート生成のページがここまで上位に来ることはありません。
Zapier:ISR の代表的な活用
Zapier には「App A + App B」の連携ページが5,000以上あります。たとえば「Slack と Gmail の連携」「Notion と Google Calendar の連携」です。
アーキテクチャ:Next.js ISR。
連携の基本情報(機能紹介、設定手順)は静的ですが、ユーザーの実際の連携状態(接続済みかどうか、最近の同期時刻)は動的です。ISR の仕組みが、ページの速さと正確さの両立を保証します。
URL 構造:/integrations/[app1]/[app2]
たとえば /integrations/slack/gmail。2階層のパスで、簡潔で分かりやすいです。
鍵となる技術:
- on-demand revalidation:連携が壊れたらすぐにページを更新
- 自動テストのカバレッジ:Playwright で各連携ページをテスト
- 内部リンクのネットワーク:app hub ページ + 関連連携のおすすめ
Zapier の ISR 実装はとても学ぶ価値があります。彼らのエンジニアチームはブログを書いており、ISR で5,000ページを管理する方法を解説しています。一読をおすすめします。
Wise:動的レンダリング + キャッシュ戦略
Wise の通貨換算ページは、為替レートが毎分変わります。純粋な静的生成では対応できません。
アーキテクチャ:Next.js SSR + stale-while-revalidate キャッシュ。
ユーザーが /currency/usd-to-eur にアクセスするたびに、サーバーはまずキャッシュを確認します。キャッシュが期限切れでなければ(たとえば5分以内)、古いデータをそのまま返します。裏側ではこっそりレートを更新します。次にユーザーがアクセスすると、新しいデータになっています。
URL 構造:/currency/[from]-to-[to]
たとえば /currency/usd-to-eur。キーワードが自然に埋め込まれています。
鍵となる技術:
- stale-while-revalidate のキャッシュ戦略
- CDN edge caching:世界中のノードでキャッシュ
- 構造化データの動的注入:JSON-LD でリアルタイムレートを表示
Wise のキャッシュ戦略はとても巧妙です。データのリアルタイム性を保ちつつ、サーバーコストも抑えています。リアルタイムデータの場面に取り組むなら、彼らの考え方を参考にできます。
落とし穴ガイド:私が踏んできた穴
ここまで成功例をたくさん話してきましたが、失敗の教訓も語りましょう。これらの穴は私がすべて踏んできたものです。あなたには避けてほしいと思います。
穴1:URL 構造の混乱
初期の弁護士ディレクトリサイトでは、URL 設計はこうでした。/service?id=lawyer&city=beijing&type=divorce。
柔軟に見えますが、問題は大きいです。Google のクローラーはパラメータ URL に不親切で、インデックス効率が低いです。さらにユーザーがこの種の URL を見ても、ページの内容が分かりません。
教訓:URL 設計は手を動かす前に必ず決めること。いったん公開すると、URL 構造の変更は代償が巨大です。すべての内部リンク、被リンク、sitemap を更新する必要があります。
穴2:テンプレート内容の重複
あるサイトを見たことがあります。2万ページが都市名を差し替えただけでした。本文の内容は完全に同じで、「北京」を「上海」に変えただけです。
Google にペナルティを受け、トラフィックは70%落ちました。
教訓:各ページには必ず固有データが必要です。固有データがなければ、そのページを生成しないこと。1万のゴミページを作るくらいなら、1,000の高品質ページだけにするほうがましです。
穴3:性能のボトルネック
5,000ページを動的レンダリングにすると、サーバー負荷が大きいです。私は初期に PHP で動的生成をしていて、サーバーがよくクラッシュしました。
教訓:ページが5,000を超えたら、必ず静的生成か ISR を使うこと。純粋な動的レンダリングでは耐えられません。
穴4:監視の仕組みがない
公開後に監視がなく、問題の発見が遅すぎました。あるプロジェクトでは公開から3か月たって、ようやく気づきました。半分のページが Google にインデックスされておらず、原因は sitemap の設定ミスでした。
教訓:公開の最初の1週間は必ず監視すること。Google Search Console でインデックス状態を見て、Screaming Frog で技術的問題をチェックし、Ahrefs でランキングの変化を監視します。
次の一歩:考えすぎず、まず手を動かす
ここまで多くを語りましたが、まだ少し迷っているかもしれません。私のおすすめは、考えすぎず、まず小さな実験を1つやることです。
第1歩:小さな場面を1つ選ぶ
最初から5,000ページを作ろうとしないこと。まずコントロールできる場面を選びます。たとえば50の都市ページです。
第2歩:技術スタックを決める
データの更新頻度が低いなら Astro。データがリアルタイムに変わるなら Next.js SSR です。
第3歩:データ構造と URL を設計する
半日かけて、データフィールドと URL パスをはっきり考えます。この工程はとても重要なので、省かないこと。
第4歩:テンプレート開発 + テスト
まず3ページ分のテンプレートを作り、コンテンツの差別化の度合いを人手で確認します。問題ないと確認してから、一括生成します。
第5歩:小バッチで公開
まず50ページを公開し、1週間観察します。インデックス率、直帰率、滞在時間を見ます。指標が正常なら、500ページに拡大します。
第6歩:反復して最適化する
データのフィードバックに応じて、テンプレート、内部リンク、URL を調整します。プログラマティック SEO は一度きりの工事ではなく、継続的に反復するプロセスです。
FAQ
静的生成・動的レンダリング・ハイブリッドはどう選ぶ?
プログラマティック SEO のページ数に上限はある?
テンプレートページは Google にペナルティを受ける?
データベースは PostgreSQL と MongoDB のどちらを選ぶ?
URL の階層は最大何階層まで?
内部リンクの自動化はどう実装する?
Astro と Next.js はどちらがプログラマティック SEO に向く?
10分で読めます · 公開日: 2026年4月4日 · 更新日: 2026年6月8日
プログラマティック SEO 完全ガイド
検索からこのページに来た場合は、前後の記事もあわせて読むと同じテーマの理解がかなり早く深まります。
前の記事
キーワード一括生成:プログラマティックSEOのコンテンツ設計戦略
キーワード一括生成はプログラマティックSEOの中核スキルです。本記事では実践的な5ステップのワークフロー、主要ツール比較(Ahrefs/SEMrush/Whitespark)、データ構造化の方法論を解説し、1日で2000以上のキーワードマトリクスを作る方法を紹介します
第 2 / 7 記事
次の記事
プログラマティックSEOのデータ品質モニタリング:コンテンツ健全性チェック実践ガイド
プログラマティックSEOのコンテンツ健全性チェックの核心を解説。データ完全性の検証から自動モニタリング体制まで、Python + GSC APIで持続可能な品質保証ループを構築する方法を紹介します。
第 4 / 7 記事
関連記事
プログラマティック SEO とは:適用範囲とスパム対策ポリシーのレッドライン
プログラマティック SEO とは:適用範囲とスパム対策ポリシーのレッドライン
SEMrush 競合分析の実践:キーワード戦略を発見から実行まで
SEMrush 競合分析の実践:キーワード戦略を発見から実行まで
SEO 競合分析の実践:5 ステップでキーワード突破口を見つける
コメント
GitHubアカウントでログインしてコメントできます