programmatic-seo について
テンプレートとデータを使ったページの大規模作成を計画する、SEO担当者向けのプログラマティック SEO スキルです。ページパターン、独自コンテンツの品質、Next.js 実装、内部リンク、インデックス登録、段階的な公開を扱います。
主な機能
- 01拡張可能なページパターンの発見
- 02ページデータモデルとテンプレートの設計
- 03品質確認と薄いコンテンツの回避
- 04Next.js のルートとサイトマップの生成
- 05リンク、インデックス、段階公開の計画
活用シーン
- 01大量ページ展開の機会評価
- 02地域別または製品組み合わせページの構築
- 03Next.js を使ったページ公開の計画
- 04インデックス状況とコンテンツ品質の診断
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ openclaudia/ openclaudia-skills --skill programmatic-seoインストール後、Claude Code で/programmatic-seoを入力して読み込まれたか確認します。
詳細
プログラマティック SEO スキル
プログラマティック SEO(pSEO)のエキスパートとして、テンプレートとデータを使ってターゲットを絞ったページを大量に作成する戦略を支援します。ページのパターンを見つけ、テンプレートを構築し、薄いコンテンツによるペナルティを避けながら大規模に展開できるようサポートします。
プログラマティック SEO とは?
プログラマティック SEO は、以下を組み合わせてページを大規模に作成します。
- ページテンプレート(レイアウト+構造)
- データソース(データベース、API、CSV)
- 動的コンテンツ(ページごとに独自の内容を持ち、単なる変数の置き換えではありません)
- SEO 最適化(メタタグ、内部リンク、スキーマ)
成功した pSEO の例:
- Zapier:「{App A} と {App B} の接続方法」(15 万ページ以上)
- Nomad List:「デジタルノマド向けの {City}」(1,000 以上の都市ページ)
- Wise:「{Currency A} から {Currency B} への為替レート」(1 万ページ以上)
- G2:「{Software} のレビュー」(10 万以上の製品ページ)
- Tripadvisor:「{city} でおすすめの {type}」(数百万ページ)
プロセス
ステップ 1:ページのパターンを見つける
ユーザーが pSEO の機会を見つけられるよう支援します。次のようなパターンを探します。
パターンの式: {Modifier} + {Head Term} + {Qualifier}
| パターンの種類 | 式 | 例 | 規模の可能性 |
|---|---|---|---|
| 場所+サービス | "{service} in {city}" | "plumber in Austin" | 都市数 x サービス数 |
| 比較 | "{product A} vs {product B}" | "Notion vs Asana" | nC2 通りの組み合わせ |
| インテグレーション | "{tool A} + {tool B} integration" | "Slack Salesforce integration" | ツール数 x ツール数 |
| テンプレート/例 | "{type} template" | "invoice template" | 種類の数 |
| 統計/データ | "{topic} statistics {year}" | "remote work statistics 2025" | トピック数 x 年数 |
| 用語集 | "What is {term}" | "What is APR" | 用語の数 |
| おすすめ/トップ | "Best {product} for {use case}" | "Best CRM for startups" | 製品数 x 用途数 |
| レビュー | "{product} review" | "Airtable review" | 製品数 |
| 代替製品 | "{product} alternatives" | "Slack alternatives" | 製品数 |
| 費用/料金 | "How much does {service} cost" | "How much does a website cost" | サービス数 |
優れた pSEO の機会を判断する基準:
- パターンから作成できるページが最低 100 ページ以上ある
- 各組み合わせに測定可能な検索ボリュームがある(規模が大きければ月 10~50 回でも問題ありません)
- 各ページに本当に役立つ独自のコンテンツを生成できる
- データを利用できる(API、データベース、ウェブスクレイピング)
- 競合他社がすでに優れたデータで市場を独占していない
- ページが実際のユーザーの検索意図に応えている(単なるキーワードの詰め込みではない)
ステップ 2:データソースを構築する
ページを支えるデータモデルを定義します。
// Example: City + Service pages
interface PageData {
// URL parameters
slug: string; // "plumber-in-austin-tx"
city: string; // "Austin"
state: string; // "TX"
service: string; // "plumber"
// SEO fields
title: string; // "Best Plumber in Austin, TX | Top 10 for 2025"
metaDescription: string; // "Find the best plumber in Austin, TX..."
h1: string; // "Best Plumber in Austin, TX"
// Dynamic content
providers: Provider[]; // Local providers data
avgCost: number; // Average cost in this market
reviewCount: number; // Total reviews aggregated
faqs: FAQ[]; // Location-specific FAQs
// Related pages (internal linking)
nearbyPages: string[]; // Nearby cities
relatedServices: string[]; // Related services
}
検討すべきデータソース:
- 公開 API(政府データ、Wikipedia、業界データベース)
- ウェブスクレイピング(許可と robots.txt の遵守が必要)
- ユーザー生成コンテンツ(レビュー、投稿)
- エンティティごとの AI 生成による独自の分析
- ライセンス取得済みデータ(有料データプロバイダー)
- 社内の製品データ(SaaS/E コマース向け)
ステップ 3:ページテンプレートを作成する
高品質で独自性のあるページを生成するテンプレートを構築します。重要な原則:各ページは単独で価値を提供しなければなりません。
テンプレートの構成
## Page Template: {Pattern Name}
### Above the Fold
- H1: {dynamic title}
- Key stat or hook: {dynamic data point}
- Quick summary: 2-3 sentences with unique data
- CTA (if commercial intent)
### Main Content Section 1: Overview
- {Entity}-specific introduction (NOT generic)
- Unique data point 1: {dynamic}
- Unique data point 2: {dynamic}
- Contextual explanation
### Main Content Section 2: Detailed Analysis
- Comparison table or detailed breakdown
- {Entity}-specific insights
- Data visualizations if applicable
### Main Content Section 3: Practical Information
- How-to or action steps
- Costs, timing, or specifications
- Location-specific or entity-specific details
### Related Content
- Internal links to related pages (same cluster)
- Links to parent/pillar page
- Links to sibling pages
### FAQ Section
- 3-5 questions specific to this entity
- Answers with unique data points
### Schema Markup
- Appropriate structured data for page type
薄いコンテンツを避ける
**pSEO における最大のリスクは、薄いコンテンツです。**すべてのページが次のチェックリストを満たす必要があります。
| チェック項目 | 要件 | 方法 |
|---|---|---|
| 独自のコンテンツ | 表示されるテキストの 60% 超がこのページ独自の内容である | 動的データ、独自の分析、UGC |
| 十分な深さ | ページごとに実質的な内容が 500 語を超える | テンプレートのセクション+動的コンテンツ |
| 独自のデータ | エンティティごとに少なくとも 2 つの固有データポイントがある | API データ、計算、集計 |
| 訪問者にとって有用 | ページが検索ユーザーのクエリに十分に答えている | 検索意図に合わせる |
| 自動生成らしさがない | テンプレートのようではなく、自然に読める | 多様な文構造、文脈 |
| 内部的な価値 | 関連する他のページとの間にリンクがある | 関連ページのセクション、パンくずリスト |
| 視覚的なコンテンツ | 独自の画像または関連画像が少なくとも 1 点ある | 地図、グラフ、エンティティ画像 |
ステップ 4:Next.js での実装
動的ルート(App Router)
// app/[service]/[city]/page.tsx
import { Metadata } from 'next';
import { notFound } from 'next/navigation';
import { getPageData, getAllPages } from '@/lib/pseo-data';
interface Props {
params: { service: string; city: string };
}
// Generate static paths at build time
export async function generateStaticParams() {
const pages = await getAllPages();
return pages.map((page) => ({
service: page.serviceSlug,
city: page.citySlug,
}));
}
// Dynamic meta tags
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const data = await getPageData(params.service, params.city);
if (!data) return {};
return {
title: data.title,
description: data.metaDescription,
alternates: {
canonical: `https://example.com/${params.service}/${params.city}`,
},
openGraph: {
title: data.ogTitle,
description: data.ogDescription,
url: `https://example.com/${params.service}/${params.city}`,
type: 'website',
},
};
}
export default async function Page({ params }: Props) {
const data = await getPageData(params.service, params.city);
if (!data) notFound();
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'WebPage',
name: data.title,
description: data.metaDescription,
// ... additional schema
};
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
{/* Page component tree */}
</>
);
}
サイトマップの生成
// app/sitemap.ts
import { MetadataRoute } from 'next';
import { getAllPages } from '@/lib/pseo-data';
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const pages = await getAllPages();
return pages.map((page) => ({
url: `https://example.com/${page.serviceSlug}/${page.citySlug}`,
lastModified: page.updatedAt,
changeFrequency: 'monthly',
priority: 0.7,
}));
}
大規模なサイトマップ(URL が 50,000 件以上)の場合は、サイトマップインデックスを使用します。
// app/sitemap.xml/route.ts
import { getAllPageCount } from '@/lib/pseo-data';
export async function GET() {
const totalPages = await getAllPageCount();
const sitemapCount = Math.ceil(totalPages / 50000);
const sitemaps = Array.from({ length: sitemapCount }, (_, i) =>
`<sitemap><loc>https://example.com/sitemap-${i}.xml</loc></sitemap>`
).join('');
return new Response(
`<?xml version="1.0" encoding="UTF-8"?>
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${sitemaps}
</sitemapindex>`,
{ headers: { 'Content-Type': 'application/xml' } }
);
}
Robots.txt の管理
// app/robots.ts
import { MetadataRoute } from 'next';
export default function robots(): MetadataRoute.Robots {
return {
rules: [
{
userAgent: '*',
allow: '/',
disallow: ['/api/', '/admin/'],
},
],
sitemap: 'https://example.com/sitemap.xml',
};
}
ステップ 5:内部リンク戦略
内部リンクは pSEO にとって非常に重要です。次のパターンを実装してください。
1. ハブとスポーク:
Hub: /services/plumber/
|- /plumber/austin-tx
|- /plumber/dallas-tx
|- /plumber/houston-tx
2. 同階層ページへのリンク: 各都市ページから、同じサービスを提供する近隣都市のページに 5~10 件リンクします。
3. カテゴリ横断リンク: 各都市ページから、同じ都市にある他のサービスへのページにリンクします。
4. パンくずリスト:
Home > Services > Plumber > Austin, TX
5. フッター/サイドバーのリンク: 人気のあるページとカテゴリのインデックス。
実装パターン:
// Internal linking component
function RelatedPages({ currentSlug, relatedPages }) {
return (
<section>
<h2>Related Pages</h2>
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
{relatedPages.map((page) => (
<Link
key={page.slug}
href={`/${page.slug}`}
className="text-blue-600 hover:underline"
>
{page.title}
</Link>
))}
</div>
</section>
);
}
ステップ 6:インデックス登録の管理
数千ページをデプロイする際は、インデックス登録を慎重に管理してください。
段階的なリリース:
- まず 50~100 ページをデプロイする
- Google Search Console でインデックス登録状況を監視する
- 「検出 - インデックス未登録」と「クロール済み - インデックス未登録」を確認する
- インデックス登録率が 80% を超えたら、500~1000 ページずつ追加でデプロイする
- インデックス登録率が 50% 未満の場合は、さらにデプロイする前にページの品質を改善する
インデックス登録のシグナル:
- サイトマップを Google Search Console に送信する
- より早く検出されるよう、IndexNow API(Bing、Yandex)を使用する
- 権威性の高い既存ページから内部リンクを設置する
- クロールのシグナルを得るため、初期ページをソーシャルメディアで共有する
品質のしきい値:
- Google にインデックス登録されるページが 30% 未満の場合、テンプレートによって薄いコンテンツが生成されている可能性があります
- ページがインデックス登録された後に削除される場合、コンテンツの品質が基準ぎりぎりです
- pSEO ページについて、GSC で「ページ エクスペリエンス」と「Core Web Vitals」を監視する
ステップ 7:メタタグの数式
メタタグを大規模に生成するには、次の数式を使用します。
タイトルタグの数式(50~60 文字):
"{Primary Keyword} in {Location} | {Brand}"
"Best {Service} in {City}, {State} ({Year})"
"{Product A} vs {Product B}: {Differentiator}"
"{Number} Best {Category} for {Use Case} ({Year})"
"How Much Does {Service} Cost in {City}? ({Year} Pricing)"
メタディスクリプションの数式(150~160 文字):
"Find the best {service} in {city}. Compare {X} local providers, read {Y} reviews, and get free quotes. Average cost: ${Z}."
"Detailed comparison of {A} vs {B}. See features, pricing, pros/cons, and which is better for {use case}."
"{X} best {category} for {audience}. We analyzed {Y} options based on {criteria}. Updated for {year}."
H1 の数式:
"Best {Service} in {City}, {State}"
"{Product A} vs {Product B}: Complete Comparison"
"{Number} Best {Category} for {Use Case}"
出力
プログラムによる SEO についてユーザーを支援する際は、次の内容を提示します。
- 機会分析 - パターン、推定ページ数、検索ボリュームの可能性
- データモデル - ページデータ用の TypeScript インターフェース
- ページテンプレート - すべてのセクションを含む完全なテンプレート
- サンプルページ - 完全にレンダリングされたページの例を 2~3 件
- 実装計画 - ルート、サイトマップ、robots.txt 用の Next.js コード
- 内部リンク戦略 - ページ同士のつなぎ方
- インデックス登録計画 - リリーススケジュールと監視する指標
- 品質チェックリスト - ページごとの品質基準
よくある落とし穴
- テンプレートへの詰め込み: 同一の文章で都市名だけを置き換えると、薄いコンテンツと判断されてペナルティを受けます
- 独自データがない: エンティティ固有のデータポイントがないページには価値がありません
- 過剰生成: 検索ボリュームがゼロの組み合わせのページを作成すると、クロールバジェットを無駄にします
- カニバリゼーションを無視する: 内容が重複するページは互いに競合します。ページごとにキーワードを 1 つ割り当ててください
- 内部リンクがない: 孤立したページはクロールもランク付けもされません
- 一度にすべて公開する: 100K ページを一晩でデプロイすると不自然に見えます。分割して公開してください
- noindex を無視する: インデックス登録に値しないページもあります。品質の低い組み合わせや重複する組み合わせには
noindexを使用してください