言語を切り替える
テーマを切り替える

テンプレート型ページ生成:プログラマティック SEO の技術実装ルート

先週、友人からプログラマティック SEO の相談がありました。キーワードマトリクスは Excel に 2000 以上のロングテールを整理済みなのに、「次の一手」で止まっている、と。

「ロジックは分かる。でも実装しようとすると次から次へと疑問が出る。Next.js と Astro どっち? DB は何を使う? URL はどう設計する? そもそも 5000 ページを一気に生成してサーバーは耐えられる?」

正直、2 年前の私も同じところで悩みました。法律サービスディレクトリを作り、3000 都市ページをプログラマティック SEO で出そうとしたときのことです。結果、初版の公開まで 2 か月。今思い出しても胃が痛くなるほどの落とし穴でした。

この記事では、その穴を埋めるための3 つの技術ルート——静的生成、動的レンダリング、ハイブリッド——をまとめます。それぞれにコードの考え方、向くシーン、実例を載せています。読み終えたら手は動かせるはずです。少なくとも、当時の私のようにゼロから試行錯誤する必要はありません。

すでに「プログラマティック SEO とは何か:適用範囲とスパム政策の境界線」「キーワード一括生成:プログラマティック SEO のコンテンツ計画戦略」を読んでいるなら、本稿はシリーズ第 3 弾として、テンプレート型ページの技術実装にフォーカスします。


まず押さえる:自分に合うのはどのルートか

実装は、フレームワークを勢いで選ぶ話ではありません。まずデータの性質を見ます。

静的生成(SSG)は向いているか

更新が週 1 回や月 1 回程度まで遅くてよいなら、静的生成がいちばん安定しやすいです。

例として、以前友人の観光ガイドサイトを手伝ったことがあります。都市ごとのページは、観光地・交通・グルメなどほぼ固定コンテンツで、半年に 1 回更新すれば十分でした。Astro の Content Collections で 2000 都市分を JSON にし、一括で静的ページを生成。ビルドはだいたい 10 分程度でしたが、公開後の TTFB(Time To First Byte)はおおむね 80ms 前後、CDN キャッシュヒット率は 95% 程度でした。

メリットは明確です。表示が速い、SEO と相性がいい、サーバー負荷が小さい。デメリットは、データ更新のたびにサイト全体の再ビルドが必要なこと、ページが 5000 を超えるとビルド時間が伸びやすいことです。

動的レンダリング(SSR)はいつ使うか

レートや在庫のように常に最新が要るなら、静的生成だけでは足りません。

国境越え送金の Wise は典型例です。通貨ペアのページでは為替が分単位で動きます。静的生成だけだと「10 分前のレート」が表示されかねず、送金判断に直結します。そこで Next.js の SSR(サーバーサイドレンダリング)で、リクエストごとに API から最新レートを取っています。

その代わりサーバー負荷は増えます。Wise は 1 日あたり百万回を超える換算クエリをさばいており、コストも馬鹿になりません。TTFB も遅くなりがちで、目安は 200〜500ms です。

ハイブリッドは「中庸」の選択

更新の遅い部分と速い部分が両方あるなら、ハイブリッドがしっくりきます。

Zapier の「アプリ A × アプリ B」連携ページがそうです。5000 以上あり、機能説明やセットアップ手順などベース情報は静的ですが、ユーザーの接続状態や最終同期時刻は動的です。

Next.js の ISR(Incremental Static Regeneration)で、まずは静的に速く返しつつ、バックグラウンドで定期的に更新。速さと正しさのバランスを取っています。

おすすめ:技術ルートを決める前に、次の 3 つに答えてください。

  1. データはどのくらいの頻度で変わるか(日次? 時間? リアルタイム?)
  2. ページ規模はどの程度か(5000 未満? 5000〜2 万? それ以上?)
  3. SEO パフォーマンスの要求は(TTFB は 100ms 未満必須? 300ms でも許容?)

ここがはっきりすれば、選択肢はかなり絞れます。

