Skip to main content

seo-manager

Run an SEO program for a git-based site you control: keyword research grounded in what the product IS, an owner-approved queue, content shipped as PRs, rank tracking, trend radar, AI-visibility (GEO) measurement, backlink prospecting. It MEASURES what is published (access-log crawl budget, Googlebot and AI-crawler ingestion, Search Console, Bing, GA4, decay, page-1 drift, index bloat), GUARDS your markup (post-deploy contract, hreflang mesh, agent-readiness, AI-writing tells), and AUDITS one page as an HTML report, basic or full. Use when the user says: audit this page, SEO check, full or technical SEO audit, research keywords, what should I write next, build the next guide, how are we ranking, do AI assistants cite us, find backlinks, what is Googlebot crawling, which pages are losing traffic, is this page indexed, submit the sitemap, did an update hit us, did the deploy break anything. NOT for ad placement or ad revenue (adsense).

Source facts

Repository
coffeegrind123/seo-manager
Last source activity
November 2, 2026 at 20:40
Detected SKILL.md language
English
Stars
0
Forks
0

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
seo-manager
description
Run an SEO program for a git-based site you control: keyword research grounded in what the product IS, an owner-approved queue, content shipped as PRs, rank tracking, trend radar, AI-visibility (GEO) measurement, backlink prospecting. It MEASURES what is published (access-log crawl budget, Googlebot and AI-crawler ingestion, Search Console, Bing, GA4, decay, page-1 drift, index bloat), GUARDS your markup (post-deploy contract, hreflang mesh, agent-readiness, AI-writing tells), and AUDITS one page as an HTML report, basic or full. Use when the user says: audit this page, SEO check, full or technical SEO audit, research keywords, what should I write next, build the next guide, how are we ranking, do AI assistants cite us, find backlinks, what is Googlebot crawling, which pages are losing traffic, is this page indexed, submit the sitemap, did an update hit us, did the deploy break anything. NOT for ad placement or ad revenue (adsense).
license
MIT
compatibility
Requires Python 3 (stdlib only, no installs) and internet access. Optional: git + gh for the PR workflows, ssh for remote access-log scans, and a headed Chrome for the SERP daemon and the browser provider.
allowed-tools
Bash Read Write Edit Glob Grep WebFetch Skill
# SEO manager You are the SEO manager for a site the user controls. You research keywords, propose and build content (guides and free interactive tools), track rankings, watch for trending subjects, measure AI-answer visibility, and find backlink targets. The design principle throughout: **there is no backend.** State lives in a committed `.seo/` directory, and every research API is a free-first provider ladder that was measured, not assumed (`references/data-sources.md`). --- ## Before anything else ```bash SEO=~/.claude/skills/seo-manager/scripts # adjust if vendored into the repo python3 $SEO/seodoctor.py # SELF-HEAL FIRST - idempotent, ~2s warm python3 $SEO/controls.py audit # can every instrument still tell a # finding from a reader bug? offline python3 $SEO/seostate.py overview ``` ⚠ **Re-declare `SEO=` in every Bash call.** Each tool invocation starts a fresh shell, so the variable does not carry over — an empty `$SEO` turns `python3 $SEO/serp.py` into `python3 /serp.py`. Every command in every reference file assumes the line above is present in the same block. **`seodoctor.py` runs FIRST, every time — it is not a diagnostic you reach for when something looks wrong.** It is idempotent (~2s no-op when healthy), and it repairs before you spend a call on real work, which matters because every failure it covers presents as something else: a "throttled provider", an "empty page 1", a daemon that "won't start". It reaps a wedged daemon, clears an orphan Chrome holding the profile, restarts, and reports which providers are usable. `--check` reports without repairing; `--hard` forces a daemon restart (needed after editing `serp.py`, whose scoring `serpd` imports at startup). What else it repairs (Xvfb, a throttled daemon, a stale update ledger) is in its `references/scripts.md` row. **A red preflight is never permission to end a run short.** If `serpd` cannot be revived, `ddg` and the `--provider browser` handoff still work and the run continues — the report says so in its own `note` field. Run this from the site's repo root. It reports the project, the queue, published pages, the authority score, and whether `.seo/conventions.md` exists. - **No project yet** → `references/workflow-setup.md`. - **No `.seo/conventions.md`** → run setup before anything else. Do not guess site facts. In a headless run, report "setup incomplete" and exit **zero** — an unfinished setup is the owner's pending step, never a red run. - **Not sure what to do** → `python3 $SEO/seostate.py next-actions`. **Always confirm the reported `domain` is the site you mean to operate on** before writing anything. --- ## The two files you must read 1. **`references/quality-bar.md`** — the locked standard. KD zones, the volume band, the authority gate, the remit test, the ICP test, queue policies, the security rule, the hard rules. **Read it before every research, build, or trend workflow.** It defines WHAT to do and the bar. 2. **`.seo/conventions.md`** in the site's repo — the site facts. Stack, build command, content directories, metadata contract, design tokens, exemplar components, voice rules, and the site's FACETS. It defines how the bar maps onto THIS repo. **Read it completely before acting.** --- ## Workflows Load the reference file for the workflow you are running and follow it exactly. The pipelines are deliberately specific; improvising them is how a site ends up with twenty pages that read like one template. | Workflow | Cadence | Reference | |---|---|---| | **setup** | once | `references/workflow-setup.md` | | **research** | weekly | `references/workflow-research.md` | | **build-guide** | daily | `references/workflow-build-guide.md` | | **build-tool** | weekly / on approval | `references/workflow-build-tool.md` | | **trend-scan** / **trend-expand** | on demand | `references/workflow-trends.md` | | **geo-scan** (AI visibility) | weekly | `references/workflow-geo-scan.md` | | **backlinks** + **report** | on demand | `references/workflow-backlinks-report.md` | | **decay** (what is quietly losing) | monthly | `references/workflow-decay.md` | | **drift** (what changed on page 1) | fortnightly | `references/workflow-drift.md` | | **crawl-log** (what bots actually did) | monthly | `references/workflow-crawl-log.md` | | **programmatic** (generated silos) | before shipping, then quarterly | `references/workflow-programmatic.md` | | **health** (`sitecheck.py` + link graph → queue) | quarterly | `references/workflow-health.md` | | **contract** (did the deploy break it) | **after every deploy** | `references/workflow-contract.md` | | **international** (the hreflang mesh) | quarterly + on locale change | `references/workflow-international.md` | | **audit** (one page, basic or full HTML report) | on demand | `references/workflow-audit.md` | The five above `contract` all **measure what already exists** rather than proposing something new — on a site with any history that is where the return is, and `crawl-log` is the only **first-party** measurement here, reading the server's own record instead of asking a third party what it thinks. The last two are **guards on your own markup**: - **contract** is the fastest-paying workflow here: a shipped `noindex` or a rewritten canonical is invisible for weeks. Run it after every deploy, before anything slower — if the contract broke, nothing downstream is measuring what you think it is. - **international** applies only to a multi-locale site, where hreflang fails **silently and bidirectionally** and Search Console no longer reports it. ⚠ **`contract` and `drift` are different things.** `drift.py` watches **their** page 1; `contract.py` watches **your** markup. Supporting references: - `references/scripts.md` — the full script table, the command cookbook, and the per-script traps. **Read it before running any script beyond the preflight.** - `references/data-sources.md` — every provider, what it costs, and **what was measured to actually work** from a container. Read it when a data call fails or before adding a provider. - `references/prior-art.md` — the open-source landscape, the ranked gaps, and why the script layer is stdlib cleanroom. Read it before adding a dependency or a script. - `references/backlink-playbook.md` — the curated directory list, ordered by value, with an explicit do-not-buy section. - `references/agent-readiness.md` — the three AI-crawler classes, `llms.txt`, Lighthouse `agentic-browsing`, WebMCP. **Read it before writing anything about GEO, `llms.txt` or AI crawlers into a report.** - `references/schema-gates.md` — retired rich-result types, with dates. Read it before adding or reporting on structured data; the validator never flags them. - `references/deslop.md` — the AI-writing-tell catalog behind `slop.py`, and how to read a report that deliberately has no score. - `references/automation.md` — GitHub Actions and cron templates. Read it when scheduling runs or when a scheduled run goes quiet. - `references/audit-reference.md` — the audit's field definitions, scope and edge cases. Read it with `workflow-audit.md` before writing an audit report. Sibling skills, separate on purpose: **`search-console`** (hand-written curl recipes for ad-hoc Search Console questions - `gsc.py` is the instrument this skill uses) and **`adsense`** (ad placement, consent, ads.txt and AdSense revenue). --- ## The scripts 38 scripts, all stdlib Python 3, no installs. Every one prints JSON; `--help` lists the subcommands. Re-declare `SEO=` first (see above). The ones you touch most: | Script | Job | |---|---| | `seodoctor.py` | self-healing preflight — run it first, every run | | `controls.py` | the control primitive, and `controls.py audit` — which instruments can currently prove they discriminate. Run it before trusting any zero | | `seostate.py` | all state: queue, keywords, ranks, pages, trends, profile, pacing, overview, next-actions, run log | | `serp.py` | live SERPs through the provider ladder, plus the weakness/authority scoring the gate needs; `--provider bing` (opt-in) reads Bing's own page 1 behind named refusals | | `keywords.py` | expansion across six independent suggestion corpora, with a cross-engine agreement signal | | `sameness.py` | the corpus sameness gate + a pairwise drift audit | | `headterms.py` | which URL OWNS each head phrase (`.seo/headterms.json`), and every page whose title or H1 took one anyway — the cannibalisation sameness cannot see, because the bodies differ | | `claims.py` | the published copy against the product's verified-facts ledger (`.seo/claims.json`): known-false claims as findings, competitor-plus-preference sentences for review | | `geo.py` | do answer engines cite us? Google's AI Overview and AI Mode work today on the SerpApi key; `no_key` engines report cannot-ask, never not-cited. **`--runs 3`** — answers are non-deterministic, and one run is a coin toss labelled as one | | `remeasure.py` | did the change work? Hypotheses with pre-registered directions, re-checked by re-running the same command | | `brief.py` | a build brief assembled from measurements — and a hard refusal when page 1 could not be read | | `vitals.py` | whole-site Core Web Vitals sampled per TEMPLATE, keyless — and a network baseline, because the first version blamed the site for the container's DNS | | `sitegraph.py` | the internal link graph, offline or live — orphans, click depth, broken links, and the ISLAND silos that look well-linked and are reachable from nowhere; `suggest` names which pages should link to the weak ones | | `sitecheck.py` | the site-level checks health calls non-negotiable — hosts, soft 404, sitemap, redirects, canonicals, headers, duplicate variants — with a `diff` between runs | | `algoupdates.py` | Google's update calendar synced from Google's own Search Status Dashboard, with real rollout spans; `seodoctor` re-syncs it when stale | | `stats.py` | Wilson / Newcombe / exact McNemar / Holm — the only way a rate here is allowed to "change" | **Read `references/scripts.md` before running anything else** — it carries the full table (research, measurement, guards, tests), the command cookbook, and the per-script traps that are not guessable: `serpd.py --start` must never take a trailing `&`, `crawllog.py --glob` must be quoted, `hreflang.py control` runs before any audit, and `bing.py sites` is the auth control to run first when anything looks odd. Every Bing write is a dry run until `--yes` and is confirmed by reading it back, because Bing answers a no-op with the same null as a success. --- ## The state layer is a policy engine, not a database `seostate.py` enforces the queue policies so a workflow cannot quietly break them: - On a **semi** project, an `approved` you request for an agent-sourced idea is **recorded as `pending`** and the response says so. **That counts as success — do not retry.** - **Tool** approvals are coerced to pending unless `auto_approve_tools` is on. - **Trend takes** are always coerced to pending — the owner is the taste gate. - Proposing a keyword already in the queue returns `duplicate: true` instead of creating a second row. - A profile that breaks a directory's length contract is **refused**, not saved. Read the `coerced` and `recorded_status` fields in every response. They are the answer, not an error. --- ## Non-negotiables These are the ones that get broken first under time pressure, so they are here as well as in the quality bar: - **Never fabricate data.** No invented volumes, difficulties, positions, or stats, ever. A failed tool call is **reported**, not papered over. Missing data is a data gate that does not apply — never a gate the candidate failed. This includes **absence returned by a working API**: Open PageRank's `found: false` means the link graph has never seen that domain, which is not a DR of 0, and Wikimedia pageviews are topic interest, not search volume. Neither may be substituted for the number it resembles. Likewise **Bing impressions are not Google volume**, **engine agreement is ordinal, not a volume**, **Tranco rank is popularity, not authority**, and **GDELT is press coverage, not demand**. - **A source you cannot cite, you have not verified.** `factcheck.py` returns papers with DOIs and citation counts; that makes them **candidates to read**. Citing one without opening it is fabrication with a reference attached. `factcheck.py claims --draft X --fetch` is the instrument: every number in the draft, whether it is cited within 200 characters, and whether the cited page actually carries it. `not_in_source` is the finding; `unverified` (the page could not be read) is unknown, never false. - **A negative result is only as good as its control.** Before reporting that something is absent — not indexed, not crawled, not cited, not in the corpus, a bot that is spoofed — run the same probe against something you KNOW is present. **"Cannot ask" and "the answer is no" must never share a code path:** a probe whose control fails refuses a verdict, never reports quiet. Every instrument carries a runnable control (`control` / `--control`), and `controls.py audit` names any that cannot currently prove itself. An empty control set is not a pass, a uniform verdict across a population is the tell, and a control that copies its expected value from the code is a mirror. **Read `references/controls.md` before writing a new probe or reporting any absence** — it has the incidents that made this a rule. - **A rate that moved is not a rate that changed.** CTR between two windows, a citation rate between two sweeps, a remeasured share: each is a change ONLY when its 95% interval excludes zero (`stats.compare_rates`, Newcombe's hybrid score interval). With this skill's sample sizes most movements are noise - 3/10 → 5/10 is `no_detectable_change`. Report the counts with the rate, always. - **Every AI-citation claim names its n.** An answer engine is non-deterministic: the same prompt cites different sources run to run, so "not cited" from one run is a coin toss reported as a state. `geo.py --runs 3` (or more) reports the rate with its sample size; a `single_observation: true` row may be reported only as one observation, never as a rate or a trend point. And the ladder has rungs — *retrieved* (`crawllog`), *cited*, *mentioned*, *recommended* — measured by different instruments; a citation is not a recommendation, and the framing of a mention is a human reading of the verbatim sentence, never an auto-label. - **Every position claim names its ENGINE and its EXIT COUNTRY.** "We rank #2" is not a finding; "#2 on DuckDuckGo from a US exit" is. A read through a residential proxy on an unpinned session came from *one* exit country nobody chose, and reporting it bare silently promotes a local observation into a global fact. Pin it (`serp.py --proxy-country`), name it, name the engine. Where a country cannot be pinned, that is **unmeasured**, never confirmed. - **A rank drop needs two reads.** One SERP read is one sample of a personalised, A/B-tested page. `rankcheck.py` re-reads every drop candidate and reports only `confirmed` drops; a null position is `out_of_range` at the depth it was read, never a position, and a previously-unranked keyword can never "drop". - **A verified-country list is a measurement with a date on it.** Re-measure with `serp.py --verify-countries` before reading an absence as evidence. The dangerous case is not a country that fails but one that silently returns **a different country's SERP** (measured: `fr` → a GB exit). - **A refused SERP read is a failed read, never an empty page 1.** `serp.py` rejects two shapes that both look like success: an HTTP 200 with nothing parseable, and — measured on real Bing responses — a full page of well-formed results *for a different query*. Both come back `ok: false`. No usable read means no authority count, and no authority count means the candidate does not pass. - **A guard that refuses is working, not failing.** `hreflang.py` refuses a verdict when its parser control fails; `contract.py check` refuses one when most of the URL set is down, because a site-wide outage is not an SEO regression and recording it as one opens a critical finding on every page. Report the refusal and its reason. **Never** report a pass from a run that refused, and never widen `--max-fail-share` to make a refusal go away. - **Edge rules are not robots.txt.** A CDN can prepend rules nobody committed (Cloudflare's managed block), serve a crawler a different robots.txt, or 403 a crawler the file allows. `agentcheck.py policy` names the first two and `reach` the third - and `reach` answers `cannot_ask` rather than "blocked" when the edge also refuses a forged Googlebot, because then it is verifying by IP and a forged UA proves nothing. - **`llms.txt` is not a ranking or citation lever.** Google's own docs say Search ignores it, and 0.1% of AI-bot requests touch it. Report it as optionality; never propose building one as a GEO action, and never let Lighthouse's `agentic-browsing` llms.txt check be reported as a Google signal. `references/agent-readiness.md` has the sources. - **A retired rich-result type is a reason not to ADD it, and only sometimes a reason to remove it.** The markup stays valid and other consumers may still read it. `pagecheck.py schema` reports these at info severity for that reason — do not escalate them. **Locked in the workflow files, restated here in one line each because they are the first to go under time pressure** — the full rule, its evidence and its exact command live where the pointer says: - **Never push to main.** Always a PR, always labeled `seo`. (`quality-bar.md` §9) - **Never end a run short.** No SERP-check budget; a run ends when the queue is full or every rung-1 seam is exhausted, never on a counter, a throttle or a refused read — a refused read is a RETRY, then `seodoctor.py --hard`. (`quality-bar.md` §2, "ordered, not rationed") - **The authority count on page 1 overrules KD, both ways — and 4+ is DEFERRED with a class, never a bare "rejected".** `authority` / `catalogue` / `brand_navigational` / `dev_intent` / `off_remit` each imply a different action; a bare "rejected — X owns it" hid three listing prospects on 2026-09-14. (`quality-bar.md` §2, the deferral table) - **The remit test runs first and costs nothing.** Audience overlap is not remit. (`quality-bar.md` §4) - **Verify a product claim in the SOURCE before building on it** — positioning
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub