Programmatic SEO skill for Codex
Thousands of pages, none of them thin. Codex treats a programmatic page set as a data product. It profiles field coverage before designing a template, writes an indexability gate into the build using field counts and shingle-based uniqueness against siblings, keeps thin combinations out of the index and the sitemap, links everything through crawlable hubs, and launches a small batch first so indexation data guides the next one.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- programmatic-seo Installs to ~/.agents/skills/programmatic-seo/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install programmatic-seo The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project programmatic-seo Installs to .agents/skills/programmatic-seo/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/programmatic-seo.zipunzip -o programmatic-seo.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) programmatic-seo PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $programmatic-seo, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Designs and builds template-driven page sets at scale — integrations, locations, comparisons, glossary, "X for Y" pages — from a structured dataset, with per-page uniqueness thresholds, indexation gating, internal linking, sitemaps and a staged rollout that stays clear of Google's scaled content abuse and doorway policies. Use when someone wants to generate hundreds or thousands of SEO pages from a database, CSV or API, when an existing programmatic section is not getting indexed, or when deciding which generated pages to noindex or prune.
Use it when
- Building many pages from a database, CSV or API
- An existing generated section is mostly crawled but not indexed
- Setting rules for which generated pages deserve indexing
- Pruning or consolidating a large templated section
Use something else when
- You need individual high-value articles: use seo-content-writing
- The pages are city or branch pages for a real local business: use local-seo
How to check the result
The build test reports how many pages pass the gate, only passing pages appear in sitemaps with index robots, and Search Console shows the indexed-to-submitted ratio for each rollout batch.
The mistake it prevents
Launching every combination at once with only the entity name swapped, which reads as doorway pages and can drag down the whole section.
Done well: every generated page answers a real query with data the visitor cannot get by swapping a word on a sibling page; pages without enough unique data are not indexed; and the rollout is staged so you learn from indexation before generating ten thousand more.
Is this a programmatic use case?
Proceed only if all are true:
- A repeating query pattern with demand exists (
[tool] + [tool] integration,[service] in [city],[A] vs [B],[term] meaning,[currency] to [currency]). - You own or can lawfully use a dataset that makes each page materially different (attributes, numbers, reviews, inventory, prices, steps, compatibility, examples).
- A template can answer the intent better than a hand-written page could at that scale.
If the only variable is the name in the H1, stop: that is the doorway pattern. Google’s spam policies (doorway abuse, scaled content abuse) target exactly this, regardless of whether pages are written by people, templates or models.
Workflow
- [ ] 1. Validate the pattern: demand, SERP format, who ranks
- [ ] 2. Audit the dataset: fields, coverage, freshness, rights
- [ ] 3. Define uniqueness and indexation thresholds
- [ ] 4. Design the template (sections fed by data, not filler)
- [ ] 5. URL scheme, hubs, internal links, sitemaps
- [ ] 6. Build with tests that enforce thresholds
- [ ] 7. Stage the rollout and measure
- [ ] 8. Prune and improve
1. Validate the pattern
Take 10–20 sample combinations across head, mid and long tail. For each, check the SERP (network needed): do template pages (marketplaces, directories, competitors) rank, or editorial guides? If editorial dominates, a template will struggle. Record volume per sample to estimate how much of the set has any demand at all; typically a small share of combinations carries most of it.
2. Audit the dataset
Profile it before designing anything:
import csv
from collections import Counter
rows = list(csv.DictReader(open("integrations.csv", encoding="utf-8")))
print(len(rows), "rows")
for field in rows[0]:
filled = sum(1 for r in rows if (r[field] or "").strip())
print(f"{field:30} {filled/len(rows):6.1%} filled, {len(set(r[field] for r in rows))} distinct")
Fields with low fill rates cannot carry uniqueness. Confirm licensing for scraped or third-party data and how often it refreshes.
3. Thresholds
Define, in code, what makes a page indexable. Example for integration pages:
| Rule | Threshold | Below threshold |
|---|---|---|
| Distinct data fields populated | ≥ 6 of 10 | noindex, follow, excluded from sitemap |
| Unique text (not shared with siblings) | ≥ 40% of body text | noindex until enriched |
| Real use cases / steps | ≥ 3 | noindex |
| Search demand or business value | demand > 0 or strategic | not generated at all |
Measure uniqueness by comparing a page’s text shingles with its nearest siblings:
import re
def shingles(text: str, k: int = 5) -> set[str]:
words = re.findall(r"\w+", text.lower())
return {" ".join(words[i:i + k]) for i in range(max(len(words) - k + 1, 1))}
def unique_share(page: str, siblings: list[str]) -> float:
mine = shingles(page)
shared = set().union(*(shingles(s) for s in siblings)) if siblings else set()
return len(mine - shared) / max(len(mine), 1)
4. Template design
Each section is either data-driven or omitted; never filler paragraphs with a swapped noun. Strong section types:
- Key facts table from structured fields (pricing, compatibility, limits, ratings).
- Computed insights: comparisons to category averages, rankings, trends.
- Steps or code specific to the combination (real API endpoints, real settings).
- First-party content: reviews, Q&A, usage stats, screenshots.
- Conditional copy: different sentences for different data states (free vs paid, supported vs not), written by a human once per state.
- Related entities: nearest siblings by real similarity, not random.
LLM-generated text per page is acceptable only when grounded in that page’s data, reviewed by sampling, and adding facts; ungrounded generated prose at scale is the scaled-content pattern.
5. URLs, hubs and links
- Stable, readable URLs:
/integrations/slack/,/integrations/slack/jira/. Decide ordering for pairs (alphabetical or canonical order) and 301 the reverse order. - Hub pages per dimension (
/integrations/,/integrations/slack/) with crawlable links (paginated, not infinite scroll) to every indexable child. - Each page links to its parent hub, siblings by similarity, and relevant editorial content. Every indexable page must be reachable within about 3–4 clicks.
- Sitemaps contain only pages that pass thresholds; split by section (see
sitemaps-robots).
6. Build with enforced thresholds
Put the gate in the build so nobody can ship thin pages by accident:
// lib/pseo/gate.ts
export function indexable(p: IntegrationPage): { index: boolean; reasons: string[] } {
const reasons: string[] = [];
if (p.populatedFields < 6) reasons.push(`only ${p.populatedFields} fields`);
if (p.useCases.length < 3) reasons.push("fewer than 3 use cases");
if (p.uniqueShare < 0.4) reasons.push(`unique share ${p.uniqueShare.toFixed(2)}`);
return { index: reasons.length === 0, reasons };
}
Feed index into robots metadata and sitemap inclusion, and write a test that fails the
build if more than an agreed share of pages fall below threshold. Log reasons to a CSV
for the content team.
7. Staged rollout
- Launch 50–200 of the strongest pages (highest data completeness and demand).
- Wait 3–6 weeks. In Search Console, track indexed vs submitted for that sitemap, impressions, and “Crawled - currently not indexed” counts.
- If most are indexed and earning impressions, expand in batches; if not, improve the template before adding more pages. More pages never fix a weak template.
8. Prune and improve
Quarterly: pages with no impressions after 6 months and below-median data → enrich,
merge into a hub, or noindex/remove (404/410, or 301 to the hub when there is a close
match). Track the section’s indexed ratio as the health metric.
Gotchas
- Indexing everything on day one invites a sitewide quality reassessment; poor pages can drag down the section and the domain.
- Near-duplicate location pages (“Plumber in Springfield” × 500 with the same text)
are doorway pages. Local pages need real local data (see
local-seo). - Pages generated for combinations that do not exist (an integration that is not built) mislead users; show “not supported” pages only if people search for that answer, and say so plainly.
- Faceted combinations explode. Generate only combinations with demand or data.
- Rendering thousands of pages at request time without caching can hammer the database when Googlebot crawls; prerender or cache with revalidation.
Output
**Pattern:** /integrations/[a]/[b] — 2,140 candidate pairs, 312 pass thresholds.
**Gate:** fields ≥ 6/10, use cases ≥ 3, unique share ≥ 0.40 (lib/pseo/gate.ts + test).
**Phase 1:** 150 pages live, sitemap-integrations-1.xml; hub /integrations/ links all.
**Measure at week 4:** indexed/submitted, impressions, crawled-not-indexed.
**Below threshold report:** pseo-gaps.csv (1,828 pairs with missing fields). What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Programmatic SEO"
short_description: "Thousands of pages, none of them thin"
default_prompt: "$programmatic-seo Generate integration pages for every pair of apps in our integrations table" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.