<100ms
静的生成の TTFB
CDN キャッシュヒット
200-500ms
動的レンダリングの TTFB
サーバー側で生成
中間
ISR
性能と鮮度のバランス
数据来源: 技術指標の比較

静的生成: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": "Beijing",
  "citySlug": "beijing",
  "province": "Beijing Municipality",
  "lawyerCount": 12500,
  "topFirms": ["King & Wood Mallesons", "Zhong Lun Law Firm", "Dacheng Law Offices"],
  "avgPrice": "2000-5000 CNY/hour",
  "specialties": ["Criminal", "Civil", "Commercial", "Intellectual Property"]
}

2000 都市なら 2000 ファイルに見えますが、スクリプトで一括生成すれば現実的です。私はよく Python か Node で DB からエクスポートし、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} Lawyer Service Guide | Top Law Firms & Pricing</title>
  <meta name="description" content={`Complete guide to lawyer services in ${lawyer.data.city}, covering ${lawyer.data.specialties.join(', ')} and more. Recommended firms: ${lawyer.data.topFirms.join(', ')}. Average rates: ${lawyer.data.avgPrice}`} />
</head>
<body>
  <h1>{lawyer.data.city} Lawyer Service Guide</h1>

  <section>
    <h2>Key Data</h2>
    <p>Number of Lawyers: {lawyer.data.lawyerCount}</p>
    <p>Main Practice Areas: {lawyer.data.specialties.join(', ')}</p>
    <p>Average Rates: {lawyer.data.avgPrice}</p>
  </section>

  <section>
    <h2>Recommended Firms</h2>
    <ul>
      {lawyer.data.topFirms.map(firm => <li>{firm}</li>)}
    </ul>
  </section>

  <!-- Internal links: related cities -->
  <section>
    <h2>Lawyer Services in Nearby Cities</h2>
    <!-- This can be populated based on province or geographic location -->
  </section>
</body>
</html>

getStaticPaths が全都市の静的ページを自動生成します。2000 都市なら 2000 枚の HTML です。

ビルドとデプロイ

ビルドコマンドはシンプルです。

npm run build

ビルド後、dist/ に静的 HTML が揃います。Cloudflare Pages や Vercel にそのまま載せれば、CDN キャッシュも効きます。

先ほどの観光ガイドの例では、2000 ページのビルドにだいたい 10 分程度でした。Astro のビルドは速く、Next.js の静的生成より効率的なことが多いです。

最適化のヒント:5000 ページを超えるなら分割ビルドも検討してください。増分ビルドで、新規データ分だけ生成し、サイト全体を毎回まるごと再ビルドしない運用も可能です。


動的レンダリング: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-');

  // Fetch real-time exchange rate
  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} Currency Converter</h1>
      <p>Current Rate: {rate}</p>
      <p>Last Updated: {new Date(lastUpdate).toLocaleString()}</p>

      {/* Conversion calculator */}
      <input type="number" placeholder="Enter amount" />
      <button>Convert</button>

      {/* Historical chart */}
      <div>30-day exchange rate trend</div>
    </div>
  );
}

ユーザーが /currency/usd-to-eur に来るたび、サーバーが為替 API から最新を取得します。常に「いま」の内容になります。

キャッシュ戦略:サーバーを潰さない

動的レンダリングの難所は負荷です。Wise のように 1 日百万回超のクエリで、毎回フルに外部 API を叩くとコストが爆発します。

解はキャッシュです。ただし戦略が要ります。

stale-while-revalidate が有効です。まずキャッシュ(数分古くてもよい)を返しつつ、裏で静かに更新。次の訪問で新しい値が見えます。

Next.js でも次のような組み立てが可能です。

export const getServerSideProps: GetServerSideProps = async (context) => {
  const { pair } = context.params;

  // Check cache
  const cached = await checkCache(pair);

  if (cached && !isExpired(cached)) {
    return { props: cached.data };
  }

  // Cache expired, update in background
  fetchExchangeRate(pair).then(data => updateCache(pair, data));

  // Return stale data first
  return { props: cached?.data || await fetchExchangeRate(pair) };
};

ユーザーには速く見せつつ、毎回外部 API を叩かずに済ませられます。

