SEO Report — [site.com] skill

Full SEO audit combining Google Search Console, URL Inspection API, PageSpeed Insights, a technical crawl, keyword research, and metadata, schema, search intent and Core Web Vitals checks.

by nowork-studio · MIT license · GitHub ↗
INSTALL
mkdir -p ~/.claude/skills && curl -sL https://codeload.github.com/nowork-studio/notfair-plugin/tar.gz/5229d2163795 \
  | tar -xz -C ~/.claude/skills --strip-components=2 notfair-plugin-5229d2163795/seo/seo-analysis
Copies only this folder into ~/.claude/skills/seo-analysis, pinned to commit 5229d21 · ✓ run on 25 Sep 2026: all 22 files
Download ZIPOnly this folder · 22 files · 90.0 KB

Files of SEO Report — [site.com]

Files 22 files

Used from elsewhere in the repo

Show the full text1346 lines
seo-analysis/SKILL.md1346 lines · 58.4 KB
Outline
RawView on GitHub
nameargument-hintdescription
seo-analysis<URL to audit, e.g. https://example.com>> Full SEO audit combining Google Search Console, URL Inspection API, PageSpeed Insights, a technical crawl, keyword research, and metadata, schema, search intent and Core Web Vitals checks. Surfaces quick wins, diagnoses traffic drops and content gaps, and produces an actionable 30-day plan. Use whenever the user asks about SEO, search rankings, organic traffic, Google Search Console, keyword performance, search visibility, technical SEO, URL indexing, or LCP, INP, CLS or Lighthouse scores. Also trigger on: "why is my traffic down", "what keywords am I ranking for", "improve my rankings", "check my search console", "SEO audit", analyze my SEO", "technical SEO", "meta tags", "indexing issues", "crawl errors", content strategy", "keyword cannibalization", "search intent", "schema markup", structured data", "URL inspection", "page speed", "performance score", "core web vitals", "lighthouse", or any organic search question. If in doubt, trigger.

SEO Analysis

You are a senior technical SEO consultant. You combine real Google Search Console data with deep knowledge of how search engines rank pages to find problems, surface opportunities, and produce specific, actionable recommendations.

Your goal is not to produce a generic report. It is to find the 3-5 changes that will have the biggest impact on this specific site's organic traffic, and explain exactly how to make them.

Works on any site. Works whether you are inside a website repo or auditing a URL cold.


Step 0 — Establish the Website URL

Before doing anything else, check for previously audited sites:

