技能创建器
skill-creator
创建、改进和评估自定义技能,把专业流程转化为可重复使用的 AI 能力。
programmatic-seo【数据与研究】Plans, builds and vets sets of search landing pages generated from a dataset and a template: integration, location, directory and glossary pages. Decides whether a page set deserves to exist, sets the data each page must carry, checks it against Google's spam policies on scaled content, doorway pages and site reputation, gates thin pages before publi…
独立 GitHub 快照 · 自动分类 · 以原作者文档为准
Programmatic SEO publishes many pages from one template and one dataset to answer a family of searches that share a shape ("payroll software for dentists", "kayak rental in Tacoma"). It works when every row of the dataset holds enough facts of its own to deserve a page. It fails, sometimes for the whole site, when the pages differ only in the word that was swapped in.
Be honest with the user from the first message: Google does not promise to index a page it has crawled, a large generated set often ends up crawled and then left out of the index, and Google's spam policies treat mass-produced pages with no added value as abuse however they were made. This skill produces a go or no-go decision, a data specification, a template outline, a gate that holds thin pages back, and a batch rollout with indexing checks.
Read .claude/product-marketing-context.md if it exists. Then find out, from the repository and
the user:
Answer each question with evidence. A "no" on any of the first three stops the project or shrinks it.
| Question | Passes when |
|---|---|
| Is there demand? | Search Console shows impressions for the pattern, or a keyword tool shows volume for at least a sample of rows |
| Does each row have its own substance? | Several facts per page that exist only for that row: measurements, prices, availability, counts, reviews, photos, steps that differ |
| Can the visitor finish the task there? | The page answers the query or lets them act, without a click to "the real page" |
| Would it exist without search traffic? | Customers or support would link to it anyway |
| Is the data yours to publish? | First-party or licensed; public data only when you add analysis nobody else shows |
Paraphrased from the Search Central spam policies page (last revised August 2026). Violations are found by automated systems and human review; the stated consequence is ranking lower or not appearing in results at all.
One row is one page. Write the specification before any template work:
page_set: trail-guides
pattern: "{trail_name} trail guide"
url: /trails/{region_slug}/{trail_slug}
source: trails, trip_reports, gps_tracks (first-party)
required: # a row missing any of these gets no page
- gps_track
- length_km, ascent_m, surface
- trip_reports_last_12_months >= 3
- photos >= 2
optional_blocks: # rendered only when the data is present
- seasonal_closures
- water_sources
- nearby_trails (computed from coordinates)
refresh: nightly; lastmod changes only when a field above changes
Rows that fail required are not published. Fewer strong pages beat full coverage.
Render the candidate pages, export the main content of each (no header, navigation or footer) as
one JSON object per line with url, title and text, and run the gate:
"""Usage: python3 page_gate.py pages.jsonl"""
import json, re, sys
from collections import Counter
pages = [json.loads(line) for line in open(sys.argv[1]) if line.strip()]
def shingles(text, n=5):
words = re.findall(r"[a-z0-9]+", text.lower())
return {" ".join(words[i:i + n]) for i in range(len(words) - n + 1)}
sets = [shingles(p["text"]) for p in pages]
seen_on = Counter(s for page_set in sets for s in page_set)
template = {s for s, n in seen_on.items() if n >= max(2, 0.2 * len(pages))}
titles = Counter(p["title"].strip().lower() for p in pages)
failed = 0
for page, page_set in zip(pages, sets):
own = page_set - template
share = len(own) / max(1, len(page_set))
problems = []
if share < 0.5:
problems.append(f"{share:.0%} of the text is specific to this page (minimum 50%)")
if len(own) < 60:
problems.append(f"{len(own)} page-specific phrases (minimum 60)")
if titles[page["title"].strip().lower()] > 1:
problems.append("title is used by another page")
failed += bool(problems)
print(("HOLD " if problems else "PASS ") + page["url"] + "".join(f"\n {p}" for p in problems))
print(f"\n{len(pages)} pages, {failed} held back")
sys.exit(1 if failed else 0)
Text that recurs on a fifth of the pages counts as template. The two thresholds are this skill's working defaults, not Google numbers; Google states it has no preferred word count. Held pages get more data or are dropped. The gate catches sameness, not falsehood: read a sample of passing pages in full and check their facts against the source rows.
rel="canonical".priority and changefreq, and uses lastmod only when it is consistently accurate.noindex to save crawl budget: Google still fetches the page to see the rule. A page
blocked in robots.txt cannot show its noindex at all.Publish the strongest rows first (50 to 200 pages), submit their sitemap, and wait four to eight weeks before the next batch. In the Page indexing report, filter by that sitemap and read the reasons:
| Reason shown | What it means | What to do |
|---|---|---|
| Indexed | In the index; says nothing about ranking | Watch impressions per page |
| Crawled - currently not indexed | Fetched, then left out. Google: "no need to resubmit" | When it dominates the batch, the template is the problem: add substance or cut rows |
| Discovered - currently not indexed | Known, not fetched yet | Check server speed and internal links; too many URLs offered at once |
| Duplicate without user-selected canonical / Google chose different canonical | Judged a copy of another page | Merge the rows or make the pages differ in substance |
| Soft 404 | Looks empty | Raise the required bar or return a real 404 |
The report lists at most 1,000 example URLs per reason. For exact per-URL status use the URL
Inspection API (POST https://searchconsole.googleapis.com/v1/urlInspection/index:inspect, fields
coverageState and googleCanonical), limited to 2,000 requests a day and 600 a minute per
property. The Indexing API cannot help: it accepts only job posting and livestream pages.
Expand only when most of a batch is indexed and earns impressions. If it is not, fix the template or stop; publishing the remaining rows adds weight to the same verdict.
Return, in this order: the decision with its evidence; the dataset specification; the template outline (fixed and data blocks); the gate result; the URL and sitemap layout; the batch schedule with the pass condition for each batch; and what will be pruned if a batch fails.
Request: "Kestrel Trails has 1,840 trails in the database. Make a page for each so we rank for '[trail] trail guide'."
The agent applies the specification from step 4. Only 620 trails have a GPS track, three recent trip reports and photos. Gate output on the first rendered batch:
PASS https://kestreltrails.app/trails/olympic-peninsula/lena-lake
PASS https://kestreltrails.app/trails/olympic-peninsula/mount-ellinor
HOLD https://kestreltrails.app/trails/olympic-peninsula/unnamed-spur-4
31% of the text is specific to this page (minimum 50%)
22 page-specific phrases (minimum 60)
120 pages, 9 held back
Plan delivered: publish 111 pages under /trails/{region}/{trail} with region hubs, sitemap
sitemap-trails-batch1.xml, review at week 6. Pass condition: 70% indexed and impressions on at
least half of the indexed pages. The 1,220 trails without reports stay in the app and out of the
sitemap until they collect three reports.
Request: "Wrenfield Plumbing has three branches. Generate 240 'plumber in [suburb]' pages for the metro area."
The agent declines the 240-page plan and says why: pages aimed at separate places that carry the same text and lead to one booking form are the example Google gives for doorway abuse. It proposes:
| Page type | Count | Row-specific content |
|---|---|---|
| Branch page | 3 | Address, opening hours, crew, service radius map, reviews for that branch |
| Service-area page | 14 | Only suburbs with 25 or more completed jobs: job mix, median response time, price range from invoices, two case notes with photos |
| Other suburbs | 223 | No page; listed as text on the branch page that serves them |
Request: "Formhollow published a template page for every document and industry combination, with AI-written introductions. Search Console shows 3,480 pages as 'Crawled - currently not indexed'."
Findings: 60 documents multiplied by 70 industries; the downloadable file is identical across industries for all but 212 combinations; the gate reports 9% page-specific text on the median page. Recommendation: keep the 60 document pages and the 212 combinations with a distinct file, redirect each removed combination to its document page, take the removed URLs out of the sitemap, and state on the remaining pages how the industry versions differ. The agent tells the user that recovery is not guaranteed and that re-evaluation of the remaining pages can take months.