動的な構造化データ

動的ページでは、構造化データ(JSON-LD)も動的に組み立てます。

// Generate JSON-LD in page component
const jsonLd = {
  "@context": "https://schema.org",
  "@type": "FinancialService",
  "name": `${from} to ${to} Currency Conversion`,
  "offers": {
    "@type": "Offer",
    "price": rate,
    "priceCurrency": to,
  },
};

// Inject into HTML
<script type="application/ld+json">
  {JSON.stringify(jsonLd)}
</script>

ページごとに常に正しい構造化データを出せ、検索結果側でも最新レートに近い情報を示しやすくなります。


ハイブリッド:Next.js の ISR

「半分は静的、半分は動的」なら、ISR(Incremental Static Regeneration)が第一候補です。

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, // Expires after 1 hour
  };
}

revalidate: 3600 は、生成後 1 時間は静的レスポンスを返し、1 時間経過後の最初の訪問では古いページを見せつつ裏で再生成し、2 回目の訪問から新しいページが返る、という動きです。

オンデマンド再検証

自然失効を待てないケースもあります。例:Zapier の連携が突然壊れ、すぐページを更新したい——1 時間は待たない、というときです。

Next.js ではオンデマンドの再検証も用意されています。

// API route: trigger update
// 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 です。見落とされがちですが、設計は SEO 効果に直結します。

SEO に効く URL の 3 原則

1. 狙いキーワードをパスに含める
URL も評価要因の一つで、自然にキーワードが入ると有利になりやすいです。

例:「北京 離婚 弁護士」向けなら /beijing/divorce-lawyer のように、地名とサービスがパスに出ます。

2. 階層は深くしすぎない(目安 3 段まで)
深すぎるパスはユーザーにもクローラにも負担です。

/service/legal/lawyer/divorce/beijing のような 6 段は、一見して何のページか分かりにくく、低品質と見なされやすいリスクもあります。

3. ハイフン区切り。クエリパラメータは避ける

/lawyer?type=divorce&city=beijing は、/beijing/divorce-lawyer より SEO 的に不利になりがちです。パラメータ URL は動的ページと誤判定され、インデックス効率が落ちることもあります。

よくある URL パターン 3 つ

現場でよく見るパターンを 3 つにまとめます。

パターン 1:コア語を先に

/lawyer/beijing/divorce

ブランド寄りサイト向き。「lawyer」を先に置き、カテゴリの認知を強めます。

パターン 2:地理を先に

/beijing/divorce-lawyer

ローカルサービス向き。「都市名+サービス」の検索クエリとパスが一致しやすく、有利になりやすいです。

パターン 3:フラット

/beijing-divorce-lawyer

超大量ページ向き。階層が 1 段で運用が単純です。

おすすめ:ユーザーの検索の言い方を見る。「都市+業種」が多ければパターン 2。クエリがばらけるならパターン 3 の柔軟さが活きます。

内部リンクの自動化

URL が決まったら内部リンクです。プログラマティック SEO の強みは、リンク網を機械的に張れることです。

例:3000 都市の弁護士ディレクトリで、各ページから関連都市へどう繋ぐか。

地理の階層:北京ページから「河北省の弁護士」「天津の弁護士」など近隣へ。

サービス種別:北京の離婚弁護士ページから、同じ都市の「刑事」「民事」など別領域へ。

コードは素直に書けます。

---
// In page template
const { lawyer } = Astro.props;
const nearbyCities = await getNearbyCities(lawyer.data.province);
const relatedSpecialties = lawyer.data.specialties;
---

<section>
  <h2>Lawyers in Nearby Cities</h2>
  {nearbyCities.map(city => (
    <a href={`/${city.slug}/${lawyer.data.specialties[0]}-lawyer`}>
      {city.name} {lawyer.data.specialties[0]} Lawyer
    </a>
  ))}
</section>

<section>
  <h2>Other Legal Services in {lawyer.data.city}</h2>
  {relatedSpecialties.map(spec => (
    <a href={`/${lawyer.data.citySlug}/${spec}-lawyer`}>
      {lawyer.data.city} {spec} Lawyer
    </a>
  ))}