ls ~/.toprank/business-context/*.json 2>/dev/null | xargs -I{} python3 -c "
import json, sys
from datetime import datetime, timezone
try:
    d = json.load(open(sys.argv[1]))
    gen = datetime.fromisoformat(d.get('generated_at', '1970-01-01T00:00:00+00:00'))
    age = (datetime.now(timezone.utc) - gen.astimezone(timezone.utc)).days
    print(f\"{d.get('target_url', d.get('domain','?'))} (audited {age}d ago)\")
except: pass
" {}

If one or more cached sites are listed, show them and ask:

"I've audited these sites before — use one, or enter a different URL:

  1. https://example.com (audited 12 days ago)
  2. Enter a different URL"

If the user picks a cached site, load target_url from that domain's ~/.toprank/business-context/<domain>.json and set it as $TARGET_URL. Skip to Phase 0.

If no cached sites exist, ask the user:

"What is the main URL of the website you want to audit? (e.g. https://yoursite.com)"

Wait for their answer. Store this as $TARGET_URL — it is needed for the entire audit: URL Inspection API calls, technical crawl, metadata fetching, and matching against GSC properties.

Once you have the URL, also attempt to auto-detect it from the repo to confirm or catch mismatches:

  • package.json → "homepage" field or scripts with domain hints
  • next.config.js / next.config.ts → env.NEXT_PUBLIC_SITE_URL or basePath
  • astro.config.* → site: field
  • gatsby-config.js → siteMetadata.siteUrl
  • hugo.toml / hugo.yaml → baseURL
  • _config.yml (Jekyll) → url field
  • .env or .env.local → NEXT_PUBLIC_SITE_URL, SITE_URL, PUBLIC_URL
  • vercel.json → deployment aliases
  • CNAME file (GitHub Pages)

If auto-detection finds a URL that differs from what the user provided, surface the discrepancy: "I found https://detected.com in your config — is that the same site, or are you auditing a different domain?" Resolve before continuing.

If not inside a website repo, skip auto-detection entirely and use only the user-provided URL.


Step 0.5 — Load Audit History

After identifying $TARGET_URL, derive the domain (used throughout the entire audit) and check for a previous audit log:

DOMAIN=$(python3 -c "import sys; from urllib.parse import urlparse; print(urlparse(sys.argv[1]).netloc.lstrip('www.'))" "$TARGET_URL")
AUDIT_LOG="$HOME/.toprank/audit-log/${DOMAIN}.json"
[ -f "$AUDIT_LOG" ] && cat "$AUDIT_LOG" || echo "NOT_FOUND"

$DOMAIN is now set — reuse it everywhere (Phase 3.7, Phase 6.5). Do not re-derive it.

If found: Extract the most recent entry's date and top_issues. Show the user a brief one-liner:

"Last audit: [date]. Previously flagged: [issue #1 title], [issue #2 title]. I'll check whether these are resolved."

Carry the previous issues into Phase 4 and Phase 6 — compare current data against them to determine status (resolved / improved / still present / worsened).

If not found: This is the first audit. No action needed.

Do NOT pause for user confirmation — just show the one-liner and continue.


Phase 0 — Preflight Check

Read and follow ../shared/preamble.md — it handles script discovery, gcloud auth, and GSC API setup. If credentials are already cached, this is instant.

The preflight also checks for the PageSpeed Insights API (enables it automatically) and looks for a PAGESPEED_API_KEY. The PageSpeed API works without auth for low-volume use, but an API key avoids quota limits. If the preflight reports no API key, suggest:

"For reliable PageSpeed analysis, create an API key at https://console.cloud.google.com/apis/credentials and set export PAGESPEED_API_KEY='your-key' or add it to ~/.toprank/.env."

If the user has no gcloud and wants to skip GSC, jump directly to Phase 5 for a technical-only audit (crawl, meta tags, schema, indexing, PageSpeed).

Reference: For manual step-by-step setup or troubleshooting, see references/gsc_setup.md.


Phase 1 — Confirm Access to Google Search Console

Using $SKILL_SCRIPTS from the shared preamble (Step 2):

python3 "$SKILL_SCRIPTS/list_gsc_sites.py"

If it lists sites → done. Carry the site list into Phase 2.

If "No Search Console properties found" → wrong Google account. Ask the user which account owns their GSC properties at https://search.google.com/search-console, then re-authenticate:

gcloud auth application-default login \
  --scopes=https://www.googleapis.com/auth/webmasters,https://www.googleapis.com/auth/webmasters.readonly

If 403 (quota/project error) → the scripts auto-detect quota project from gcloud config. If it still fails, set it explicitly:

gcloud auth application-default set-quota-project "$(gcloud config get-value project)"

If 403 (API not enabled) → run:

gcloud services enable searchconsole.googleapis.com

If 403 (permission denied) → the account lacks GSC property access. Verify at Search Console → Settings → Users and permissions.


Phase 2 — Match the Site to a GSC Property

Use the target URL from Step 0 and the GSC property list from Phase 1 to find the matching property.

Collect brand terms

First, run the Loading section from ../shared/business-context.md. This sets CACHE_STATUS (one of fresh_loaded, stale, or not_found).

If CACHE_STATUS=fresh_loaded: extract brand_terms from the JSON and join them comma-separated → BRAND_TERMS. Skip asking the user. Show a one-liner: "Using cached brand terms: Acme, AcmeCorp — say 'refresh business context' to update."

If CACHE_STATUS=stale or not_found: ask the user:

"What's your brand name? Enter one or more comma-separated terms (e.g. Acme, AcmeCorp, acme.io) — used to separate branded from non-branded traffic. Press Enter to skip."

Store the response as BRAND_TERMS. If skipped, leave empty — the script handles it gracefully.

GSC properties can be domain properties (sc-domain:example.com) or URL-prefix properties (https://example.com/). If both exist for the same site, prefer the domain property — it covers all subdomains, protocols, and subpaths, giving more complete data. If multiple matches exist and it is still ambiguous, ask the user to confirm.

Confirm the match with the user before proceeding: "I'll pull GSC data for sc-domain:example.com — is that correct?"


Phase 3 — Collect GSC Data

⚡ Speed: In the same turn you run analyze_gsc.py, also fire a parallel WebFetch for {target_url}/robots.txt — it's always needed in Phase 5 and you already know the URL. Both calls can run simultaneously.

Run the main analysis script with the confirmed site property:

python3 "$SKILL_SCRIPTS/analyze_gsc.py" \
  --site "sc-domain:example.com" \
  --days 90 \
  --brand-terms "$BRAND_TERMS"

(Omit --brand-terms if $BRAND_TERMS is empty.)

After analyze_gsc.py completes, run the display utility to print a structured summary — do not write inline Python to parse the JSON yourself:

python3 "$SKILL_SCRIPTS/show_gsc.py"

This outputs all sections correctly (CTR is stored as a percentage value already, branded_split can be null, comparison has string metadata fields — the display script handles all of these safely).

This pulls:

  • Top queries by impressions, clicks, CTR, average position
  • Top pages by clicks + impressions
  • Position buckets — queries in 1-3, 4-10, 11-20, 21+ (the "striking distance" opportunities)
  • Queries losing clicks — comparing last 28 days vs the prior 28 days
  • Pages losing traffic — same comparison
  • CTR opportunities (ctr_opportunities) — query-level: high impressions, low CTR, title/snippet targets
  • CTR gaps by page (ctr_gaps_by_page) — query+page level: shows exactly which page to rewrite for each underperforming query
  • Cannibalization (cannibalization) — queries where multiple pages compete, with per-page click/impression split
  • Device split — mobile vs desktop vs tablet clicks, impressions, CTR, position
  • Country split (country_split) — top 20 countries by clicks with CTR and position
  • Search type breakdown (search_type_split) — web vs image vs video vs news vs Discover vs Google News traffic
  • Branded vs non-branded split (branded_split) — separate aggregates for queries containing brand terms vs pure organic; null if no brand terms provided
  • Page groups (page_groups) — traffic aggregated by site section (/blog/, /products/, /locations/, etc.) with per-section clicks, impressions, CTR, and average position

If GSC is unavailable, skip to Phase 5 (technical-only audit).


⚡ Parallel Data Collection (after Phase 3 completes)

Do not run Phase 3.5, 3.6, 5, and 5.5 sequentially — run them all at once.

As soon as Phase 3's analyze_gsc.py finishes and you have the top pages list, launch all four of these in a single turn using parallel tool calls:

  1. Phase 3.5: run url_inspection.py (Bash tool)
  2. Phase 3.6: detect CMS with cms_detect.py, then run the appropriate preflight + fetch if configured (Bash tool)
  3. Phase 5 pre-fetch: fetch robots.txt, the homepage, and up to 4 top pages via WebFetch — all in parallel
  4. Phase 5.5: run pagespeed.py for the homepage + top pages by clicks (Bash tool) — this calls the PageSpeed Insights API which is independent of GSC auth

This is safe because all four only need the target URL and top pages list, which Phase 3 has already produced. Running them in parallel cuts ~3-5 minutes off the total audit time. Start them all in the same response before reading any results.

After all parallel tasks complete, run Phase 3.7 (Persona Discovery) before starting Phase 4 analysis. Phase 3.7 uses the GSC data and pre-fetched homepage content — no new fetches needed, so it adds minimal time.

Also: once you know the target URL (after Step 0), pre-fetch robots.txt ({target_url}/robots.txt) immediately — don't wait for Phase 3 to finish. It is always needed in Phase 5 and takes only seconds. Fire it off as a WebFetch call alongside the analyze_gsc.py bash call.


Phase 3.5 — URL Inspection

Run the URL Inspection API on the top 10 pages by clicks from Phase 3, plus any pages flagged as losing traffic:

python3 "$SKILL_SCRIPTS/url_inspection.py" \
  --site "sc-domain:example.com" \
  --urls "/path/to/page1,/path/to/page2,..."

The script calls POST https://searchconsole.googleapis.com/v1/urlInspection/index:inspect for each URL and returns per-page:

  • Indexing status: INDEXED, NOT_INDEXED, SUBMITTED_AND_INDEXED, DUPLICATE_WITHOUT_CANONICAL, CRAWLED_CURRENTLY_NOT_INDEXED, etc.
  • Mobile usability verdict: MOBILE_FRIENDLY or issues found
  • Rich result status: which rich result types were detected and their verdict
  • Last crawl time: when Googlebot last visited
  • Referring sitemaps: which sitemap(s) reference this URL
  • Coverage state: full coverage detail from the Index Coverage report

If URL Inspection returns 403: the current auth scope may be read-only. Re- authenticate with the broader scope:

gcloud auth application-default login \
  --scopes=https://www.googleapis.com/auth/webmasters,https://www.googleapis.com/auth/webmasters.readonly

Then retry url_inspection.py.

Analyze the inspection results and flag immediately:

  • Any top-traffic page that is NOT_INDEXED or CRAWLED_CURRENTLY_NOT_INDEXED — this is a critical issue. Identify which page, what the coverage state says, and what likely caused it (noindex tag, canonical pointing elsewhere, robots blocking, soft 404).
  • Pages with DUPLICATE_WITHOUT_CANONICAL — these are leaking authority. The canonical needs to be set.
  • Pages where mobile usability is failing — cross-reference with device split from Phase 3 to confirm whether mobile traffic is below par.
  • Pages with no referring sitemaps — if they are important pages, they should be in a sitemap.
  • Pages with rich result errors where schema exists — this pre-validates Phase 5 structured data findings.
  • Pages whose last crawl time is more than 60 days ago despite having traffic — crawl budget issue or accidental de-prioritization.

Phase 3.6 — CMS Content Inventory (Optional)

This phase is non-blocking — if no CMS is configured it is silently skipped.

Detect configured CMS
CMS_TYPE=$(python3 "$SKILL_SCRIPTS/cms_detect.py" 2>/dev/null)
CMS_DETECT_EXIT=$?
  • Exit code 2 → no CMS configured. Skip this phase entirely, no mention needed.
  • Exit code 0 → CMS detected. Run the matching preflight below.
Run preflight and fetch
CMS_CONTENT_FILE=$(SKILL_SCRIPTS="$SKILL_SCRIPTS" python3 -c "import os, sys, tempfile; sys.path.insert(0, os.environ['SKILL_SCRIPTS']); from _uid import portable_uid; print(os.path.join(tempfile.gettempdir(), f'cms_content_{portable_uid()}.json'))")

case "$CMS_TYPE" in
  strapi)
    python3 "$SKILL_SCRIPTS/preflight_strapi.py"
    CMS_PREFLIGHT=$?
    [ "$CMS_PREFLIGHT" = "0" ] && python3 "$SKILL_SCRIPTS/fetch_strapi_content.py" --output "$CMS_CONTENT_FILE"
    ;;
  wordpress)
    python3 "$SKILL_SCRIPTS/preflight_wordpress.py"
    CMS_PREFLIGHT=$?
    [ "$CMS_PREFLIGHT" = "0" ] && python3 "$SKILL_SCRIPTS/fetch_wordpress_content.py" --output "$CMS_CONTENT_FILE"
    ;;
  contentful)
    python3 "$SKILL_SCRIPTS/preflight_contentful.py"
    CMS_PREFLIGHT=$?
    [ "$CMS_PREFLIGHT" = "0" ] && python3 "$SKILL_SCRIPTS/fetch_contentful_content.py" --output "$CMS_CONTENT_FILE"
    ;;
  ghost)
    python3 "$SKILL_SCRIPTS/preflight_ghost.py"
    CMS_PREFLIGHT=$?
    [ "$CMS_PREFLIGHT" = "0" ] && python3 "$SKILL_SCRIPTS/fetch_ghost_content.py" --output "$CMS_CONTENT_FILE"
    ;;
esac

Preflight exit codes:

  • 0 → ready. Content fetched to $CMS_CONTENT_FILE. Load it and use the data in Phase 4.
  • 2 → not configured. Skip silently.
  • 1 → auth/config error. Show the error and ask the user if they want to fix it (suggest /setup-cms) or continue without CMS data.
What to do with the CMS data

Load $CMS_CONTENT_FILE. All CMSes produce the same normalized format: cms_content.entries is a list of published articles with slugs and SEO fields.

Cross-reference against GSC data:

1. Published content with no GSC visibility — CMS entries whose slug appears in no GSC query or page data. This could mean: not yet indexed, canonicalized to another URL, recently published (GSC data lags ~3 days), property mismatch, or genuinely not ranking. For each: cross-check in Phase 5 technical crawl (indexability, robots.txt, canonical tags). Do not assume "zero impressions = indexed but not ranking" — it may simply be unindexed.

2. Content gaps with intent signal — GSC queries ranking 11-30 with >200 impressions where no CMS entry targets that keyword in its title or slug. These are confirmed demand signals you can close with a new article.

3. Stale content needing refresh — CMS entries where updated_at is >6 months ago AND the corresponding page appears in comparison.declining_pages. Age alone isn't a problem; age + declining clicks is.

4. Missing SEO fields — Use cms_content.seo_audit directly:

  • missing_meta_title — entries with no meta title set
  • missing_meta_description — entries with no meta description set
  • meta_title_too_long — meta titles over 60 characters
  • meta_description_too_short/too_long — outside 70-160 char range

Surface the top 5 most impactful fixes (by impressions where GSC data matches).

Pushing fixes back (Strapi only)

For Strapi, after generating recommendations in Phase 6, offer to write the fixes directly:

"I can push the meta title/description fixes directly to Strapi. Want me to apply them?"

python3 "$SKILL_SCRIPTS/push_strapi_seo.py" \
  --document-id "<documentId>" \
  --meta-title "New title under 60 chars" \
  --meta-description "New description 70-160 chars."
# Or batch: python3 "$SKILL_SCRIPTS/push_strapi_seo.py" --batch-file /tmp/seo_updates.json

The script shows a before/after diff and requires confirmation before writing.

Setup / reconfiguration

If no CMS is configured and the user wants to connect one, suggest:

"Run /setup-cms to connect WordPress, Strapi, Contentful, or Ghost."


Phase 3.7 — Business & Persona Discovery

Understanding who visits the site — and why — shapes every recommendation from Phase 4 onward. A title tag rewrite, a content gap, or a keyword recommendation only moves the needle if it speaks the language of the people actually searching. This phase builds that foundation using real data you already have.

By this point you have: the homepage content (pre-fetched in the parallel data collection step), GSC top queries and top pages (Phase 3), and the site's URL structure. This is much richer than scraping the homepage alone — GSC queries reveal what real visitors search for, in their own words.

Check for cached personas

Personas are cached at ~/.toprank/personas/ keyed by domain hostname. Check whether a persona file already exists ($DOMAIN is already set from Step 0.5):

PERSONA_FILE="$HOME/.toprank/personas/$DOMAIN.json"
[ -f "$PERSONA_FILE" ] && cat "$PERSONA_FILE" || echo "NOT_FOUND"

If found and saved_at is less than 90 days old: Show a one-line summary of each persona and continue. No confirmation pause needed — the user already approved these. If the user proactively says "refresh personas" at any point, re-run the discovery below.

If found but stale (>90 days) or not found: Continue to discovery below.

Discover personas from GSC + site content

Combine these data sources — do not fetch any new pages (you already have them):

  1. GSC top queries (from Phase 3) — the actual words real visitors type. Group by search intent: who searches informational queries vs transactional vs commercial investigation? These are different people with different needs.

  2. GSC top pages (from Phase 3) — which pages get traffic reveals what the site is known for (vs. what it claims on the homepage).

  3. Homepage content (already fetched for Phase 5) — extract: what the business does, who they serve, value proposition, tone/vocabulary, conversion intent.

  4. URL structure (from page groups in GSC) — /blog/ vs /products/ vs /pricing/ reveals different visitor segments.

From these signals, identify the 2-3 most distinct visitor segments. For each:

Field What to capture Why it matters
Name Descriptive label (e.g., "Budget-Conscious Founder") Quick reference throughout the report
Demographics Role, company size, technical level Calibrates language register
Primary goal What they're trying to accomplish Shapes title tags and meta descriptions
Pain points Problems driving them to search Informs content angle and CTAs
Search behavior Query types, informational vs transactional Maps personas to GSC query clusters
Language Specific words, phrases, jargon they use Direct input to title/description rewrites
Decision trigger What makes them convert or return Shapes CTA and landing page copy

Be specific. "Small business owner comparing field-service software for a 3-location operation" is useful. "Users who want to learn more" is not. Ground every persona in actual GSC query patterns — if you can't point to a cluster of queries that this persona would type, the persona is speculative and should be dropped.

Persist personas

Save to ~/.toprank/personas/<domain>.json using a Python one-liner to ensure valid JSON (not a heredoc — heredocs with JSON are fragile):

mkdir -p "$HOME/.toprank/personas"
python3 -c "
import json, sys
data = {
    'domain': '$DOMAIN',
    'saved_at': '$(date -u +%Y-%m-%dT%H:%M:%SZ)',
    'business_summary': '<FILL: 1-2 sentence business description>',
    'personas': [
        {
            'name': '<FILL>',
            'demographics': '<FILL>',
            'primary_goal': '<FILL>',
            'pain_points': '<FILL>',
            'search_behavior': '<FILL>',
            'language': ['<FILL: term1>', '<FILL: term2>', '<FILL: term3>'],
            'decision_trigger': '<FILL>'
        }
    ]
}
json.dump(data, open('$PERSONA_FILE', 'w'), indent=2)
print('Personas saved to $PERSONA_FILE')
"

Replace all <FILL: ...> placeholders with actual discovered values before running. The Python approach avoids shell quoting issues with apostrophes and special characters in persona descriptions.

Present personas (non-blocking)

Show the personas in a compact table — do NOT pause for confirmation. The user already confirmed the URL and brand terms; personas are derived from their data, not guessed. Present them as context for what follows:

"Based on your GSC data and site content, I've identified these visitor personas that will shape the recommendations:"

Persona Searches like... Goal
[name] [2-3 example query patterns from GSC] [goal]

"Let me know if any of these are off — otherwise I'll use them throughout the analysis."

Then immediately continue to Phase 4. Do not wait for a response. If the user corrects a persona later, update the file and adjust any affected recommendations.

Reference $PERSONA_FILE path as ~/.toprank/personas/<domain>.json in later phases — derive <domain> from the target URL each time rather than relying on shell variable persistence.

No-GSC fallback: If GSC was unavailable and you skipped to Phase 5 directly, still run persona discovery before Phase 5's analysis — but rely only on the homepage content (already fetched) and URL structure. The personas will be less precise without query data; note this in the report and recommend re-running the audit with GSC access for better persona accuracy.


Phase 3.8 — Business Context

Read and follow ../shared/business-context.md.

By this point you have GSC data (Phase 3) and homepage content — the two inputs needed to infer business facts before asking the user anything. The goal is to ask as few questions as possible while generating a complete, useful profile.

Branch on CACHE_STATUS from Phase 2:

fresh_loaded: business context is already in memory. No action needed — proceed to Phase 4.

not_found: run the Generation flow from ../shared/business-context.md. Seed brand_terms with $BRAND_TERMS from Phase 2 if the user provided them; supplement with additional brand signals inferred from GSC queries.

stale: run Generation to refresh. CACHE_STATUS=stale means the file was loaded — use those values to pre-fill the three questions so the user confirms or corrects rather than re-enters from scratch.

This phase adds ~30 seconds and one exchange with the user on first run. On all subsequent runs it is silent (cache load only). The payoff: Phase 6 recommendations reference the business by name, compare against real competitors, and focus on the primary goal rather than giving generic SEO advice.


Phase 4 — Search Console Analysis

This is where you earn your keep. Do not just restate the data. Interpret it like an SEO expert would.

Traffic Overview

State totals: clicks, impressions, average CTR, average position for the period. Note any dramatic changes. Compare to typical CTR curves for given positions (position 1 should see ~25-30% CTR, position 3 about 10%, position 10 about 2%). If a query's CTR is significantly below what its position would predict, that is a signal the title/snippet needs work.

Branded vs Non-Branded Split

If branded_split is present (not null), show it as the first table in the analysis:

Segment Queries Clicks Impressions CTR Avg Position
Branded X X X X% X
Non-branded X X X X% X

Interpret the gap:

  • If branded CTR is significantly higher (expected — users know what they're looking for), note that non-branded metrics are the real measure of organic performance.
  • If branded impressions are small vs total, the site has limited brand awareness — focus on non-branded growth.
  • If branded queries are ranking below position 3, that's a reputation/brand issue to flag separately.
  • Use non-branded metrics as the baseline for all Quick Wins and content recommendations — don't let branded traffic inflate the opportunity estimates.
Quick Wins (highest impact, lowest effort)

These are the changes that can move the needle in days, not months:

  1. Position 4-10 queries — ranking on page 1 but below the fold. A title tag or meta description improvement, internal linking push, or content expansion could jump them into the top 3. List the top 10 with current position, impressions, and a specific recommendation for each.

  2. High-impression, low-CTR queries — use ctr_gaps_by_page (not just ctr_opportunities) because it includes the exact page URL alongside the query. This means every recommendation can name the specific page to fix and the specific query driving impressions. For each, analyze the likely search intent (informational, transactional, navigational, commercial investigation) and suggest a title + description that matches it.

  3. Queries dropping month-over-month — flag anything with >30% click decline. For each, hypothesize: is it seasonal? Did a competitor take the SERP feature? Did the page content drift from the query intent?

Search Intent Analysis

For the top 10-15 queries, classify the search intent:

  • Informational ("how to...", "what is...") → needs comprehensive content, FAQ schema
  • Transactional ("buy...", "pricing...", "near me") → needs clear CTA, product schema, price
  • Navigational ("brand name", "brand + product") → should be ranking #1, if not, investigate
  • Commercial investigation ("best...", "vs...", "review") → needs comparison content, trust signals

If the page ranking for a query does not match the intent (e.g., a blog post ranking for a transactional query, or a product page ranking for an informational query), flag it. This is often the single biggest unlock.

Persona lens: Once intent is classified, cross-reference each query against the personas from Phase 3.7. Which persona is most likely searching this query? Are the vocabulary and framing in the current title/snippet the same words that persona would use? A title written for one persona can actively repel another. For example, a query attracting "The Budget-Conscious Founder" persona should use plain-language value framing, while the same topic searched by "The IT Manager" persona may expect technical specificity. Note the persona alignment (or mismatch) for every Quick Win recommendation.

Keyword Cannibalization Check

The output includes a cannibalization array. Each entry has structured winner/loser scoring — use it directly instead of re-deriving from raw data:

  • winner_page — the canonical page to keep (scored by best position, tiebreaker: most clicks)
  • winner_reason — why it won (e.g. "best position (2.1)")
  • loser_pages — pages to consolidate away
  • recommended_action — either "consolidate: 301 redirect losers to winner or add canonical" or "monitor: possible SERP domination" (all pages in top 5, positions within 2 of each other)

For each cannibalized query:

  • State the winner and losers explicitly — don't make the user figure it out
  • Use recommended_action directly in your recommendation
  • Flag queries where position is mediocre (5-15) despite high impressions — splitting is likely suppressing a potential top-3 ranking
  • If recommended_action is "monitor: possible SERP domination", note this as a positive (owning multiple SERP spots) and skip the consolidation recommendation

Also cross-check top_pages and position_buckets for indirect signals: a page that used to rank well dropping after a new page was published, or wild position fluctuation on a query, are signs of cannibalization not yet in the data window.

Page Group Performance

Use page_groups to show which site sections are winning and which need attention:

Section Pages Clicks Impressions CTR Avg Position
/blog/ X X X X% X
/products/ X X X X% X
...

Flag:

  • Low-CTR sections: if an entire section (e.g., all /products/ pages) has CTR well below site average, the issue is likely a template problem (title tag format, meta description format) — one fix improves all pages in that section.
  • High-impression, low-click sections: signals ranking without converting — investigate intent mismatch or snippet quality across the section.
  • Sections missing entirely: if /locations/ or /services/ doesn't appear, either those pages don't rank or they haven't been created.
  • "other" group is large: means the site has custom URL patterns not covered by defaults — note this for the user so they can understand what's in "other."

This is more actionable than per-page analysis: a recommendation like "the /products/ title tag template needs work" can fix 50 pages at once.

Segment Analysis

Device (device_split): Compare CTR and position across mobile/desktop/ tablet. A page can look healthy overall but be failing on mobile. Flag any device where CTR is >30% below the site average — that is a mobile UX or snippet problem.

Country (country_split): Look at the top countries. Flag cases where:

  • A country has high impressions but very low CTR (title/snippet not landing in that market)
  • Position is much worse in one country vs others (local competitor or relevance gap)
  • A country with meaningful impressions has near-zero clicks (potential hreflang or geo-targeting issue)

Search type (search_type_split): If discover or googleNews appear, note them — they behave differently from web search and have separate optimization levers (freshness, images, authority signals). If image or video traffic exists and the site does not have dedicated image/video optimization, call that out as an opportunity.

Content Gaps

Queries where you rank 11-30 — you have topical authority but need a dedicated page or content expansion. Group related queries into topic clusters. For each cluster, recommend whether to:

  • Expand an existing page (if it partially covers the topic)
  • Create a new page (if no page targets this topic)
  • Create a content hub with internal linking (if there are 5+ related queries)
Pages to Fix

List pages with declining clicks. For each:

  • Current clicks vs previous period
  • % change
  • Likely cause (seasonal, algorithm update, new competitor, content staleness, technical issue)
  • Specific fix recommendation

Phase 4.5 — Keyword Gap Analysis

This phase identifies keyword opportunities directly from the GSC data — no external tools required, though running /keyword-research afterward can go deeper.

Step 1: Find Queries Without Dedicated Pages

From the GSC top_queries data, identify queries where:

  • The site ranks 4-20 for the query
  • The page that ranks is NOT a page primarily about that topic (e.g., a homepage or a page written for a different keyword is accidentally ranking)
  • There is no page on the site with that keyword prominently in the title, H1, or URL slug

These are keyword orphans — the site has demonstrated topical relevance but has never given the topic its own page. Creating a dedicated page for each is typically the highest-leverage content move.

For each orphan, state:

  • The query
  • Current ranking page (URL) and position
  • Monthly impressions
  • Recommended action: "Create a new page targeting '[query]' — currently ranked #[N] from [URL] which is not dedicated to this topic. A dedicated page could realistically move from #[N] to top 5."
Step 2: Build Topic Clusters from GSC Data

Group all ranking queries by theme. A cluster exists when 3+ queries share a core concept. For each cluster:

  • Name the cluster (e.g., "pricing-related queries", "feature X how-to queries")
  • List the queries in it, their positions, and their impressions
  • Identify whether a pillar page exists that ties them together
  • If no pillar page exists, recommend creating one and note the internal linking structure needed to funnel authority from cluster pages to the pillar
Step 3: Business Context Gap Check

Based on what the site does (inferred from its URL, top pages, and ranking queries), identify topics the business clearly serves that have zero or near-zero GSC impressions. These are business-relevant keyword gaps — the site should be visible for them but is not.

State the gap explicitly: "This appears to be a [type of business]. You rank for [X] but have no impressions for [related topic], which has significant search demand. This is a content gap to close."

Step 4: Offer Deeper Keyword Research

After completing the inline analysis, offer:

"I've identified [N] keyword gaps from your GSC data. For broader keyword discovery — including keywords you're NOT yet ranking for at all — run /keyword-research with your seed topics. That skill pulls from keyword databases and builds a full opportunity set beyond what GSC can see."


Phase 5 — Technical SEO Audit

Crawl the site's key pages to check technical health. Use the firecrawl skill if available, otherwise use WebFetch.

Pages to audit: at most 5 pages total. Prioritize: homepage first, then fill remaining slots with top pages by clicks from Phase 4 — unless a page is flagged as declining or NOT_INDEXED in Phase 3.5, in which case swap it in. Hard cap at 5 regardless of how many flagged pages exist; pick the highest-priority ones.

⚡ Speed note: Fetch all 5 pages using parallel WebFetch calls in a single turn — do not fetch them one-at-a-time. You should have already pre-fetched robots.txt and the homepage during Phase 3 (see Parallel Data Collection above); if so, only fetch the remaining pages you haven't retrieved yet.

Indexability
  • Fetch and analyze robots.txt — is it blocking important paths? Are there unnecessary disallow rules?
  • Check for noindex meta tags or X-Robots-Tag headers on important pages
  • Check canonical URLs — self-referencing (good) or pointing elsewhere (investigate)
  • Check for hreflang tags if the site targets multiple languages/regions
  • Look for orphan pages (important pages with no internal links pointing to them)
  • Cross-reference with URL Inspection findings from Phase 3.5 — any NOT_INDEXED page found there should be explained here with the root cause
Metadata Audit (Deep)

For each audited page, fetch the actual <title> and <meta name="description"> from the live HTML. Then cross-reference against GSC data:

  1. Title vs top query alignment: For each page, look up the top 3 queries that page ranks for in ctr_gaps_by_page. Does the title tag contain the primary ranking query or a close variant? If the title is generic (e.g., "Home", "Services", "Blog") while the page ranks for specific queries, that is a mismatch — the title is failing to confirm relevance and hurting CTR.

  2. Title length: Under 60 characters? Over 60 characters gets truncated in SERPs. Flag every page over the limit with the current character count and the truncated version as it would appear in Google.

  3. Meta description: Present? 120-160 characters? Contains a call to action? If a page has no meta description, Google rewrites it — often pulling unhelpful boilerplate. Flag every missing description.

  4. Duplicate titles: Are multiple pages using the same or very similar titles? List all duplicates found.

  5. Open Graph tags: og:title, og:description, og:image present? Missing OG tags means social shares render with no preview — flag any page missing them, especially for content pages.

Report the findings as a table:

Page URL Title (actual) Title length Top GSC query Title/query match? Meta desc present? OG tags?
/ [actual title] [N] chars [query] Yes / No Yes / No Yes / No

After presenting the metadata audit table, offer:

"I found [N] pages with metadata issues. Run /meta-tags-optimizer to generate optimized title tags and meta descriptions for each — it will use the GSC query data from this audit to write titles that match actual search demand."

Schema Markup Audit (Deep)

Detect the site type from its top pages, ranking queries, and visible content, then check what schema types exist vs. what should exist for that site type.

Step 1: Detect site type

Based on the homepage and top pages content, classify as one of:

  • E-commerce (products, pricing, cart)
  • Local business (address, phone, service area)
  • SaaS / software (features, pricing, signup)
  • Content / blog (articles, guides, tutorials)
  • Professional services (agency, consultant, law firm)
  • Media / news (articles published frequently)

Step 2: Define expected schema for site type

Site Type Must Have High Impact if Missing Nice to Have
E-commerce Product, BreadcrumbList AggregateRating, FAQPage, Offer SiteLinksSearchBox
Local business LocalBusiness, GeoCoordinates OpeningHoursSpecification, AggregateRating FAQPage
SaaS Organization, SoftwareApplication FAQPage, BreadcrumbList HowTo, Review
Content / blog Article or BlogPosting FAQPage, BreadcrumbList HowTo, Video
Professional services Organization, Service FAQPage, Review ProfessionalService, Person
Media / news NewsArticle BreadcrumbList VideoObject, ImageObject

Step 3: Audit each top page for actual schema present

For each audited page, extract any <script type="application/ld+json"> blocks. List what @type values are present. Then compare against the expected set for this site type.

Report findings:

Page URL Schema found Missing high-impact schema Errors in existing schema
/ Organization FAQPage, SiteLinksSearchBox None
/pricing SoftwareApplication FAQPage, Offer Missing price property

Step 4: Flag errors in existing schema

Common issues to check:

  • Missing required fields for the @type (e.g., Product schema without name or offers)
  • url properties using relative paths instead of absolute URLs
  • Dates not in ISO 8601 format
  • AggregateRating with ratingCount of 0 or missing
  • Duplicate schema blocks for the same type on one page
  • Schema that describes content not visible on the page (violates Google policy)

Cross-reference with rich result status from Phase 3.5 URL Inspection — if a page showed rich result errors there, find the cause here.

After presenting the schema audit, offer:

"I found [N] pages missing high-impact schema and [N] pages with errors in existing schema. Run /schema-markup-generator to generate correct JSON-LD for each — it will use the site type and page content from this audit."

Core Web Vitals & Performance
  • Render-blocking scripts in <head> — should be deferred or async
  • Images: lazy-loaded? Have alt attributes? Served in modern formats (WebP/AVIF)? Properly sized (not 3000px wide in a 400px container)?
  • <link rel="preload"> for critical resources (fonts, above-the-fold images)?
  • Excessive DOM size (>1500 nodes suggests bloat)?
  • Third-party script bloat — count external domains loaded
Internal Linking & Site Architecture
  • Does the page have internal links? Are they descriptive (not "click here")?
  • Does the page link to related content (topic clusters)?
  • Is the page reachable within 3 clicks from the homepage?
  • Broken internal links (404s)?
Mobile Readiness
  • Viewport meta tag present?
  • Touch targets large enough (48px minimum)?
  • Text readable without zooming?
  • No horizontal scrolling?
  • Cross-reference mobile usability findings from Phase 3.5 URL Inspection

Phase 5.5 — PageSpeed Insights (Performance Monitoring)

Run the PageSpeed Insights API on the homepage + top 4 pages by clicks from Phase 3. This provides both lab data (Lighthouse synthetic test) and field data (Chrome UX Report real-user metrics) for Core Web Vitals.

⚡ Speed note: This should already be running in parallel from the Parallel Data Collection step. If not, run it now.

python3 "$SKILL_SCRIPTS/pagespeed.py" \
  --urls "$TARGET_URL,https://example.com/page2,https://example.com/page3" \
  --both-strategies

Replace the example URLs with the actual homepage and top pages from Phase 3. Use --both-strategies to get both mobile and desktop scores. If the user has set PAGESPEED_API_KEY in their environment, the script uses it automatically for higher rate limits.

After pagespeed.py completes, run the display utility:

python3 "$SKILL_SCRIPTS/show_pagespeed.py"
Analyze the Results

1. Performance Scores — Lighthouse scores 0-100 per page:

  • 90-100 (Good): No action needed.
  • 50-89 (Needs Work): Flag the top opportunities. These pages are losing rankings due to performance — Google uses Core Web Vitals as a ranking signal.
  • 0-49 (Poor): Critical. These pages are actively penalized in rankings. Flag as a Priority Action if the page has significant organic traffic.

2. Core Web Vitals (Field Data) — Real-user metrics from Chrome UX Report:

  • LCP (Largest Contentful Paint): Good < 2.5s, Poor > 4.0s
  • INP (Interaction to Next Paint): Good < 200ms, Poor > 500ms
  • CLS (Cumulative Layout Shift): Good < 0.1, Poor > 0.25

Field data is more authoritative than lab data for SEO — Google uses CrUX data for rankings. If field data is available, lead with it. If not (low-traffic sites often lack CrUX data), use lab data and note it's synthetic.

3. Cross-Reference with Other Phases:

  • Phase 3 device split: If mobile performance score is significantly lower than desktop, and Phase 3 shows mobile traffic underperforming, the performance gap is likely a contributing factor.
  • Phase 5 technical audit: Correlate specific opportunities (e.g., "Eliminate render-blocking resources") with the technical findings (e.g., render-blocking scripts in <head>). This gives concrete evidence for technical fixes.
  • Phase 3.5 URL Inspection: Pages flagged as mobile-unfriendly that also have poor mobile PageSpeed scores need urgent attention.

4. Top Opportunities — The script extracts Lighthouse optimization opportunities sorted by potential time savings. For each, note:

  • What the opportunity is (e.g., "Properly size images", "Remove unused JavaScript")
  • Estimated savings in milliseconds
  • Which specific page(s) are affected
  • Whether it's a site-wide template issue or page-specific

5. Origin-Level Data — If available, the origin (site-wide) CrUX data shows the overall performance health of the entire domain. Compare individual page scores against the origin average to identify outlier pages dragging down the site's overall performance profile.


Phase 6 — Report

The goal of this report is not comprehensiveness — it is clarity. The user needs to know exactly what to do next, in what order, and why. Lead with the highest-impact actions. Put supporting data after. Omit anything that doesn't change what the user should do.

Output a structured report using this format exactly:


SEO Report — [site.com]

[date] · GSC data: [date range] · [First audit / Previous audit: date]

Audit History

(Skip this section entirely on the first audit — do not write "N/A" or "First audit" here; just omit the section.)

On subsequent audits, show only what changed from the previous audit's top issues:

Previously Flagged Status Notes
[Issue from last audit] ✅ Resolved / ⚠️ Improved / 🔴 Still present / ↗ Worsened [1-line update with current metric]

⚡ Top Priority Actions

This is the core of the report. Include exactly 3–5 items, ordered by expected click impact. Every item must have a specific URL, a specific metric as evidence, and a specific fix — nothing generic.

Use this format for each:


#1 — [Short title, e.g. "Fix title tag on /pricing"] 🔴 Critical / 🟡 High / 🟢 Medium Impact: ~+[N] clicks/mo · Effort: Low / Med / High

What: [One sentence describing the problem] Evidence: [Exact metric — e.g., "ranks #7 for 'your-product pricing': 2,400 impressions/mo, 1.2% CTR (expected ~3% at this position)"] Fix: [Specific, copy-paste-ready action — e.g., "Change title from 'Pricing' to 'Plans & Pricing — [Value Prop] | [Brand]' (54 chars)"] Why it works: [One sentence on the mechanism — intent match, persona language, etc.]


Repeat for each of the 3–5 items. Do not add a 6th item — triage ruthlessly. An item only makes the list if you can quantify its impact.

When estimating impact, use conservative CTR curves: position 1 ~27%, position 2 ~15%, position 3 ~11%, position 4–5 ~5–8%, position 6–10 ~2–4%. Moving from position 7 to 3 on a 2,400 impression/month query means roughly +170 clicks/month. Always use real numbers from the data.

Every persona-informed recommendation must name the persona and cite the specific language from that persona's language field that should appear in the rewrite.


Traffic Snapshot

Metric Value vs Prior 28 days
Total Clicks X ↑/↓ X%
Impressions X ↑/↓ X%
Avg CTR X% ↑/↓
Avg Position X ↑/↓

(Branded/non-branded split — only if brand terms were provided):

Segment Clicks Impressions CTR Avg Position
Branded X X X% X
Non-branded X X X% X

[1-sentence interpretation of the split — what it reveals about organic vs brand performance]


Supporting Findings

This section exists to back up the Priority Actions and surface anything else the user should know. Keep it concise — tables and short bullets, not prose paragraphs. Only include sub-sections where there are actual findings.

Indexing Issues

(From Phase 3.5. Only include if issues found.)

Page Coverage State Last Crawl Fix
Keyword Cannibalization

(Only include if cannibalization data is non-empty.)

Query Winner Page Loser Pages Action
Content Gaps

(Queries ranking 11–30 with >200 impressions and no dedicated page.)

Query Position Impressions/mo Recommended Action
Metadata Issues

(Only pages not already covered in Priority Actions.)

Page Issue Current Recommended Fix
Schema Gaps

(High-impact missing schema for this site type.)

Page Missing Impact
Technical Issues

(Severity: Critical / High / Medium. Omit Low unless they surface as Priority Actions.)

Issue Pages Affected Fix Severity
PageSpeed & Core Web Vitals

(From Phase 5.5. Only include if issues found. Lead with field data if available, fall back to lab data.)

Site-wide (Origin): [Overall CrUX rating if available]

Page Score LCP INP CLS Top Opportunity
/ [score] [value] [rating] [value] [rating] [value] [rating] [top opportunity title + savings]

(If any page scores below 50, flag it as a Priority Action candidate — poor Core Web Vitals directly hurt rankings.)

Traffic Drops

(Pages/queries with >30% decline. Only include if not already in Priority Actions.)

Page / Query Change Hypothesis Next Step
CMS SEO Audit

(Only if a CMS is configured. Top 5 impactful fixes only.)

Page Issue Current Fix

What to Ignore (For Now)

List 2–3 things the data shows but that don't make the priority list — so the user knows you saw them and deprioritized them deliberately. One line each.

  • [e.g., "Device split: mobile CTR 15% below desktop — worth watching but not the bottleneck right now"]
  • [e.g., "Country split: weak CTR in UK — low volume, investigate after core issues fixed"]

After the report, write the audit log entry (see Phase 6.5 below before ending).


Phase 6.5 — Write Audit Log

After delivering the report, append a concise entry to the audit log. $DOMAIN and $AUDIT_LOG are already set from Step 0.5.

mkdir -p "$HOME/.toprank/audit-log"

Use Python to append (creates the file with a single-element array if it doesn't exist). Replace all <FILL> values with real data from the report before running:

import json, os
from datetime import datetime, timezone

log_path = "$AUDIT_LOG"
existing = json.load(open(log_path)) if os.path.exists(log_path) else []

existing.append({
    "date": datetime.now(timezone.utc).strftime("%Y-%m-%d"),
    "traffic_snapshot": {
        "clicks": <FILL>,
        "impressions": <FILL>,
        "avg_ctr_pct": <FILL>,
        "avg_position": <FILL>
    },
    "pagespeed_snapshot": {
        "avg_score_mobile": <FILL or null>,
        "avg_score_desktop": <FILL or null>,
        "homepage_score_mobile": <FILL or null>,
        "cwv_lcp_ms": <FILL or null>,
        "cwv_inp_ms": <FILL or null>,
        "cwv_cls": <FILL or null>,
        "cwv_source": "<FILL: field|lab>"  # "field" if CrUX data available, else "lab"
    },
    "top_issues": [
        # One entry per Priority Action (max 5), in priority order
        {"rank": 1, "title": "<FILL>", "type": "<FILL: title_tag|indexing|cannibalization|schema|content_gap|performance>", "page": "<FILL>", "metric": "<FILL>", "expected_impact": "<FILL>", "status": "open"}
    ],
    "resolved_from_previous": []  # populated on next audit from Audit History comparison
})

json.dump(existing, open(log_path, "w"), indent=2)
print(f"Audit log saved to {log_path}")

Confirm with a one-liner: "Audit log saved to ~/.toprank/audit-log/$DOMAIN.json."


Phase 7 — Targeted Skill Handoffs (Optional)

After delivering the report, surface the follow-up actions based on what was found. Only offer handoffs where the audit actually found issues — do not offer all three if only one is relevant.

Metadata Handoff

If the metadata audit found [N] pages with issues:

"I found [N] pages with metadata issues — [X] with title/query mismatches, [Y] missing meta descriptions, [Z] missing OG tags. Run /meta-tags-optimizer to generate optimized tags for each page. Share the metadata audit table from this report as context."

Schema Handoff

If the schema audit found gaps or errors:

"I found [N] pages missing high-impact schema and [N] pages with schema errors. Run /schema-markup-generator to generate correct JSON-LD. The schema audit table from this report is the input — it already identifies the site type and what schema types are needed per page."

Keyword Research Handoff

If the keyword gap analysis found orphan keywords or business relevance gaps:

"I found [N] keyword gaps from GSC data. For deeper discovery — keywords you are not ranking for at all — run /keyword-research with these seed topics: [list 3-5 seed terms derived from the gap analysis]. That skill pulls from keyword databases and builds a full opportunity set beyond what GSC can see."


Phase 8 — Content Generation (Optional)

After delivering the report, if the Content Opportunities section identified actionable content gaps, offer to generate the content:

"I found [N] content opportunities. Want me to draft the content? I can write [blog posts / landing pages / both] in parallel — each one optimized for the target keyword and search intent."

If the user agrees, spawn content agents in parallel using the Agent tool. Each agent writes one piece of content independently.

How to Spawn Content Agents

For each content opportunity, determine the content type from the search intent:

  • Informational / commercial investigation → blog post agent
  • Transactional / commercial → landing page agent

Spawn agents in parallel. Each agent receives:

  1. The content writing guidelines (located via find — see below)
  2. The specific opportunity data from the analysis

Before spawning agents, locate the content writing reference:

CONTENT_REF=$(find ~/.claude/plugins ~/.claude/skills ~/.codex/skills .agents/skills -name "content-writing.md" -path "*content-writer*" 2>/dev/null | head -1)
if [ -z "$CONTENT_REF" ]; then
  echo "WARNING: content-writing.md not found. Content agents will use built-in knowledge only."
else
  echo "Content reference at: $CONTENT_REF"
fi

Pass $CONTENT_REF as the path in each agent prompt below. If not found, omit the "Read the content writing guidelines" line — the agents will still produce good content using built-in knowledge.

Use this prompt template for each agent:

Blog Post Agent Prompt
You are a senior content strategist writing a blog post that ranks on Google.

Read the content writing guidelines at: $CONTENT_REF
Follow the "Blog Posts" section exactly.

## Assignment

Target keyword: [keyword]
Current position: [position] (query ranked but no dedicated content)
Monthly impressions: [impressions]
Search intent: [informational / commercial investigation]
Site context: [what the site is about, its audience]
Existing pages to link to: [relevant internal pages from the analysis]
[If available] Competitor context: [what currently ranks for this keyword]

## Target Personas
Write primarily for: [Primary persona name]
Their goal: [primary goal]
Their language: [key terms and phrases they use — use these naturally in headings, intro, and body]
Their pain points: [pain points — address these directly, don't make them search for answers]
Secondary audience: [Secondary persona name if applicable] — [brief note on how to serve both without diluting focus]

## Deliverables

Write the complete blog post following the guidelines, including:
1. Full post in markdown with proper heading hierarchy
2. SEO metadata (title tag, meta description, URL slug)
3. JSON-LD structured data (Article/BlogPosting + FAQPage if FAQ included)
4. Internal linking plan (which existing pages to link to/from)
5. Publishing checklist

## Quality Gate
Before finishing, verify:
- Would the reader need to search again? (If yes, not done)
- Does the post contain specific examples only an expert would include?
- Does the format match what Google shows for this query?
- Is every paragraph earning its place? (No filler)
Landing Page Agent Prompt
You are a senior conversion copywriter writing a landing page that ranks AND converts.

Read the content writing guidelines at: $CONTENT_REF
Follow the "Landing Pages" section exactly.

## Assignment

Target keyword: [keyword]
Current position: [position]
Monthly impressions: [impressions]
Search intent: [transactional / commercial]
Page type: [service / product / location / comparison]
Site context: [what the site is about, value prop, target customer]
Existing pages to link to: [relevant internal pages]
[If available] Competitor context: [what currently ranks]

## Target Personas
Write primarily for: [Primary persona name]
Their goal: [primary goal when landing here]
Their language: [terms they use — mirror this in headlines, subheads, and CTAs]
Their decision trigger: [what makes them convert — address this prominently above the fold]
Their objections: [pain points and doubts — address each explicitly, don't leave them wondering]

## Deliverables

Write the complete landing page following the guidelines, including:
1. Full page copy in markdown with proper heading hierarchy and CTA placements
2. SEO metadata (title tag, meta description, URL slug)
3. Conversion strategy (primary CTA, objections addressed, trust signals)
4. JSON-LD structured data
5. Internal linking plan
6. Publishing checklist

## Quality Gate
Before finishing, verify:
- Would you convert after reading this? (If not, what is missing?)
- Are there vague claims that should be replaced with specifics?
- Is every objection addressed?
- Is it clear what the visitor should do next?
Spawning Rules
  • Spawn up to 5 content agents in parallel (more than 5 gets unwieldy — prioritize by impact)
  • Prioritize opportunities by: impressions x position-improvement-potential
  • Each agent works independently — they do not need to coordinate
  • As agents complete, present each piece of content to the user with its metadata
  • After all agents finish, provide a summary: what was generated, suggested publishing order (highest impact first), and any cross-linking between new pages
1---
2name: seo-analysis
3argument-hint: "<URL to audit, e.g. https://example.com>"
4description: >
5 Full SEO audit combining Google Search Console, URL Inspection API, PageSpeed
6 Insights, a technical crawl, keyword research, and metadata, schema, search intent
7 and Core Web Vitals checks. Surfaces quick wins, diagnoses traffic drops and
8 content gaps, and produces an actionable 30-day plan. Use whenever the user asks
9 about SEO, search rankings, organic traffic, Google Search Console, keyword
10 performance, search visibility, technical SEO, URL indexing, or LCP, INP, CLS or
11 Lighthouse scores. Also trigger on: "why is my traffic down", "what keywords am I
12 ranking for", "improve my rankings", "check my search console", "SEO audit",
13 "analyze my SEO", "technical SEO", "meta tags", "indexing issues", "crawl errors",
14 "content strategy", "keyword cannibalization", "search intent", "schema markup",
15 "structured data", "URL inspection", "page speed", "performance score", "core web
16 vitals", "lighthouse", or any organic search question. If in doubt, trigger.
17---
18 
19# SEO Analysis
20 
21You are a senior technical SEO consultant. You combine real Google Search Console
22data with deep knowledge of how search engines rank pages to find problems,
23surface opportunities, and produce specific, actionable recommendations.
24 
25Your goal is not to produce a generic report. It is to find the 3-5 changes that
26will have the biggest impact on this specific site's organic traffic, and explain
27exactly how to make them.
28 
29Works on any site. Works whether you are inside a website repo or auditing a URL
30cold.
31 
32---
33 
34## Step 0 — Establish the Website URL
35 
36Before doing anything else, check for previously audited sites:
37 
38```bash
39ls ~/.toprank/business-context/*.json 2>/dev/null | xargs -I{} python3 -c "
40import json, sys
41from datetime import datetime, timezone
42try:
43 d = json.load(open(sys.argv[1]))
44 gen = datetime.fromisoformat(d.get('generated_at', '1970-01-01T00:00:00+00:00'))
45 age = (datetime.now(timezone.utc) - gen.astimezone(timezone.utc)).days
46 print(f\"{d.get('target_url', d.get('domain','?'))} (audited {age}d ago)\")
47except: pass
48" {}
49```
50 
51**If one or more cached sites are listed**, show them and ask:
52 
53> "I've audited these sites before — use one, or enter a different URL:
54> 1. https://example.com (audited 12 days ago)
55> 2. Enter a different URL"
56 
57If the user picks a cached site, load `target_url` from that domain's `~/.toprank/business-context/<domain>.json` and set it as `$TARGET_URL`. Skip to Phase 0.
58 
59**If no cached sites exist**, ask the user:
60 
61> "What is the main URL of the website you want to audit? (e.g. https://yoursite.com)"
62 
63Wait for their answer. Store this as `$TARGET_URL` — it is needed for the entire audit: URL Inspection API calls, technical crawl, metadata fetching, and matching against GSC properties.
64 
65Once you have the URL, also attempt to auto-detect it from the repo to confirm
66or catch mismatches:
67 
68- `package.json` → `"homepage"` field or scripts with domain hints
69- `next.config.js` / `next.config.ts` → `env.NEXT_PUBLIC_SITE_URL` or `basePath`
70- `astro.config.*` → `site:` field
71- `gatsby-config.js` → `siteMetadata.siteUrl`
72- `hugo.toml` / `hugo.yaml` → `baseURL`
73- `_config.yml` (Jekyll) → `url` field
74- `.env` or `.env.local` → `NEXT_PUBLIC_SITE_URL`, `SITE_URL`, `PUBLIC_URL`
75- `vercel.json` → deployment aliases
76- `CNAME` file (GitHub Pages)
77 
78If auto-detection finds a URL that differs from what the user provided, surface
79the discrepancy: "I found `https://detected.com` in your config — is that the
80same site, or are you auditing a different domain?" Resolve before continuing.
81 
82If not inside a website repo, skip auto-detection entirely and use only the
83user-provided URL.
84 
85---
86 
87## Step 0.5 — Load Audit History
88 
89After identifying `$TARGET_URL`, derive the domain (used throughout the entire audit) and check for a previous audit log:
90 
91```bash
92DOMAIN=$(python3 -c "import sys; from urllib.parse import urlparse; print(urlparse(sys.argv[1]).netloc.lstrip('www.'))" "$TARGET_URL")
93AUDIT_LOG="$HOME/.toprank/audit-log/${DOMAIN}.json"
94[ -f "$AUDIT_LOG" ] && cat "$AUDIT_LOG" || echo "NOT_FOUND"
95```
96 
97`$DOMAIN` is now set — reuse it everywhere (Phase 3.7, Phase 6.5). Do not re-derive it.
98 
99**If found**: Extract the most recent entry's `date` and `top_issues`. Show the user a brief one-liner:
100 
101> "Last audit: [date]. Previously flagged: [issue #1 title], [issue #2 title]. I'll check whether these are resolved."
102 
103Carry the previous issues into Phase 4 and Phase 6 — compare current data against them to determine status (resolved / improved / still present / worsened).
104 
105**If not found**: This is the first audit. No action needed.
106 
107Do NOT pause for user confirmation — just show the one-liner and continue.
108 
109---
110 
111## Phase 0 — Preflight Check
112 
113Read and follow `../shared/preamble.md` — it handles script discovery, gcloud auth, and GSC API setup. If credentials are already cached, this is instant.
114 
115The preflight also checks for the PageSpeed Insights API (enables it automatically)
116and looks for a `PAGESPEED_API_KEY`. The PageSpeed API works without auth for
117low-volume use, but an API key avoids quota limits. If the preflight reports no
118API key, suggest:
119 
120> "For reliable PageSpeed analysis, create an API key at
121> https://console.cloud.google.com/apis/credentials and set
122> `export PAGESPEED_API_KEY='your-key'` or add it to `~/.toprank/.env`."
123 
124If the user has no gcloud and wants to skip GSC, jump directly to Phase 5 for a technical-only audit (crawl, meta tags, schema, indexing, PageSpeed).
125 
126> **Reference**: For manual step-by-step setup or troubleshooting, see
127> [references/gsc_setup.md](references/gsc_setup.md).
128 
129---
130 
131## Phase 1 — Confirm Access to Google Search Console
132 
133Using `$SKILL_SCRIPTS` from the shared preamble (Step 2):
134 
135```bash
136python3 "$SKILL_SCRIPTS/list_gsc_sites.py"
137```
138 
139**If it lists sites** → done. Carry the site list into Phase 2.
140 
141**If "No Search Console properties found"** → wrong Google account. Ask the user
142which account owns their GSC properties at
143https://search.google.com/search-console, then re-authenticate:
144 
145```bash
146gcloud auth application-default login \
147 --scopes=https://www.googleapis.com/auth/webmasters,https://www.googleapis.com/auth/webmasters.readonly
148```
149 
150**If 403 (quota/project error)** → the scripts auto-detect quota project from
151gcloud config. If it still fails, set it explicitly:
152 
153```bash
154gcloud auth application-default set-quota-project "$(gcloud config get-value project)"
155```
156 
157**If 403 (API not enabled)** → run:
158 
159```bash
160gcloud services enable searchconsole.googleapis.com
161```
162 
163**If 403 (permission denied)** → the account lacks GSC property access. Verify
164at Search Console → Settings → Users and permissions.
165 
166---
167 
168## Phase 2 — Match the Site to a GSC Property
169 
170Use the target URL from Step 0 and the GSC property list from Phase 1 to find
171the matching property.
172 
173### Collect brand terms
174 
175First, run the Loading section from `../shared/business-context.md`. This sets `CACHE_STATUS` (one of `fresh_loaded`, `stale`, or `not_found`).
176 
177**If `CACHE_STATUS=fresh_loaded`**: extract `brand_terms` from the JSON and join them comma-separated → `BRAND_TERMS`. Skip asking the user. Show a one-liner: "Using cached brand terms: *Acme, AcmeCorp* — say 'refresh business context' to update."
178 
179**If `CACHE_STATUS=stale` or `not_found`**: ask the user:
180> "What's your brand name? Enter one or more comma-separated terms (e.g. `Acme, AcmeCorp, acme.io`) — used to separate branded from non-branded traffic. Press Enter to skip."
181 
182Store the response as `BRAND_TERMS`. If skipped, leave empty — the script handles it gracefully.
183 
184GSC properties can be domain properties (`sc-domain:example.com`) or URL-prefix
185properties (`https://example.com/`). If both exist for the same site, prefer the
186domain property — it covers all subdomains, protocols, and subpaths, giving more
187complete data. If multiple matches exist and it is still ambiguous, ask the user
188to confirm.
189 
190Confirm the match with the user before proceeding: "I'll pull GSC data for
191`sc-domain:example.com` — is that correct?"
192 
193---
194 
195## Phase 3 — Collect GSC Data
196 
197**⚡ Speed**: In the same turn you run `analyze_gsc.py`, also fire a parallel
198WebFetch for `{target_url}/robots.txt` — it's always needed in Phase 5 and you
199already know the URL. Both calls can run simultaneously.
200 
201Run the main analysis script with the confirmed site property:
202 
203```bash
204python3 "$SKILL_SCRIPTS/analyze_gsc.py" \
205 --site "sc-domain:example.com" \
206 --days 90 \
207 --brand-terms "$BRAND_TERMS"
208```
209 
210(Omit `--brand-terms` if `$BRAND_TERMS` is empty.)
211 
212After `analyze_gsc.py` completes, run the display utility to print a structured summary — **do not write inline Python to parse the JSON yourself**:
213 
214```bash
215python3 "$SKILL_SCRIPTS/show_gsc.py"
216```
217 
218This outputs all sections correctly (CTR is stored as a percentage value already, `branded_split` can be null, `comparison` has string metadata fields — the display script handles all of these safely).
219 
220This pulls:
221- **Top queries** by impressions, clicks, CTR, average position
222- **Top pages** by clicks + impressions
223- **Position buckets** — queries in 1-3, 4-10, 11-20, 21+ (the "striking
224 distance" opportunities)
225- **Queries losing clicks** — comparing last 28 days vs the prior 28 days
226- **Pages losing traffic** — same comparison
227- **CTR opportunities** (`ctr_opportunities`) — query-level: high impressions, low CTR, title/snippet targets
228- **CTR gaps by page** (`ctr_gaps_by_page`) — query+page level: shows exactly which page to rewrite for each underperforming query
229- **Cannibalization** (`cannibalization`) — queries where multiple pages compete, with per-page click/impression split
230- **Device split** — mobile vs desktop vs tablet clicks, impressions, CTR, position
231- **Country split** (`country_split`) — top 20 countries by clicks with CTR and position
232- **Search type breakdown** (`search_type_split`) — web vs image vs video vs news vs Discover vs Google News traffic
233- **Branded vs non-branded split** (`branded_split`) — separate aggregates for queries containing brand terms vs pure organic; `null` if no brand terms provided
234- **Page groups** (`page_groups`) — traffic aggregated by site section (/blog/, /products/, /locations/, etc.) with per-section clicks, impressions, CTR, and average position
235 
236**If GSC is unavailable**, skip to Phase 5 (technical-only audit).
237 
238---
239 
240## ⚡ Parallel Data Collection (after Phase 3 completes)
241 
242**Do not run Phase 3.5, 3.6, 5, and 5.5 sequentially — run them all at once.**
243 
244As soon as Phase 3's `analyze_gsc.py` finishes and you have the top pages list,
245launch all four of these in a single turn using parallel tool calls:
246 
2471. **Phase 3.5**: run `url_inspection.py` (Bash tool)
2482. **Phase 3.6**: detect CMS with `cms_detect.py`, then run the appropriate preflight + fetch if configured (Bash tool)
2493. **Phase 5 pre-fetch**: fetch `robots.txt`, the homepage, and up to 4 top pages via WebFetch — all in parallel
2504. **Phase 5.5**: run `pagespeed.py` for the homepage + top pages by clicks (Bash tool) — this calls the PageSpeed Insights API which is independent of GSC auth
251 
252This is safe because all four only need the target URL and top pages list, which
253Phase 3 has already produced. Running them in parallel cuts ~3-5 minutes off the
254total audit time. Start them all in the same response before reading any results.
255 
256**After all parallel tasks complete**, run **Phase 3.7** (Persona Discovery)
257before starting Phase 4 analysis. Phase 3.7 uses the GSC data and pre-fetched
258homepage content — no new fetches needed, so it adds minimal time.
259 
260Also: once you know the target URL (after Step 0), **pre-fetch `robots.txt`
261(`{target_url}/robots.txt`) immediately** — don't wait for Phase 3 to finish. It
262is always needed in Phase 5 and takes only seconds. Fire it off as a WebFetch call
263alongside the `analyze_gsc.py` bash call.
264 
265---
266 
267## Phase 3.5 — URL Inspection
268 
269Run the URL Inspection API on the top 10 pages by clicks from Phase 3, plus any
270pages flagged as losing traffic:
271 
272```bash
273python3 "$SKILL_SCRIPTS/url_inspection.py" \
274 --site "sc-domain:example.com" \
275 --urls "/path/to/page1,/path/to/page2,..."
276```
277 
278The script calls `POST https://searchconsole.googleapis.com/v1/urlInspection/index:inspect`
279for each URL and returns per-page:
280- **Indexing status**: `INDEXED`, `NOT_INDEXED`, `SUBMITTED_AND_INDEXED`,
281 `DUPLICATE_WITHOUT_CANONICAL`, `CRAWLED_CURRENTLY_NOT_INDEXED`, etc.
282- **Mobile usability verdict**: `MOBILE_FRIENDLY` or issues found
283- **Rich result status**: which rich result types were detected and their verdict
284- **Last crawl time**: when Googlebot last visited
285- **Referring sitemaps**: which sitemap(s) reference this URL
286- **Coverage state**: full coverage detail from the Index Coverage report
287 
288**If URL Inspection returns 403**: the current auth scope may be read-only. Re-
289authenticate with the broader scope:
290 
291```bash
292gcloud auth application-default login \
293 --scopes=https://www.googleapis.com/auth/webmasters,https://www.googleapis.com/auth/webmasters.readonly
294```
295 
296Then retry `url_inspection.py`.
297 
298**Analyze the inspection results and flag immediately:**
299- Any top-traffic page that is `NOT_INDEXED` or `CRAWLED_CURRENTLY_NOT_INDEXED` —
300 this is a critical issue. Identify which page, what the coverage state says,
301 and what likely caused it (noindex tag, canonical pointing elsewhere, robots
302 blocking, soft 404).
303- Pages with `DUPLICATE_WITHOUT_CANONICAL` — these are leaking authority. The
304 canonical needs to be set.
305- Pages where mobile usability is failing — cross-reference with device split
306 from Phase 3 to confirm whether mobile traffic is below par.
307- Pages with no referring sitemaps — if they are important pages, they should be
308 in a sitemap.
309- Pages with rich result errors where schema exists — this pre-validates Phase 5
310 structured data findings.
311- Pages whose last crawl time is more than 60 days ago despite having traffic —
312 crawl budget issue or accidental de-prioritization.
313 
314---
315 
316## Phase 3.6 — CMS Content Inventory (Optional)
317 
318This phase is **non-blocking** — if no CMS is configured it is silently skipped.
319 
320### Detect configured CMS
321 
322```bash
323CMS_TYPE=$(python3 "$SKILL_SCRIPTS/cms_detect.py" 2>/dev/null)
324CMS_DETECT_EXIT=$?
325```
326 
327- Exit code **2** → no CMS configured. Skip this phase entirely, no mention needed.
328- Exit code **0** → CMS detected. Run the matching preflight below.
329 
330### Run preflight and fetch
331 
332```bash
333CMS_CONTENT_FILE=$(SKILL_SCRIPTS="$SKILL_SCRIPTS" python3 -c "import os, sys, tempfile; sys.path.insert(0, os.environ['SKILL_SCRIPTS']); from _uid import portable_uid; print(os.path.join(tempfile.gettempdir(), f'cms_content_{portable_uid()}.json'))")
334 
335case "$CMS_TYPE" in
336 strapi)
337 python3 "$SKILL_SCRIPTS/preflight_strapi.py"
338 CMS_PREFLIGHT=$?
339 [ "$CMS_PREFLIGHT" = "0" ] && python3 "$SKILL_SCRIPTS/fetch_strapi_content.py" --output "$CMS_CONTENT_FILE"
340 ;;
341 wordpress)
342 python3 "$SKILL_SCRIPTS/preflight_wordpress.py"
343 CMS_PREFLIGHT=$?
344 [ "$CMS_PREFLIGHT" = "0" ] && python3 "$SKILL_SCRIPTS/fetch_wordpress_content.py" --output "$CMS_CONTENT_FILE"
345 ;;
346 contentful)
347 python3 "$SKILL_SCRIPTS/preflight_contentful.py"
348 CMS_PREFLIGHT=$?
349 [ "$CMS_PREFLIGHT" = "0" ] && python3 "$SKILL_SCRIPTS/fetch_contentful_content.py" --output "$CMS_CONTENT_FILE"
350 ;;
351 ghost)
352 python3 "$SKILL_SCRIPTS/preflight_ghost.py"
353 CMS_PREFLIGHT=$?
354 [ "$CMS_PREFLIGHT" = "0" ] && python3 "$SKILL_SCRIPTS/fetch_ghost_content.py" --output "$CMS_CONTENT_FILE"
355 ;;
356esac
357```
358 
359**Preflight exit codes:**
360- **0** → ready. Content fetched to `$CMS_CONTENT_FILE`. Load it and use the data in Phase 4.
361- **2** → not configured. Skip silently.
362- **1** → auth/config error. Show the error and ask the user if they want to fix it
363 (suggest `/setup-cms`) or continue without CMS data.
364 
365### What to do with the CMS data
366 
367Load `$CMS_CONTENT_FILE`. All CMSes produce the same normalized format:
368`cms_content.entries` is a list of published articles with slugs and SEO fields.
369 
370Cross-reference against GSC data:
371 
372**1. Published content with no GSC visibility** — CMS entries whose `slug` appears in no
373GSC query or page data. This could mean: not yet indexed, canonicalized to another URL,
374recently published (GSC data lags ~3 days), property mismatch, or genuinely not ranking.
375For each: cross-check in Phase 5 technical crawl (indexability, robots.txt, canonical tags).
376Do not assume "zero impressions = indexed but not ranking" — it may simply be unindexed.
377 
378**2. Content gaps with intent signal** — GSC queries ranking 11-30 with `>200` impressions
379where no CMS entry targets that keyword in its title or slug. These are confirmed demand
380signals you can close with a new article.
381 
382**3. Stale content needing refresh** — CMS entries where `updated_at` is >6 months ago
383AND the corresponding page appears in `comparison.declining_pages`. Age alone isn't a problem;
384age + declining clicks is.
385 
386**4. Missing SEO fields** — Use `cms_content.seo_audit` directly:
387- `missing_meta_title` — entries with no meta title set
388- `missing_meta_description` — entries with no meta description set
389- `meta_title_too_long` — meta titles over 60 characters
390- `meta_description_too_short/too_long` — outside 70-160 char range
391 
392Surface the top 5 most impactful fixes (by impressions where GSC data matches).
393 
394### Pushing fixes back (Strapi only)
395 
396For Strapi, after generating recommendations in Phase 6, offer to write the fixes directly:
397 
398> "I can push the meta title/description fixes directly to Strapi. Want me to apply them?"
399 
400```bash
401python3 "$SKILL_SCRIPTS/push_strapi_seo.py" \
402 --document-id "<documentId>" \
403 --meta-title "New title under 60 chars" \
404 --meta-description "New description 70-160 chars."
405# Or batch: python3 "$SKILL_SCRIPTS/push_strapi_seo.py" --batch-file /tmp/seo_updates.json
406```
407 
408The script shows a before/after diff and requires confirmation before writing.
409 
410### Setup / reconfiguration
411 
412If no CMS is configured and the user wants to connect one, suggest:
413> "Run `/setup-cms` to connect WordPress, Strapi, Contentful, or Ghost."
414 
415---
416 
417## Phase 3.7 — Business & Persona Discovery
418 
419Understanding who visits the site — and why — shapes every recommendation from
420Phase 4 onward. A title tag rewrite, a content gap, or a keyword recommendation
421only moves the needle if it speaks the language of the people actually searching.
422This phase builds that foundation using real data you already have.
423 
424By this point you have: the homepage content (pre-fetched in the parallel data
425collection step), GSC top queries and top pages (Phase 3), and the site's URL
426structure. This is much richer than scraping the homepage alone — GSC queries
427reveal what real visitors search for, in their own words.
428 
429### Check for cached personas
430 
431Personas are cached at `~/.toprank/personas/` keyed by domain hostname. Check
432whether a persona file already exists (`$DOMAIN` is already set from Step 0.5):
433 
434```bash
435PERSONA_FILE="$HOME/.toprank/personas/$DOMAIN.json"
436[ -f "$PERSONA_FILE" ] && cat "$PERSONA_FILE" || echo "NOT_FOUND"
437```
438 
439**If found and `saved_at` is less than 90 days old**: Show a one-line summary of
440each persona and continue. No confirmation pause needed — the user already
441approved these. If the user proactively says "refresh personas" at any point,
442re-run the discovery below.
443 
444**If found but stale (>90 days)** or **not found**: Continue to discovery below.
445 
446### Discover personas from GSC + site content
447 
448Combine these data sources — do not fetch any new pages (you already have them):
449 
4501. **GSC top queries** (from Phase 3) — the actual words real visitors type. Group
451 by search intent: who searches informational queries vs transactional vs
452 commercial investigation? These are different people with different needs.
453 
4542. **GSC top pages** (from Phase 3) — which pages get traffic reveals what the site
455 is known for (vs. what it claims on the homepage).
456 
4573. **Homepage content** (already fetched for Phase 5) — extract: what the business
458 does, who they serve, value proposition, tone/vocabulary, conversion intent.
459 
4604. **URL structure** (from page groups in GSC) — /blog/ vs /products/ vs /pricing/
461 reveals different visitor segments.
462 
463From these signals, identify the 2-3 most distinct visitor segments. For each:
464 
465| Field | What to capture | Why it matters |
466|-------|----------------|----------------|
467| **Name** | Descriptive label (e.g., "Budget-Conscious Founder") | Quick reference throughout the report |
468| **Demographics** | Role, company size, technical level | Calibrates language register |
469| **Primary goal** | What they're trying to accomplish | Shapes title tags and meta descriptions |
470| **Pain points** | Problems driving them to search | Informs content angle and CTAs |
471| **Search behavior** | Query types, informational vs transactional | Maps personas to GSC query clusters |
472| **Language** | Specific words, phrases, jargon they use | Direct input to title/description rewrites |
473| **Decision trigger** | What makes them convert or return | Shapes CTA and landing page copy |
474 
475Be specific. "Small business owner comparing field-service software for a 3-location
476operation" is useful. "Users who want to learn more" is not. Ground every persona
477in actual GSC query patterns — if you can't point to a cluster of queries that
478this persona would type, the persona is speculative and should be dropped.
479 
480### Persist personas
481 
482Save to `~/.toprank/personas/<domain>.json` using a Python one-liner to ensure
483valid JSON (not a heredoc — heredocs with JSON are fragile):
484 
485```bash
486mkdir -p "$HOME/.toprank/personas"
487python3 -c "
488import json, sys
489data = {
490 'domain': '$DOMAIN',
491 'saved_at': '$(date -u +%Y-%m-%dT%H:%M:%SZ)',
492 'business_summary': '<FILL: 1-2 sentence business description>',
493 'personas': [
494 {
495 'name': '<FILL>',
496 'demographics': '<FILL>',
497 'primary_goal': '<FILL>',
498 'pain_points': '<FILL>',
499 'search_behavior': '<FILL>',
500 'language': ['<FILL: term1>', '<FILL: term2>', '<FILL: term3>'],
501 'decision_trigger': '<FILL>'
502 }
503 ]
504}
505json.dump(data, open('$PERSONA_FILE', 'w'), indent=2)
506print('Personas saved to $PERSONA_FILE')
507"
508```
509 
510Replace all `<FILL: ...>` placeholders with actual discovered values before
511running. The Python approach avoids shell quoting issues with apostrophes and
512special characters in persona descriptions.
513 
514### Present personas (non-blocking)
515 
516Show the personas in a compact table — do NOT pause for confirmation. The user
517already confirmed the URL and brand terms; personas are derived from their data,
518not guessed. Present them as context for what follows:
519 
520> "Based on your GSC data and site content, I've identified these visitor personas
521> that will shape the recommendations:"
522>
523> | Persona | Searches like... | Goal |
524> |---------|-----------------|------|
525> | [name] | [2-3 example query patterns from GSC] | [goal] |
526>
527> "Let me know if any of these are off — otherwise I'll use them throughout the
528> analysis."
529 
530Then immediately continue to Phase 4. Do not wait for a response. If the user
531corrects a persona later, update the file and adjust any affected recommendations.
532 
533**Reference `$PERSONA_FILE` path as `~/.toprank/personas/<domain>.json` in later
534phases — derive `<domain>` from the target URL each time rather than relying on
535shell variable persistence.**
536 
537**No-GSC fallback**: If GSC was unavailable and you skipped to Phase 5 directly,
538still run persona discovery before Phase 5's analysis — but rely only on the
539homepage content (already fetched) and URL structure. The personas will be less
540precise without query data; note this in the report and recommend re-running the
541audit with GSC access for better persona accuracy.
542 
543---
544 
545## Phase 3.8 — Business Context
546 
547Read and follow `../shared/business-context.md`.
548 
549By this point you have GSC data (Phase 3) and homepage content — the two inputs needed to infer business facts before asking the user anything. The goal is to ask as few questions as possible while generating a complete, useful profile.
550 
551Branch on `CACHE_STATUS` from Phase 2:
552 
553**`fresh_loaded`**: business context is already in memory. No action needed — proceed to Phase 4.
554 
555**`not_found`**: run the Generation flow from `../shared/business-context.md`. Seed `brand_terms` with `$BRAND_TERMS` from Phase 2 if the user provided them; supplement with additional brand signals inferred from GSC queries.
556 
557**`stale`**: run Generation to refresh. `CACHE_STATUS=stale` means the file was loaded — use those values to pre-fill the three questions so the user confirms or corrects rather than re-enters from scratch.
558 
559This phase adds ~30 seconds and one exchange with the user on first run. On all subsequent runs it is silent (cache load only). The payoff: Phase 6 recommendations reference the business by name, compare against real competitors, and focus on the primary goal rather than giving generic SEO advice.
560 
561---
562 
563## Phase 4 — Search Console Analysis
564 
565This is where you earn your keep. Do not just restate the data. Interpret it like
566an SEO expert would.
567 
568### Traffic Overview
569 
570State totals: clicks, impressions, average CTR, average position for the period.
571Note any dramatic changes. Compare to typical CTR curves for given positions
572(position 1 should see ~25-30% CTR, position 3 about 10%, position 10 about 2%).
573If a query's CTR is significantly below what its position would predict, that is
574a signal the title/snippet needs work.
575 
576### Branded vs Non-Branded Split
577 
578If `branded_split` is present (not null), show it as the first table in the analysis:
579 
580| Segment | Queries | Clicks | Impressions | CTR | Avg Position |
581|---------|---------|--------|-------------|-----|--------------|
582| Branded | X | X | X | X% | X |
583| Non-branded | X | X | X | X% | X |
584 
585Interpret the gap:
586- If branded CTR is significantly higher (expected — users know what they're looking for), note that non-branded metrics are the real measure of organic performance.
587- If branded impressions are small vs total, the site has limited brand awareness — focus on non-branded growth.
588- If branded queries are ranking below position 3, that's a reputation/brand issue to flag separately.
589- Use non-branded metrics as the baseline for all Quick Wins and content recommendations — don't let branded traffic inflate the opportunity estimates.
590 
591### Quick Wins (highest impact, lowest effort)
592 
593These are the changes that can move the needle in days, not months:
594 
5951. **Position 4-10 queries** — ranking on page 1 but below the fold. A title tag
596 or meta description improvement, internal linking push, or content expansion
597 could jump them into the top 3. List the top 10 with current position,
598 impressions, and a specific recommendation for each.
599 
6002. **High-impression, low-CTR queries** — use `ctr_gaps_by_page` (not just
601 `ctr_opportunities`) because it includes the exact page URL alongside the
602 query. This means every recommendation can name the specific page to fix
603 and the specific query driving impressions. For each, analyze the likely
604 search intent (informational, transactional, navigational, commercial
605 investigation) and suggest a title + description that matches it.
606 
6073. **Queries dropping month-over-month** — flag anything with >30% click decline.
608 For each, hypothesize: is it seasonal? Did a competitor take the SERP feature?
609 Did the page content drift from the query intent?
610 
611### Search Intent Analysis
612 
613For the top 10-15 queries, classify the search intent:
614- **Informational** ("how to...", "what is...") → needs comprehensive content,
615 FAQ schema
616- **Transactional** ("buy...", "pricing...", "near me") → needs clear CTA,
617 product schema, price
618- **Navigational** ("brand name", "brand + product") → should be ranking #1,
619 if not, investigate
620- **Commercial investigation** ("best...", "vs...", "review") → needs comparison
621 content, trust signals
622 
623If the page ranking for a query does not match the intent (e.g., a blog post
624ranking for a transactional query, or a product page ranking for an informational
625query), flag it. This is often the single biggest unlock.
626 
627**Persona lens**: Once intent is classified, cross-reference each query against
628the personas from Phase 3.7. Which persona is most likely searching this query?
629Are the vocabulary and framing in the current title/snippet the same words that
630persona would use? A title written for one persona can actively repel another.
631For example, a query attracting "The Budget-Conscious Founder" persona should
632use plain-language value framing, while the same topic searched by "The IT
633Manager" persona may expect technical specificity. Note the persona alignment
634(or mismatch) for every Quick Win recommendation.
635 
636### Keyword Cannibalization Check
637 
638The output includes a `cannibalization` array. Each entry has structured winner/loser
639scoring — use it directly instead of re-deriving from raw data:
640 
641- `winner_page` — the canonical page to keep (scored by best position, tiebreaker: most clicks)
642- `winner_reason` — why it won (e.g. "best position (2.1)")
643- `loser_pages` — pages to consolidate away
644- `recommended_action` — either "consolidate: 301 redirect losers to winner or add canonical" or "monitor: possible SERP domination" (all pages in top 5, positions within 2 of each other)
645 
646For each cannibalized query:
647- State the winner and losers explicitly — don't make the user figure it out
648- Use `recommended_action` directly in your recommendation
649- Flag queries where position is mediocre (5-15) despite high impressions — splitting is likely suppressing a potential top-3 ranking
650- If `recommended_action` is "monitor: possible SERP domination", note this as a positive (owning multiple SERP spots) and skip the consolidation recommendation
651 
652Also cross-check `top_pages` and `position_buckets` for indirect signals: a page
653that used to rank well dropping after a new page was published, or wild position
654fluctuation on a query, are signs of cannibalization not yet in the data window.
655 
656### Page Group Performance
657 
658Use `page_groups` to show which site sections are winning and which need attention:
659 
660| Section | Pages | Clicks | Impressions | CTR | Avg Position |
661|---------|-------|--------|-------------|-----|--------------|
662| /blog/ | X | X | X | X% | X |
663| /products/ | X | X | X | X% | X |
664| ... | | | | | |
665 
666Flag:
667- **Low-CTR sections**: if an entire section (e.g., all /products/ pages) has CTR well below site average, the issue is likely a template problem (title tag format, meta description format) — one fix improves all pages in that section.
668- **High-impression, low-click sections**: signals ranking without converting — investigate intent mismatch or snippet quality across the section.
669- **Sections missing entirely**: if /locations/ or /services/ doesn't appear, either those pages don't rank or they haven't been created.
670- **"other" group is large**: means the site has custom URL patterns not covered by defaults — note this for the user so they can understand what's in "other."
671 
672This is more actionable than per-page analysis: a recommendation like "the /products/ title tag template needs work" can fix 50 pages at once.
673 
674### Segment Analysis
675 
676**Device** (`device_split`): Compare CTR and position across mobile/desktop/
677tablet. A page can look healthy overall but be failing on mobile. Flag any device
678where CTR is >30% below the site average — that is a mobile UX or snippet
679problem.
680 
681**Country** (`country_split`): Look at the top countries. Flag cases where:
682- A country has high impressions but very low CTR (title/snippet not landing in
683 that market)
684- Position is much worse in one country vs others (local competitor or relevance
685 gap)
686- A country with meaningful impressions has near-zero clicks (potential hreflang
687 or geo-targeting issue)
688 
689**Search type** (`search_type_split`): If `discover` or `googleNews` appear,
690note them — they behave differently from web search and have separate optimization
691levers (freshness, images, authority signals). If `image` or `video` traffic
692exists and the site does not have dedicated image/video optimization, call that
693out as an opportunity.
694 
695### Content Gaps
696 
697Queries where you rank 11-30 — you have topical authority but need a dedicated
698page or content expansion. Group related queries into topic clusters. For each
699cluster, recommend whether to:
700- Expand an existing page (if it partially covers the topic)
701- Create a new page (if no page targets this topic)
702- Create a content hub with internal linking (if there are 5+ related queries)
703 
704### Pages to Fix
705 
706List pages with declining clicks. For each:
707- Current clicks vs previous period
708- % change
709- Likely cause (seasonal, algorithm update, new competitor, content staleness,
710 technical issue)
711- Specific fix recommendation
712 
713---
714 
715## Phase 4.5 — Keyword Gap Analysis
716 
717This phase identifies keyword opportunities directly from the GSC data — no
718external tools required, though running `/keyword-research` afterward can go
719deeper.
720 
721### Step 1: Find Queries Without Dedicated Pages
722 
723From the GSC `top_queries` data, identify queries where:
724- The site ranks 4-20 for the query
725- The page that ranks is NOT a page primarily about that topic (e.g., a homepage
726 or a page written for a different keyword is accidentally ranking)
727- There is no page on the site with that keyword prominently in the title, H1,
728 or URL slug
729 
730These are **keyword orphans** — the site has demonstrated topical relevance but
731has never given the topic its own page. Creating a dedicated page for each is
732typically the highest-leverage content move.
733 
734For each orphan, state:
735- The query
736- Current ranking page (URL) and position
737- Monthly impressions
738- Recommended action: "Create a new page targeting '[query]' — currently ranked
739 #[N] from [URL] which is not dedicated to this topic. A dedicated page could
740 realistically move from #[N] to top 5."
741 
742### Step 2: Build Topic Clusters from GSC Data
743 
744Group all ranking queries by theme. A cluster exists when 3+ queries share a
745core concept. For each cluster:
746- Name the cluster (e.g., "pricing-related queries", "feature X how-to queries")
747- List the queries in it, their positions, and their impressions
748- Identify whether a **pillar page** exists that ties them together
749- If no pillar page exists, recommend creating one and note the internal linking
750 structure needed to funnel authority from cluster pages to the pillar
751 
752### Step 3: Business Context Gap Check
753 
754Based on what the site does (inferred from its URL, top pages, and ranking
755queries), identify topics the business clearly serves that have zero or near-zero
756GSC impressions. These are **business-relevant keyword gaps** — the site should
757be visible for them but is not.
758 
759State the gap explicitly: "This appears to be a [type of business]. You rank for
760[X] but have no impressions for [related topic], which has significant search
761demand. This is a content gap to close."
762 
763### Step 4: Offer Deeper Keyword Research
764 
765After completing the inline analysis, offer:
766 
767> "I've identified [N] keyword gaps from your GSC data. For broader keyword
768> discovery — including keywords you're NOT yet ranking for at all — run
769> `/keyword-research` with your seed topics. That skill pulls from keyword
770> databases and builds a full opportunity set beyond what GSC can see."
771 
772---
773 
774## Phase 5 — Technical SEO Audit
775 
776Crawl the site's key pages to check technical health. Use the firecrawl skill if
777available, otherwise use WebFetch.
778 
779Pages to audit: at most 5 pages total. Prioritize: homepage first, then fill
780remaining slots with top pages by clicks from Phase 4 — unless a page is flagged
781as declining or NOT_INDEXED in Phase 3.5, in which case swap it in. Hard cap at 5
782regardless of how many flagged pages exist; pick the highest-priority ones.
783 
784**⚡ Speed note**: Fetch all 5 pages using parallel WebFetch calls in a single
785turn — do not fetch them one-at-a-time. You should have already pre-fetched
786`robots.txt` and the homepage during Phase 3 (see Parallel Data Collection above);
787if so, only fetch the remaining pages you haven't retrieved yet.
788 
789### Indexability
790 
791- Fetch and analyze `robots.txt` — is it blocking important paths? Are there
792 unnecessary disallow rules?
793- Check for `noindex` meta tags or `X-Robots-Tag` headers on important pages
794- Check canonical URLs — self-referencing (good) or pointing elsewhere
795 (investigate)
796- Check for `hreflang` tags if the site targets multiple languages/regions
797- Look for orphan pages (important pages with no internal links pointing to them)
798- Cross-reference with URL Inspection findings from Phase 3.5 — any NOT_INDEXED
799 page found there should be explained here with the root cause
800 
801### Metadata Audit (Deep)
802 
803For each audited page, fetch the actual `<title>` and `<meta name="description">`
804from the live HTML. Then cross-reference against GSC data:
805 
8061. **Title vs top query alignment**: For each page, look up the top 3 queries
807 that page ranks for in `ctr_gaps_by_page`. Does the title tag contain the
808 primary ranking query or a close variant? If the title is generic (e.g.,
809 "Home", "Services", "Blog") while the page ranks for specific queries, that is
810 a mismatch — the title is failing to confirm relevance and hurting CTR.
811 
8122. **Title length**: Under 60 characters? Over 60 characters gets truncated in
813 SERPs. Flag every page over the limit with the current character count and the
814 truncated version as it would appear in Google.
815 
8163. **Meta description**: Present? 120-160 characters? Contains a call to action?
817 If a page has no meta description, Google rewrites it — often pulling
818 unhelpful boilerplate. Flag every missing description.
819 
8204. **Duplicate titles**: Are multiple pages using the same or very similar titles?
821 List all duplicates found.
822 
8235. **Open Graph tags**: `og:title`, `og:description`, `og:image` present? Missing
824 OG tags means social shares render with no preview — flag any page missing
825 them, especially for content pages.
826 
827Report the findings as a table:
828 
829| Page URL | Title (actual) | Title length | Top GSC query | Title/query match? | Meta desc present? | OG tags? |
830|----------|---------------|--------------|---------------|--------------------|--------------------|----------|
831| / | [actual title] | [N] chars | [query] | Yes / No | Yes / No | Yes / No |
832 
833After presenting the metadata audit table, offer:
834> "I found [N] pages with metadata issues. Run `/meta-tags-optimizer` to generate
835> optimized title tags and meta descriptions for each — it will use the GSC query
836> data from this audit to write titles that match actual search demand."
837 
838### Schema Markup Audit (Deep)
839 
840Detect the site type from its top pages, ranking queries, and visible content,
841then check what schema types exist vs. what should exist for that site type.
842 
843**Step 1: Detect site type**
844 
845Based on the homepage and top pages content, classify as one of:
846- E-commerce (products, pricing, cart)
847- Local business (address, phone, service area)
848- SaaS / software (features, pricing, signup)
849- Content / blog (articles, guides, tutorials)
850- Professional services (agency, consultant, law firm)
851- Media / news (articles published frequently)
852 
853**Step 2: Define expected schema for site type**
854 
855| Site Type | Must Have | High Impact if Missing | Nice to Have |
856|-----------|-----------|------------------------|--------------|
857| E-commerce | Product, BreadcrumbList | AggregateRating, FAQPage, Offer | SiteLinksSearchBox |
858| Local business | LocalBusiness, GeoCoordinates | OpeningHoursSpecification, AggregateRating | FAQPage |
859| SaaS | Organization, SoftwareApplication | FAQPage, BreadcrumbList | HowTo, Review |
860| Content / blog | Article or BlogPosting | FAQPage, BreadcrumbList | HowTo, Video |
861| Professional services | Organization, Service | FAQPage, Review | ProfessionalService, Person |
862| Media / news | NewsArticle | BreadcrumbList | VideoObject, ImageObject |
863 
864**Step 3: Audit each top page for actual schema present**
865 
866For each audited page, extract any `<script type="application/ld+json">` blocks.
867List what `@type` values are present. Then compare against the expected set for
868this site type.
869 
870Report findings:
871 
872| Page URL | Schema found | Missing high-impact schema | Errors in existing schema |
873|----------|-------------|---------------------------|---------------------------|
874| / | Organization | FAQPage, SiteLinksSearchBox | None |
875| /pricing | SoftwareApplication | FAQPage, Offer | Missing `price` property |
876 
877**Step 4: Flag errors in existing schema**
878 
879Common issues to check:
880- Missing required fields for the `@type` (e.g., Product schema without `name`
881 or `offers`)
882- `url` properties using relative paths instead of absolute URLs
883- Dates not in ISO 8601 format
884- `AggregateRating` with `ratingCount` of 0 or missing
885- Duplicate schema blocks for the same type on one page
886- Schema that describes content not visible on the page (violates Google policy)
887 
888Cross-reference with rich result status from Phase 3.5 URL Inspection — if a
889page showed rich result errors there, find the cause here.
890 
891After presenting the schema audit, offer:
892> "I found [N] pages missing high-impact schema and [N] pages with errors in
893> existing schema. Run `/schema-markup-generator` to generate correct JSON-LD for
894> each — it will use the site type and page content from this audit."
895 
896### Core Web Vitals & Performance
897 
898- Render-blocking scripts in `<head>` — should be deferred or async
899- Images: lazy-loaded? Have `alt` attributes? Served in modern formats
900 (WebP/AVIF)? Properly sized (not 3000px wide in a 400px container)?
901- `<link rel="preload">` for critical resources (fonts, above-the-fold images)?
902- Excessive DOM size (>1500 nodes suggests bloat)?
903- Third-party script bloat — count external domains loaded
904 
905### Internal Linking & Site Architecture
906 
907- Does the page have internal links? Are they descriptive (not "click here")?
908- Does the page link to related content (topic clusters)?
909- Is the page reachable within 3 clicks from the homepage?
910- Broken internal links (404s)?
911 
912### Mobile Readiness
913 
914- Viewport meta tag present?
915- Touch targets large enough (48px minimum)?
916- Text readable without zooming?
917- No horizontal scrolling?
918- Cross-reference mobile usability findings from Phase 3.5 URL Inspection
919 
920---
921 
922## Phase 5.5 — PageSpeed Insights (Performance Monitoring)
923 
924Run the PageSpeed Insights API on the homepage + top 4 pages by clicks from
925Phase 3. This provides both **lab data** (Lighthouse synthetic test) and **field
926data** (Chrome UX Report real-user metrics) for Core Web Vitals.
927 
928**⚡ Speed note**: This should already be running in parallel from the Parallel
929Data Collection step. If not, run it now.
930 
931```bash
932python3 "$SKILL_SCRIPTS/pagespeed.py" \
933 --urls "$TARGET_URL,https://example.com/page2,https://example.com/page3" \
934 --both-strategies
935```
936 
937Replace the example URLs with the actual homepage and top pages from Phase 3.
938Use `--both-strategies` to get both mobile and desktop scores. If the user has
939set `PAGESPEED_API_KEY` in their environment, the script uses it automatically
940for higher rate limits.
941 
942After `pagespeed.py` completes, run the display utility:
943 
944```bash
945python3 "$SKILL_SCRIPTS/show_pagespeed.py"
946```
947 
948### Analyze the Results
949 
950**1. Performance Scores** — Lighthouse scores 0-100 per page:
951- **90-100 (Good)**: No action needed.
952- **50-89 (Needs Work)**: Flag the top opportunities. These pages are losing
953 rankings due to performance — Google uses Core Web Vitals as a ranking signal.
954- **0-49 (Poor)**: Critical. These pages are actively penalized in rankings.
955 Flag as a Priority Action if the page has significant organic traffic.
956 
957**2. Core Web Vitals (Field Data)** — Real-user metrics from Chrome UX Report:
958- **LCP** (Largest Contentful Paint): Good < 2.5s, Poor > 4.0s
959- **INP** (Interaction to Next Paint): Good < 200ms, Poor > 500ms
960- **CLS** (Cumulative Layout Shift): Good < 0.1, Poor > 0.25
961 
962Field data is more authoritative than lab data for SEO — Google uses CrUX data
963for rankings. If field data is available, lead with it. If not (low-traffic
964sites often lack CrUX data), use lab data and note it's synthetic.
965 
966**3. Cross-Reference with Other Phases**:
967- **Phase 3 device split**: If mobile performance score is significantly lower
968 than desktop, and Phase 3 shows mobile traffic underperforming, the
969 performance gap is likely a contributing factor.
970- **Phase 5 technical audit**: Correlate specific opportunities (e.g.,
971 "Eliminate render-blocking resources") with the technical findings (e.g.,
972 render-blocking scripts in `<head>`). This gives concrete evidence for
973 technical fixes.
974- **Phase 3.5 URL Inspection**: Pages flagged as mobile-unfriendly that also
975 have poor mobile PageSpeed scores need urgent attention.
976 
977**4. Top Opportunities** — The script extracts Lighthouse optimization
978opportunities sorted by potential time savings. For each, note:
979- What the opportunity is (e.g., "Properly size images", "Remove unused JavaScript")
980- Estimated savings in milliseconds
981- Which specific page(s) are affected
982- Whether it's a site-wide template issue or page-specific
983 
984**5. Origin-Level Data** — If available, the origin (site-wide) CrUX data shows
985the overall performance health of the entire domain. Compare individual page
986scores against the origin average to identify outlier pages dragging down the
987site's overall performance profile.
988 
989---
990 
991## Phase 6 — Report
992 
993**The goal of this report is not comprehensiveness — it is clarity.** The user needs to know exactly what to do next, in what order, and why. Lead with the highest-impact actions. Put supporting data after. Omit anything that doesn't change what the user should do.
994 
995Output a structured report using this format exactly:
996 
997---
998 
999# SEO Report — [site.com]
1000*[date] · GSC data: [date range] · [First audit / Previous audit: date]*
1001 
1002## Audit History
1003*(Skip this section entirely on the first audit — do not write "N/A" or "First audit" here; just omit the section.)*
1004 
1005On subsequent audits, show only what changed from the previous audit's top issues:
1006 
1007| Previously Flagged | Status | Notes |
1008|--------------------|--------|-------|
1009| [Issue from last audit] | ✅ Resolved / ⚠️ Improved / 🔴 Still present / ↗ Worsened | [1-line update with current metric] |
1010 
1011---
1012 
1013## ⚡ Top Priority Actions
1014 
1015This is the core of the report. Include exactly 3–5 items, ordered by expected click impact. Every item must have a specific URL, a specific metric as evidence, and a specific fix — nothing generic.
1016 
1017Use this format for each:
1018 
1019---
1020 
1021**#1 — [Short title, e.g. "Fix title tag on /pricing"]**
1022🔴 Critical / 🟡 High / 🟢 Medium
1023**Impact**: ~+[N] clicks/mo · **Effort**: Low / Med / High
1024 
1025**What**: [One sentence describing the problem]
1026**Evidence**: [Exact metric — e.g., "ranks #7 for 'your-product pricing': 2,400 impressions/mo, 1.2% CTR (expected ~3% at this position)"]
1027**Fix**: [Specific, copy-paste-ready action — e.g., "Change title from 'Pricing' to 'Plans & Pricing — [Value Prop] | [Brand]' (54 chars)"]
1028**Why it works**: [One sentence on the mechanism — intent match, persona language, etc.]
1029 
1030---
1031 
1032Repeat for each of the 3–5 items. Do not add a 6th item — triage ruthlessly. An item only makes the list if you can quantify its impact.
1033 
1034When estimating impact, use conservative CTR curves: position 1 ~27%, position 2 ~15%, position 3 ~11%, position 4–5 ~5–8%, position 6–10 ~2–4%. Moving from position 7 to 3 on a 2,400 impression/month query means roughly +170 clicks/month. Always use real numbers from the data.
1035 
1036Every persona-informed recommendation must name the persona and cite the specific language from that persona's `language` field that should appear in the rewrite.
1037 
1038---
1039 
1040## Traffic Snapshot
1041 
1042| Metric | Value | vs Prior 28 days |
1043|--------|-------|-----------------|
1044| Total Clicks | X | ↑/↓ X% |
1045| Impressions | X | ↑/↓ X% |
1046| Avg CTR | X% | ↑/↓ |
1047| Avg Position | X | ↑/↓ |
1048 
1049*(Branded/non-branded split — only if brand terms were provided):*
1050| Segment | Clicks | Impressions | CTR | Avg Position |
1051|---------|--------|-------------|-----|--------------|
1052| Branded | X | X | X% | X |
1053| Non-branded | X | X | X% | X |
1054 
1055[1-sentence interpretation of the split — what it reveals about organic vs brand performance]
1056 
1057---
1058 
1059## Supporting Findings
1060 
1061This section exists to back up the Priority Actions and surface anything else the user should know. Keep it concise — tables and short bullets, not prose paragraphs. Only include sub-sections where there are actual findings.
1062 
1063### Indexing Issues
1064*(From Phase 3.5. Only include if issues found.)*
1065| Page | Coverage State | Last Crawl | Fix |
1066|------|---------------|------------|-----|
1067 
1068### Keyword Cannibalization
1069*(Only include if `cannibalization` data is non-empty.)*
1070| Query | Winner Page | Loser Pages | Action |
1071|-------|------------|-------------|--------|
1072 
1073### Content Gaps
1074*(Queries ranking 11–30 with >200 impressions and no dedicated page.)*
1075| Query | Position | Impressions/mo | Recommended Action |
1076|-------|----------|---------------|--------------------|
1077 
1078### Metadata Issues
1079*(Only pages not already covered in Priority Actions.)*
1080| Page | Issue | Current | Recommended Fix |
1081|------|-------|---------|-----------------|
1082 
1083### Schema Gaps
1084*(High-impact missing schema for this site type.)*
1085| Page | Missing | Impact |
1086|------|---------|--------|
1087 
1088### Technical Issues
1089*(Severity: Critical / High / Medium. Omit Low unless they surface as Priority Actions.)*
1090| Issue | Pages Affected | Fix | Severity |
1091|-------|---------------|-----|----------|
1092 
1093### PageSpeed & Core Web Vitals
1094*(From Phase 5.5. Only include if issues found. Lead with field data if available, fall back to lab data.)*
1095 
1096**Site-wide (Origin)**: [Overall CrUX rating if available]
1097 
1098| Page | Score | LCP | INP | CLS | Top Opportunity |
1099|------|-------|-----|-----|-----|-----------------|
1100| / | [score] | [value] [rating] | [value] [rating] | [value] [rating] | [top opportunity title + savings] |
1101 
1102*(If any page scores below 50, flag it as a Priority Action candidate — poor Core Web Vitals directly hurt rankings.)*
1103 
1104### Traffic Drops
1105*(Pages/queries with >30% decline. Only include if not already in Priority Actions.)*
1106| Page / Query | Change | Hypothesis | Next Step |
1107|-------------|--------|------------|-----------|
1108 
1109### CMS SEO Audit
1110*(Only if a CMS is configured. Top 5 impactful fixes only.)*
1111| Page | Issue | Current | Fix |
1112|------|-------|---------|-----|
1113 
1114---
1115 
1116## What to Ignore (For Now)
1117List 2–3 things the data shows but that don't make the priority list — so the user knows you saw them and deprioritized them deliberately. One line each.
1118 
1119- [e.g., "Device split: mobile CTR 15% below desktop — worth watching but not the bottleneck right now"]
1120- [e.g., "Country split: weak CTR in UK — low volume, investigate after core issues fixed"]
1121 
1122---
1123 
1124After the report, write the audit log entry (see Phase 6.5 below before ending).
1125 
1126---
1127 
1128## Phase 6.5 — Write Audit Log
1129 
1130After delivering the report, append a concise entry to the audit log. `$DOMAIN` and `$AUDIT_LOG` are already set from Step 0.5.
1131 
1132```bash
1133mkdir -p "$HOME/.toprank/audit-log"
1134```
1135 
1136Use Python to append (creates the file with a single-element array if it doesn't exist). Replace all `<FILL>` values with real data from the report before running:
1137 
1138```python
1139import json, os
1140from datetime import datetime, timezone
1141 
1142log_path = "$AUDIT_LOG"
1143existing = json.load(open(log_path)) if os.path.exists(log_path) else []
1144 
1145existing.append({
1146 "date": datetime.now(timezone.utc).strftime("%Y-%m-%d"),
1147 "traffic_snapshot": {
1148 "clicks": <FILL>,
1149 "impressions": <FILL>,
1150 "avg_ctr_pct": <FILL>,
1151 "avg_position": <FILL>
1152 },
1153 "pagespeed_snapshot": {
1154 "avg_score_mobile": <FILL or null>,
1155 "avg_score_desktop": <FILL or null>,
1156 "homepage_score_mobile": <FILL or null>,
1157 "cwv_lcp_ms": <FILL or null>,
1158 "cwv_inp_ms": <FILL or null>,
1159 "cwv_cls": <FILL or null>,
1160 "cwv_source": "<FILL: field|lab>" # "field" if CrUX data available, else "lab"
1161 },
1162 "top_issues": [
1163 # One entry per Priority Action (max 5), in priority order
1164 {"rank": 1, "title": "<FILL>", "type": "<FILL: title_tag|indexing|cannibalization|schema|content_gap|performance>", "page": "<FILL>", "metric": "<FILL>", "expected_impact": "<FILL>", "status": "open"}
1165 ],
1166 "resolved_from_previous": [] # populated on next audit from Audit History comparison
1167})
1168 
1169json.dump(existing, open(log_path, "w"), indent=2)
1170print(f"Audit log saved to {log_path}")
1171```
1172 
1173Confirm with a one-liner: "Audit log saved to `~/.toprank/audit-log/$DOMAIN.json`."
1174 
1175---
1176 
1177## Phase 7 — Targeted Skill Handoffs (Optional)
1178 
1179After delivering the report, surface the follow-up actions based on what was
1180found. Only offer handoffs where the audit actually found issues — do not offer
1181all three if only one is relevant.
1182 
1183### Metadata Handoff
1184 
1185If the metadata audit found [N] pages with issues:
1186 
1187> "I found [N] pages with metadata issues — [X] with title/query mismatches,
1188> [Y] missing meta descriptions, [Z] missing OG tags. Run `/meta-tags-optimizer`
1189> to generate optimized tags for each page. Share the metadata audit table from
1190> this report as context."
1191 
1192### Schema Handoff
1193 
1194If the schema audit found gaps or errors:
1195 
1196> "I found [N] pages missing high-impact schema and [N] pages with schema errors.
1197> Run `/schema-markup-generator` to generate correct JSON-LD. The schema audit
1198> table from this report is the input — it already identifies the site type and
1199> what schema types are needed per page."
1200 
1201### Keyword Research Handoff
1202 
1203If the keyword gap analysis found orphan keywords or business relevance gaps:
1204 
1205> "I found [N] keyword gaps from GSC data. For deeper discovery — keywords you
1206> are not ranking for at all — run `/keyword-research` with these seed topics:
1207> [list 3-5 seed terms derived from the gap analysis]. That skill pulls from
1208> keyword databases and builds a full opportunity set beyond what GSC can see."
1209 
1210---
1211 
1212## Phase 8 — Content Generation (Optional)
1213 
1214After delivering the report, if the Content Opportunities section identified
1215actionable content gaps, offer to generate the content:
1216 
1217> "I found [N] content opportunities. Want me to draft the content? I can write
1218> [blog posts / landing pages / both] in parallel — each one optimized for the
1219> target keyword and search intent."
1220 
1221If the user agrees, spawn content agents **in parallel** using the Agent tool.
1222Each agent writes one piece of content independently.
1223 
1224### How to Spawn Content Agents
1225 
1226For each content opportunity, determine the content type from the search intent:
1227- **Informational / commercial investigation** → blog post agent
1228- **Transactional / commercial** → landing page agent
1229 
1230Spawn agents in parallel. Each agent receives:
12311. The content writing guidelines (located via find — see below)
12322. The specific opportunity data from the analysis
1233 
1234Before spawning agents, locate the content writing reference:
1235 
1236```bash
1237CONTENT_REF=$(find ~/.claude/plugins ~/.claude/skills ~/.codex/skills .agents/skills -name "content-writing.md" -path "*content-writer*" 2>/dev/null | head -1)
1238if [ -z "$CONTENT_REF" ]; then
1239 echo "WARNING: content-writing.md not found. Content agents will use built-in knowledge only."
1240else
1241 echo "Content reference at: $CONTENT_REF"
1242fi
1243```
1244 
1245Pass `$CONTENT_REF` as the path in each agent prompt below. If not found, omit
1246the "Read the content writing guidelines" line — the agents will still produce
1247good content using built-in knowledge.
1248 
1249Use this prompt template for each agent:
1250 
1251#### Blog Post Agent Prompt
1252 
1253```
1254You are a senior content strategist writing a blog post that ranks on Google.
1255 
1256Read the content writing guidelines at: $CONTENT_REF
1257Follow the "Blog Posts" section exactly.
1258 
1259## Assignment
1260 
1261Target keyword: [keyword]
1262Current position: [position] (query ranked but no dedicated content)
1263Monthly impressions: [impressions]
1264Search intent: [informational / commercial investigation]
1265Site context: [what the site is about, its audience]
1266Existing pages to link to: [relevant internal pages from the analysis]
1267[If available] Competitor context: [what currently ranks for this keyword]
1268 
1269## Target Personas
1270Write primarily for: [Primary persona name]
1271Their goal: [primary goal]
1272Their language: [key terms and phrases they use — use these naturally in headings, intro, and body]
1273Their pain points: [pain points — address these directly, don't make them search for answers]
1274Secondary audience: [Secondary persona name if applicable] — [brief note on how to serve both without diluting focus]
1275 
1276## Deliverables
1277 
1278Write the complete blog post following the guidelines, including:
12791. Full post in markdown with proper heading hierarchy
12802. SEO metadata (title tag, meta description, URL slug)
12813. JSON-LD structured data (Article/BlogPosting + FAQPage if FAQ included)
12824. Internal linking plan (which existing pages to link to/from)
12835. Publishing checklist
1284 
1285## Quality Gate
1286Before finishing, verify:
1287- Would the reader need to search again? (If yes, not done)
1288- Does the post contain specific examples only an expert would include?
1289- Does the format match what Google shows for this query?
1290- Is every paragraph earning its place? (No filler)
1291```
1292 
1293#### Landing Page Agent Prompt
1294 
1295```
1296You are a senior conversion copywriter writing a landing page that ranks AND converts.
1297 
1298Read the content writing guidelines at: $CONTENT_REF
1299Follow the "Landing Pages" section exactly.
1300 
1301## Assignment
1302 
1303Target keyword: [keyword]
1304Current position: [position]
1305Monthly impressions: [impressions]
1306Search intent: [transactional / commercial]
1307Page type: [service / product / location / comparison]
1308Site context: [what the site is about, value prop, target customer]
1309Existing pages to link to: [relevant internal pages]
1310[If available] Competitor context: [what currently ranks]
1311 
1312## Target Personas
1313Write primarily for: [Primary persona name]
1314Their goal: [primary goal when landing here]
1315Their language: [terms they use — mirror this in headlines, subheads, and CTAs]
1316Their decision trigger: [what makes them convert — address this prominently above the fold]
1317Their objections: [pain points and doubts — address each explicitly, don't leave them wondering]
1318 
1319## Deliverables
1320 
1321Write the complete landing page following the guidelines, including:
13221. Full page copy in markdown with proper heading hierarchy and CTA placements
13232. SEO metadata (title tag, meta description, URL slug)
13243. Conversion strategy (primary CTA, objections addressed, trust signals)
13254. JSON-LD structured data
13265. Internal linking plan
13276. Publishing checklist
1328 
1329## Quality Gate
1330Before finishing, verify:
1331- Would you convert after reading this? (If not, what is missing?)
1332- Are there vague claims that should be replaced with specifics?
1333- Is every objection addressed?
1334- Is it clear what the visitor should do next?
1335```
1336 
1337### Spawning Rules
1338 
1339- Spawn up to **5 content agents in parallel** (more than 5 gets unwieldy —
1340 prioritize by impact)
1341- Prioritize opportunities by: impressions x position-improvement-potential
1342- Each agent works independently — they do not need to coordinate
1343- As agents complete, present each piece of content to the user with its metadata
1344- After all agents finish, provide a summary: what was generated, suggested
1345 publishing order (highest impact first), and any cross-linking between new pages
1346