/programmatic-batch — Matrix-driven service page generator
Programmatic SEO done right: generate N pages from a structured variable matrix while enforcing the anti-doorway-page rules that distinguish legitimate scaled content from spam. The skill REFUSES to ship any cell that can't be filled with genuine local data — that refusal is the feature, not the bug. Programmatic SEO without this guardrail is the fastest way to trigger a Google manual action.
This skill is the natural pair to /cluster (informational scale, low risk) — /programmatic-batch is commercial scale, high risk if mishandled.
Inputs
Required (asked at start if not given):
- Site — must match a folder under
sites/[name]/withservice-business: true. Skill refuses on non-commercial sites. - Matrix file — path to a CSV in the site folder (default:
sites/[site]/matrix.csv). Schema documented below.
Optional (per-run overrides):
--max-batch=N— caps how many cells to ship in this run. Default: derived from footprint perSEO_GUIDE.mdSection 4.2 (service-area: 5/week max; multi-location: 5/week max; single-location/national-online: full matrix). The skill enforces pacing — exceeding the cap requires explicit--override-pacingflag.--cell-filter— comma-separated cell IDs to ship (e.g.,tapas-houston,tapas-katy). Useful for re-running specific cells after fixing local-data gaps.--dry-run— output the matrix expansion + uniqueness audit only; don't generate full drafts. Use for matrix validation before committing to a full batch.- Keyword tool — overrides the site's default. Used for SERP analysis per cell.
The matrix CSV schema
The matrix CSV is the source-of-truth for what pages get generated. Required columns:
cell_id,axis_1_value,axis_2_value,primary_keyword,local_data_file,status
tapas-houston,Tapas Catering,Houston,tapas catering houston,_local-data/tapas-houston.md,ready
tapas-katy,Tapas Catering,Katy,tapas catering katy,_local-data/tapas-katy.md,draft
paella-houston,Paella Catering,Houston,paella catering houston,_local-data/paella-houston.md,ready
...
cell_id— unique identifier (kebab-case). Used as slug source.axis_1_value/axis_2_value— the two dimensions. For most matrices, axis 1 = service, axis 2 = city. The skill is agnostic to dimension semantics but reads fromsite-info.md→ "Matrix axes" for naming.primary_keyword— the target keyword for this cell. Must be unique across the matrix (no two cells targeting the same keyword).local_data_file— path (relative to the site folder) to the unique local-data file for this cell. REQUIRED. The skill refuses any cell with this field empty.status—ready(skill will process) |draft(still gathering local data — skill skips with a flag) |published(skill skips silently) |skipped(manually excluded with reason in notes.md).
Optional columns:
hero_image— path to a unique hero image for this cell.volume— keyword volume from research (used for prioritization).peak_months— seasonality marker.
The local-data file schema
Each cell has its own _local-data/{cell_id}.md file. This is the file that makes the cell pass anti-doorway rules. Required fields:
---
cell_id: tapas-houston
unique_opening_words: 247 # MUST be ≥ 200 per anti-doorway rule 1
landmarks:
- "Discovery Green"
- "Memorial Park"
- "the Heights neighborhood"
local_faqs:
- q: "Do you cater tapas events in [specific Houston suburb]?"
a: "[answer]"
- q: "What's the typical guest count for a [Houston-context] tapas event?"
a: "[answer]"
nap:
phone: "555-555-0100"
area_code_matches_city: true
address: "[real address]"
testimonials:
- name: "Maria L."
neighborhood: "Montrose"
quote: "[verbatim, dated, consented]"
consent_date: "2025-11-12"
hero_image: "/images/tapas-houston-hero.jpg"
hero_image_unique: true # MUST be true — not shared with another cell
intervention_hours: 12 # how many hours of real local-data gathering went into this cell
---
# Unique opening (≥ 200 words)
[Case study OR neighborhood reference OR real customer quote OR local stat — must be city-specific, not boilerplate-with-city-name-swap]
# City-specific details
[Anything else uniquely local — Houston-specific dietary preferences, common Houston event venues, regional pricing context]
If ANY of these required fields is missing or boilerplate (the skill detects boilerplate via 90%+ similarity check across cells in axis_2), the cell is rejected.
Workflow
Step 0 — Cron-mode detection (if invoked with --cron)
Cron mode is supported but discouraged for this skill — programmatic batches benefit from human approval per-batch. If invoked with --cron, the skill defaults to --dry-run mode and writes the dry-run output to _inbox/programmatic-dry-run-{date}.md for human review. Idempotency key: (site, matrix-name, date). Audit log per scripts/lib/audit-log.mjs conventions.
Step 1 — Read context (Rule 1)
Load in order:
CLAUDE.md(toolkit root)SEO_GUIDE.md— Section 4.1 (anti-doorway rules), Section 4.2 (volume + pacing), Section 4.3 (NAP by footprint)on-page-seo.md(every cell satisfies Tier 1)sites/[site]/site-info.md— Footprint, NAP, Architecture pointer, Matrix axessites/[site]/architecture.md(REQUIRED for service-business sites with > 3 keywords perCLAUDE.md)- Resolved
references/(root + per-site overrides) - The matrix CSV
sites/[site]/used-keywords.mdsites/[site]/notes.md
If any required file is missing → refuse and report which.
Step 2 — Footprint guard
Refuse if site-info.md declares any of:
footprint: national-onlineAND matrix has city axis (no localization needed; programmatic is wasted effort)footprint: single-locationAND matrix has > 1 location row (use/serviceper service instead)service-business: false(programmatic on non-commercial sites = doorway pages)
Step 3 — Matrix audit (pre-flight)
For every row with status: ready:
- Local-data file exists? If no → flag, skip cell, write to
_inbox/programmatic-missing-localdata-{cell_id}.md. - All 6 anti-doorway requirements satisfied? Walk the local-data file:
- Unique opening ≥ 200 words? (parse word count)
- Landmarks list ≥ 3 entries? (parse YAML)
- Local FAQs ≥ 2 entries with city-specific phrasing? (parse YAML + regex check for cell axis value)
- NAP phone matches city area code? (parse + cross-check against axis_2 region map in site-info.md)
- Testimonials ≥ 1 with neighborhood + consent date? (parse YAML)
- Hero image marked
hero_image_unique: true? (parse YAML)
- Boilerplate detection. Compute pairwise Jaccard similarity of the "Unique opening" section across all
readycells. Any pair > 0.7 similarity → flag both, refuse to ship until rewritten. - Keyword cannibalization check. Cross-reference every
primary_keywordagainstused-keywords.md. Already-published keywords → flag, refuse. - Architecture consistency. Every cell's URL must fit the site's
architecture.mdURL pattern (catalog:/[category]/[subcategory]/[leaf]/; non-catalog: per architecture.md decision).
Step 4 — Volume cap + pacing
Apply SEO_GUIDE.md Section 4.2 hard rules:
- Service-area: max 30 cells initial total, max 5 new per week
- Multi-location: max 50 cells total, max 5 new per week
- Single-location/national-online: not applicable to programmatic (refused at Step 2)
If batch size exceeds weekly cap → split into multiple weekly waves. Report:
Matrix: tapas × Houston-suburbs (11 cells ready, 9 pass uniqueness)
Footprint: service-area → 5/week max
This batch ships: 5 cells. Remaining 4 cells queued for next week's run.
--override-pacing flag bypasses the cap. Skill warns explicitly: "Override pacing flag set. Shipping N cells in one batch. Recommended ramp is 5/week — exceeding this raises algorithmic risk per Section 4.2."
Step 5 — Per-cell SERP analysis (only for cells that passed Step 3)
For each ready cell, in parallel where possible (skill batches into groups of 5 SERP queries):
- Search Google (incognito, target region from
site-info.md) forprimary_keyword. - Identify top 3 organic results. Skip Reddit, Quora, forums, Wikipedia, brand directories.
- Extract: median word count, H2 outline, FAQ count, image count.
- Note one differentiation opportunity per cell (a section the top 3 omit but the local-data file supports).
Step 6 — Wireframe per cell (delegated to /wireframe per cell)
Each cell gets its own wireframe per CLAUDE.md Rule "wireframe before content." The skill batch-invokes /wireframe with the cell's parameters. Wireframes share a SKELETON template (consistent zone structure across cells) but each gets unique CONTENT slots filled from the local-data file.
Step 7 — Per-cell content generation
For each cell, the skill follows the same Step 7+ flow as /service, with these batch-specific differences:
- Voice resolution is shared across the batch (one resolution pass), but cells in multilingual sites generate per-language fan-out per cell.
- Internal linking — each cell links to:
- The parent service page (from architecture.md)
- 2–3 sibling cells (lateral links — pillar pattern at the cell level)
- 1–2 informational blog posts that match the cell's intent
- Schema — each cell's
LocalBusinessschema uses the cell's NAP from local-data file;areaServedlists the specific city. - Unique content enforcement — after generating each cell's body, the skill RE-RUNS the boilerplate check (Step 3 item 3) on the GENERATED CONTENT, not just the local-data file. Any cell with > 0.6 Jaccard similarity to a sibling cell's body → rejected, regenerated with explicit "increase city-specific content density" instruction.
Step 8 — Pre-ship batch audit
Before any cell ships, validate batch-wide:
- Every cell passes Tier 1 (16 items each).
- Schema validates on every cell (Google Rich Results + Schema.org Validator).
- Voice anti-AI check passes on every cell (universal rules from
references/voice.md). - No keyword cannibalization within the batch (every cell's primary keyword is distinct AND distinct from
used-keywords.md). - Cadence check: this batch + previous 7-day publish count ≤ footprint's weekly cap.
If ANY batch-level check fails, the skill refuses to ship ANY cell. Single-cell failure flags that cell but other cells can still ship.
Step 9 — Output
Drafts land in: sites/[site]/_drafts/programmatic-{matrix-name}/{cell_id}/ per cell, with:
meta.json— slug, primary keyword, axis values, local-data source pointerwireframe.mdcontent.md(markdown body) orprompt.md(Lovable-format) depending on publishing methodschema.jsonld- Plus per-language subfolders if multilingual
Top-level batch summary at sites/[site]/_drafts/programmatic-{matrix-name}/BATCH-SUMMARY.md:
- Cells shipped this batch (count + IDs)
- Cells flagged + reason per cell
- Pacing report (this batch / weekly cap / cumulative this month)
- Next-batch recommendation
Step 10 — Update trackers
- Append every shipped cell's primary keyword to
used-keywords.mdwith statusdraft-staged. - Append batch summary to
notes.mdunder "Programmatic batches" heading. - Do NOT mark cells as
status: publishedin the CSV — that happens after human approval + publish. The skill marks themstaged.
Step 11 — Report
✅ Programmatic batch complete: {matrix-name}
Site: {site}
Cells ready: N
Cells passed uniqueness: M
Cells shipped this batch: K (pacing cap applied)
Cells flagged: P (reasons in BATCH-SUMMARY.md)
Pacing this week: K/{cap}
Next batch eligible: {date}
Output: sites/{site}/_drafts/programmatic-{matrix-name}/
Refusal conditions
This skill refuses to ship if:
- Site is not
service-business: true. - Site is
national-onlineand matrix has city axis. - Site is
single-locationand matrix has > 1 location. architecture.mdmissing on a service-business site with > 3 service-keywords.- Any cell's local-data file missing any required field.
- Boilerplate similarity > 0.7 between any pair of cells.
- Keyword cannibalization detected (any matrix keyword already in
used-keywords.md). - Batch size exceeds weekly cap AND
--override-pacingnot set.
Routine versioning + idempotency contract
Idempotency key per cell: (site, cell_id, language). Skip cells where _drafts/programmatic-{matrix-name}/{cell_id}/meta.json exists with status: staged or status: published AND today's date.
The matrix CSV itself is the routine's state: a cell flips from ready → staged → published over its lifecycle. The skill never edits cells with status: published (those are live; use /refresh instead).
Templates
Scaffold templates live at templates/programmatic/:
matrix.csv— schema-correct example matrix (10 rows, partly populated)_local-data/example-cell.md— schema-correct example local-data fileREADME.md— user-facing setup instructions for adopting programmatic batch on a new site
Run cp -r templates/programmatic/ sites/[site]/programmatic/ to scaffold.