</section>

各ページに数十本の内部リンクが付き、サイト全体がネット状につながります。クロール効率も上がり、ユーザーも関連に辿りやすくなります。


データベース選定:考えすぎない

データ構造が決まったら保存先です。長々と悩む人が多いですが、そこまで難しくありません。

選択肢 4 つと向くシーン

PostgreSQL:構造化データと複雑クエリ。
フィールドが固定で、「北京の離婚案件で 3000 円未満の…」のような検索が要るなら安定の選択です。ACID、トランザクション、全文検索も揃います。

MongoDB:スキーマがまだ流動的。
初期にフィールドがよく変わるなら柔軟です。先に厳密なスキーマを決めずに拡張できます。私も初期案件では構造変更が多く Mongo を使うことがありました。

Airtable / Google スプレッドシート:小規模・共同編集。
数十〜数百行でチームで触るなら十分戦えます。非エンジニアも更新できる。友人の小案件は 200 行程度を Airtable で回していてメンテが軽いです。

CSV / JSON ファイル:純粋な静的生成。
データが完全に静的でページも 1000 未満なら、ファイル直読みで十分です。DB を持たず、ビルド時に読み込む。Astro の Content Collections はまさにこの思想です。

まず規模で決める

1000 件未満:CSV/JSON か Airtable。
1000〜1 万件:PostgreSQL か MongoDB。
1 万件超:PostgreSQL + Redis キャッシュなども検討。

完璧な一枚を最初から狙わず、動くものを選びましょう。後からスキーマが変わっても、移行は想定より小さく済むことが多いです。


テンプレート開発の要:コンテンツの差別化

基盤ができてからが本番で、多くのプログラマティック SEO がテンプレの中身が似すぎて失敗します。

落とし穴:キーワード差し替えだけのテンプレ

失敗例を見ました。「都市名+ホテル」で 2 万ページ。都市名だけ変えて本文は同一。「北京のホテルおすすめ」「上海のホテルおすすめ」……全部同じ型。

結果、アルゴリズム的に評価を落とし、トラフィックが 7 割減。回復に 8 か月かかった、という話です。

本質:ページごとに独自価値がない。北京で検索したユーザーと上海で検索したユーザーが、ほぼ同じ文章を見せられる——明らかな量産スパムに見えます。

差別化の 3 手法

1. 動的データの注入
各ページに固有データを載せる。弁護士ディレクトリなら、都市ごとの人数、事務所リスト、平均料金など。DB から出すだけでページ間の差が付きます。

2. UGC の統合
ユーザー生成コンテンツが最強の差別化材料です。TripAdvisor のホテルページは、レビューがページごとに違い、テンプレ量産ではありません。

UGC があるなら必ず載せましょう。弁護士サイトなら「口コミ」モジュールなど。

<section>
  <h2>User Reviews</h2>
  {lawyer.data.reviews.map(review => (
    <div>
      <p>{review.content}</p>
      <span>Rating: {review.rating}/5</span>
    </div>
  ))}
</section>

3. AI による補助的な拡張
動的データも UGC もないとき、AI で説明文を厚くする手もあります。ただし人間によるレビュー必須。コア情報の代替にはせず、補助の文章に留めるべきです。

試しに、コアデータ(人数・事務所リスト)は DB、各都市の「法務の特徴」一文だけ AI 補助、という切り分けをしたことがあります。

ポイント:AI は足し算。各ページに本物のユニークデータがない限り、AI だけでは救えません。

構造化データの自動生成

各ページに JSON-LD が必要ですが、これは自動生成に向きます。

const jsonLd = {
  "@context": "https://schema.org",
  "@type": "LegalService",
  "name": `${lawyer.data.city} Lawyer Services`,
  "areaServed": {
    "@type": "City",
    "name": lawyer.data.city,
  },
  "provider": lawyer.data.topFirms.map(firm => ({
    "@type": "Organization",
    "name": firm,
  })),
};

実データから JSON-LD を組み立てれば、検索結果でも事務所名や地域情報がリッチに出やすくなり、CTR の改善にもつながります。


