Example output: examples/seo-local-sweetgreen-com-20260514/LOCAL-SEO-REPORT.md
Local SEO
Score a local business's website against the signals that drive local-pack and "near me" visibility — GBP integration on the page, NAP consistency, on-page local intent, citation footprint on Tier-1 directories, review-platform presence, and local-pack rank for the business's primary keywords. Deliverable is one prioritised fix list, anchored in observable signals.
Adapted from
AgriciDaniel/claude-seo'sseo-localskill (MIT). Concept and dimension structure mirror the upstream; backend rewired to SE Ranking + Firecrawl + Google APIs. DataForSEO Maps geo-grid and Business Listings checks from the upstream are dropped (no equivalent backend) — see "Limitations" in the deliverable.
Prerequisites
- SE Ranking MCP server connected (used for local-pack rank, on-page audit data, domain context).
- Claude's
WebFetchtool available (used for sense-check fallback when Firecrawl is unavailable). - User provides: (a) a target domain or homepage URL, (b) at least one primary local keyword (e.g.
"dentist Brooklyn","plumber near me"), (c) target country and ideally city/region for local-pack scoping. Optional: GBP listing URL, Yelp/Trustpilot URLs for review scraping.
Process
-
Validate target & preflight. See
skills/seo-firecrawl/references/preflight.mdfor the canonical 3-stage preflight (credit balance, Firecrawl availability, Google APIs). Skill-specific notes:- Normalise the target (strip protocol from domain; confirm homepage is fetchable). Confirm at least one local keyword was provided — if none, infer from
<title>+<h1>of the homepage; if still ambiguous, ask the user before continuing. - Estimated SE Ranking cost for this skill: ~15–25 credits (1 audit re-check + 3–5 SERP queries + 1 domain overview).
- Firecrawl: optional with WebFetch fallback, ~6–9 Firecrawl credits if available (hard cap 12). When available, steps 4 (GBP-on-page audit), 5 (NAP extraction), and 7 (review scraping) run on the homepage + 5 sample pages + provided review URLs. Without Firecrawl those steps degrade to WebFetch-only — schema/JSON-LD detection and
tel:/ address element extraction become best-effort prose inspection. Pass--no-firecrawlto force WebFetch-only. - Google APIs: tier 1 (GSC) unlocks step 8b (GSC local query performance) after the local-pack rank step; tier 2 (GA4) additionally unlocks step 8c (GA4 organic-by-landing-page enrichment). See
skills/seo-google/references/cross-skill-integration.mdfor the full enrichment contract.
- Normalise the target (strip protocol from domain; confirm homepage is fetchable). Confirm at least one local keyword was provided — if none, infer from
-
Business-type detection
- Read homepage +
/contact+ footer prose (WebFetch markdown is enough for this). - Classify as one of:
- Brick-and-Mortar — visible street address, "Visit us at", embedded Maps iframe.
- Service Area Business (SAB) — no street address, "serving {region}", "we come to you",
areaServedin schema withoutaddress.streetAddress. - Hybrid — both signals present (e.g. showroom + service area).
- This determines which checks apply downstream. SAB skips embedded-map and physical-address consistency. Record in
LOCAL-SEO-REPORT.md"Snapshot".
- Read homepage +
-
Industry-vertical detection
- From URL patterns (
/menu,/practice-areas,/listings,/inventory),<title>, page prose, infer one of: Restaurant / Healthcare / Legal / Home Services / Real Estate / Automotive / Generic. - This routes citation-source recommendations and schema-subtype recommendations later — load
references/local-citation-sources.mdfor the vertical's Tier-1 directories.
- From URL patterns (
-
GBP signals on the page
mcp__firecrawl-mcp__firecrawl_scrape(withformats: ["rawHtml"])- Scrape homepage +
/contact(or whichever page has the most local intent). - From
rawHtmlextract:- Embedded Google Maps iframe (
<iframe src="https://www.google.com/maps/embed?...">) — record place ID if present. - Reviews widget / GBP rich snippet markup.
aggregateRatingJSON-LD block (presence is the strongest signal that the site wants stars in SERPs).- Business hours visibility on page (open-at-search-time correlates with rank — Whitespark's #5 factor).
- Click-to-call: count of
<a href="tel:...">elements. - GBP profile link: any
<a href>tohttps://g.page/...orhttps://maps.app.goo.gl/...orhttps://www.google.com/maps/place/....
- Embedded Google Maps iframe (
- If Firecrawl unavailable: WebFetch markdown can detect a
tel:link in some renderings but loses iframes and JSON-LD. Mark Maps embed / aggregateRating / GBP profile-link detection as(skipped — Firecrawl required).
- Scrape homepage +
-
NAP consistency
mcp__firecrawl-mcp__firecrawl_scrapeon homepage + 5 sample pages- Sample pages: homepage,
/contact,/about, plus 2 service or location pages (pick from sitemap or top traffic pages). - For each, extract:
- Visible NAP from rendered prose. Address pattern (street + city + region + postal), phone (
tel:href + display format), business name (logo alt, footer, schemaname). - NAP from JSON-LD. Parse every
<script type="application/ld+json">block. Pullname,address.streetAddress,address.addressLocality,address.addressRegion,address.postalCode,telephone.
- Visible NAP from rendered prose. Address pattern (street + city + region + postal), phone (
- Compare across the 6 page samples + schema. Any divergence (different phone format on the contact page vs homepage; "Suite 200" missing from one footer; schema phone in international format while page shows local format) → record in
nap-inconsistencies.csv. - Brick-and-mortar only: if a Maps iframe is present, attempt to read the embedded address from the iframe URL (the place ID and address are URL-encoded). Compare to page/schema NAP. SAB skips this.
- If
nap-inconsistencies.csvis empty after the scan, writenap-inconsistencies.csvas a one-line file with header only and note "NAP consistent across {n} pages and schema" inLOCAL-SEO-REPORT.md.
- Sample pages: homepage,
-
Local-pack rank tracking
DATA_getSerpResultswith country/region filters- For each user-provided local keyword (or the 1–3 inferred from homepage):
- Call
DATA_getSerpResultswith the user's country and the most specific region/city the API supports (useDATA_getSerpLocationsfirst to confirm a valid location code if the user supplied a city). - Capture: top 10 organic, local-pack presence (yes/no), the 3 businesses in the local pack if shown (name, rating, review count), AIO presence.
- Cross-check: is the target domain in the top 10 organic? Is the target business name in the local pack?
- Call
- Save the parsed result per keyword to
local-keywords.csv(columns:keyword,country,location,local_pack_present,target_in_pack,target_pack_position,target_organic_position,top_pack_competitor_1,top_pack_competitor_2,top_pack_competitor_3). - Note the local-pack-ads caveat: the SE Ranking SERP returns the AI/ads-modified pack as Google serves it. If the local pack shows ads, record that — local-pack ad density jumped from 1% to 22% of mobile US local searches in 2025–2026 per Sterling Sky.
- For each user-provided local keyword (or the 1–3 inferred from homepage):
-
Reviews scraping
mcp__firecrawl-mcp__firecrawl_scrapeon user-provided review URLs- Inputs (user-provided, optional). GBP listing URL (
https://www.google.com/maps/place/...), Yelp business URL, Trustpilot business URL, BBB profile URL. - For each provided URL: scrape with
formats: ["rawHtml"]. From the parsed DOM, extract: total review count, average rating, date of most recent review (review velocity proxy), count of owner responses on the most recent 10 reviews. - Aggregate signals:
- Velocity: ≥1 new review in last 18 days = healthy (Sterling Sky 18-day rule). >21 days since last = "review cliff" risk.
- Volume: <10 Google reviews flags below the magic threshold.
- Star rating: 4.5+ matches c
- Inputs (user-provided, optional). GBP listing URL (