パフォーマンス:待たせすぎない

ページ数が多いほど、速度最適化は無視できません。

押さえる 3 指標

TTFB(Time To First Byte):サーバー応答の速さ。
静的生成はだいたい <100ms、動的は 200〜500ms が目安。500ms を超えると、ユーザーは本文を見る前に離脱しかねません。

LCP(Largest Contentful Paint):主コンテンツが表示されるまで。
目安は <2.5 秒。画像や重いコンポーネントが多いと LCP が伸びます。

FID(First Input Delay):操作への応答。
目安 <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]',
        },
      },
    },
  },
});

ビルド成果物にユニークなハッシュが付くため、CDN と相性が良くなります。

画像最適化:表示を遅くしない

各ページのヒーローをそのままの JPG で載せると数 MB になりがちで、読み込みが重くなります。

Astro には画像最適化が組み込まれています。

---
import { Image } from 'astro:assets';
import heroImage from '../images/lawyer-hero.jpg';
---

<Image src={heroImage} alt="Lawyer Services" width={1200} height={675} />

WebP への変換や圧縮が自動で、例えば 2MB の画像が 200KB 程度になることも珍しくありません。

クロールバジェット:大量ページの必修

5000 ページを超えると、Google のクローラが全部を回りきれないことがあります。いわゆるクロールバジェットの浪費です。

対策

まず sitemap の分割。5000 ページを 1 ファイルに詰め込まず、複数ファイルに分けます。

// 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>

1 ファイルあたり多くても 500 URL 程度に抑えると、インデックス効率が上がりやすいです。

次に 内部リンクの優先度。検索ボリュームの大きい都市ページを、トップやナビから多く指す。重要度の低いページへのリンクは減らす。クローラは自然と重要ページを優先します。


実例:大手はどう組んでいるか

理論のあとは事例です。アーキテクチャのヒントになります。

TripAdvisor:ハイブリッドの型

数百万のホテルページをどう回しているか。

アーキテクチャ:静的生成 + 動的更新のハイブリッド。
名称・住所・設備など基本情報は静的。レビューと評価は動的読み込み。静的は DB エクスポート、動的はレビュー API、の二系統です。

URL/hotel/[city]/[hotel-name]
例:/hotel/beijing/grand-hyatt。3 段で SEO に無理が少ない構成です。

技術の要点

  • レビューのリアルタイム更新(UGC)
  • 価格比較の動的読み込み(API)
  • Review スキーマなどの構造化データ自動化

成功の鍵は UGC です。ページごとに何百もの実レビューがあり、テンプレだけでは出せない差が付きます。

Zapier:ISR の教科書

「アプリ A × アプリ B」の連携ページが 5000 以上。例:Slack と Gmail、Notion と Google カレンダー。

アーキテクチャ:Next.js ISR。
機能説明・手順などベースは静的、接続状態・最終同期は動的。ISR で速さと正しさを両立。

URL/integrations/[app1]/[app2]
例:/integrations/slack/gmail。2 段で明快です。

技術の要点

  • オンデマンド再検証(連携トラブル時の即更新)
  • Playwright などでの自動テスト
  • アプリハブと関連連携への内部リンク網

エンジニアリングブログで ISR と数千ページ運用について語られており、読む価値があります。

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 エッジキャッシュ
  • リアルタイムレートを JSON-LD で表現

コストを抑えつつ鮮度を確保する好例です。リアルタイム寄りの画面を作るなら参考になります。


落とし穴集:私が踏んだミス

成功例のあとは失敗談です。以下は実際に踏んだ穴です。

落とし穴 1:URL が混沌

初期の法律ディレクトリで /service?id=lawyer&city=beijing&type=divorce のような URL にしていました。

柔軟に見えますが、クローラはクエリ URL を好みません。インデックス効率が落ち、ユーザーにも「何のページか」が伝わりにくいです。

教訓:着手前に URL を固める。公開後の全面変更は、内部リンク・外部リンク・サイトマップの修正コストが巨大です。

落とし穴 2:テンプレの重複

2 万ページで都市名だけ差し替え、本文は完全コピー。「北京」を「上海」に換えただけ、というサイトを見ました。

Google 側の評価が落ち、トラフィック 7 割減。

教訓:ユニークデータがないページは作らない。1 万の薄いページより、1000 の質の高いページ。

落とし穴 3:性能の壁

5000 ページを動的生成だけで回すとサーバーが悲鳴を上げます。昔 PHP で回していたときは落ちまくりでした。

教訓:5000 超なら静的生成か ISR を主役に。純ダイナミックだけでは持ちません。

落とし穴 4:監視なき公開

公開後に監視がなく、気づいたら 3 か月後——半分のページがインデックスされていなかった、原因はサイトマップ設定ミス、という案件がありました。

教訓:公開直後の 1 週間は必ずモニタリング。Search Console でインデックス、Screaming Frog で技術面、Ahrefs 等で順位の変化を見る。


次の一歩:考えすぎず小さく始める

ここまで読んでまだ圧倒されるなら、小さな実験からでよいです。

Step 1:小さなシナリオを選ぶ

いきなり 5000 ページにしない。まず 50 都市など、扱える規模で。

Step 2:スタックを決める

更新が遅いなら Astro。リアルタイムなら Next.js SSR。

Step 3:データと URL を設計する

半日でもいいので、フィールドとパスを紙に書く。飛ばさない。

Step 4:テンプレを作って検証

まず 3 ページ分でテンプレを作り、差別化を目視確認。問題なければバッチ生成。

Step 5:小ロットで公開

50 ページで出して 1 週間様子を見る。インデックス率、直帰率、滞在時間。問題なければ 500 へ拡大。

Step 6:反復で磨く

データを見ながらテンプレ・内部リンク・URL を調整する。プログラマティック SEO は一度きりのプロジェクトではなく、継続的な改善です。


FAQ

静的生成・動的レンダリング・ハイブリッドはどう選べばよいですか?
データの更新頻度とページ規模で決めます。更新が週〜月程度で 5000 未満なら静的生成(Astro)が無難。常に最新が要るなら動的(Next.js SSR)。その中間なら ISR のハイブリッドです。
プログラマティック SEO のページ数に上限はありますか?
絶対上限はありませんが、クロールバジェットを意識します。5000 未満なら Google がほぼ回し切れることが多いです。5000〜2 万はサイトマップ分割と内部リンク設計が重要。2 万超は検索ボリュームのあるページに絞るなど、品質フィルタが必須です。
テンプレートページは Google にペナルティを食らいますか?
各ページに実質的な価値があれば問題ありません。ページごとに固有データがあること(キーワード差し替えだけにしない)、UGC やインタラクション、必要なら人的レビューが鍵です。差し替えだけの薄いテンプレはリスクが高いです。
DB は PostgreSQL と MongoDB どちらがよいですか?
スキーマが固定で複雑なクエリが多いなら PostgreSQL(ACID と安定性)。スキーマがまだ変わるなら MongoDB(柔軟性)。小規模(数百件)なら Airtable や JSON ファイルでも十分です。
URL の階層は最大どれくらいがよいですか?
目安は 3 段までです。例:`/city/service/type` が上限イメージ。それより深いとユーザー・クローラ双方に負担です。超大量ページなら 1 段のフラット URL も選択肢です。
内部リンクの自動化はどう実装しますか?
地理階層(近隣都市同士)とトピッククラスタ(同じサービス種別同士)の 2 軸が基本です。テンプレート内でタグや地域フィールドから関連ページを引き、リンク一覧を自動生成します。
プログラマティック SEO には Astro と Next.js どちらが向いていますか?
純粋な静的なら Astro(Content Collections と大量ページ・高速ビルドに強い)。動的レンダリングや ISR が要るなら Next.js(SSR/ISR が成熟)。どちらでも実装は可能で、データの更新頻度との一致が最重要です。

9 min read · 公開日: 2026年4月4日 · 更新日: 2026年4月5日

コメント

GitHubアカウントでログインしてコメントできます

関連記事