Local-Business AEO / GEO Schema — Agent Skill
Make a marketing site rank in local search AND get cited by answer engines (Google AI Overviews, ChatGPT, Perplexity, Claude) by hand-authoring a schema.org JSON-LD @graph and by making the site crawlable to AI-citation bots. This is for sites that need richer structured data than a CMS auto-generates — a customized everjust.app/Odoo tenant, or any page where you emit your own <script type="application/ld+json">.
This skill is the schema design + AEO strategy. Two neighbors own the mechanics: [[everjust-website-infra-views]] SHIPS a sitewide JSON-LD view over XML-RPC and edits robots.txt; [[everjust-website-seo]] owns per-page meta title/description and the platform's AUTO Organization/WebSite blocks. Note the contrast with everjust-website-seo: it says 'do not author your own JSON-LD, feed the auto blocks.' That holds for a vanilla tenant. This skill applies to a hand-authored-schema site where you deliberately inject a graph — a different, more customized surface. Do not run both stances on the same node (see Pitfalls).
When to use this skill
- A local-business marketing site needs LocalBusiness / Service / FAQPage / Review / BreadcrumbList structured data.
- Other pages already emit
provider: {"@id":"#business"}refs that resolve to nothing — you need the canonical@id='#business'node. - You want the page's visible FAQ mirrored into
FAQPageschema (Google requires the answer be visible on the page). - You need to add star ratings / reviews to rich results — and must place
aggregateRatingcorrectly to stay eligible. - The site is up but not appearing in Google or AI answers — you suspect a bot challenge.
- Someone wants to 'add llms.txt for AI SEO' and you need the honest cost/benefit.
Do NOT use this skill for:
- Per-page meta title/description/OG or the platform AUTO Org/WebSite JSON-LD → [[everjust-website-seo]].
- The mechanics of writing the infra view / robots.txt on an everjust tenant → [[everjust-website-infra-views]].
- Page body content → [[everjust-website]].
The schema model — the @graph map
| Node | Where it lives | Key point |
|---|---|---|
LocalBusiness @id='#business' |
ONE sitewide node, injected into <head> on every page |
Canonical business identity: name, address, geo, telephone, url, openingHours, sameAs. Other pages reference it by @id — this node RESOLVES those refs. |
Service |
Per service page | provider: {"@id":"#business"} links back to the sitewide node. This is where aggregateRating/review go (see Dec-2025 rule). |
FAQPage |
Per page that shows a visible FAQ | mainEntity[] of Question→acceptedAnswer, built from the page's VISIBLE Q&A. |
BreadcrumbList |
Per deep page | itemListElement[] mirroring the visible breadcrumb trail. |
Review / AggregateRating |
ON the Service node (NOT LocalBusiness/Organization) |
Only REAL reviews (author name + verbatim body). |
Core rule 1 — the sitewide @id='#business' node resolves cross-page refs
Per-page Service graphs commonly emit "provider": {"@id": "#business"} — a REFERENCE, not a definition. If nothing on the page defines @id: "#business", the ref dangles and validators warn. Fix once: inject a single sitewide LocalBusiness node with "@id": "#business" into <head> on every page (ship it via [[everjust-website-infra-views]] Recipe 4). Every page's Service provider ref now resolves to it.
{ "@context": "https://schema.org", "@type": "LocalBusiness", "@id": "#business",
"name": "...", "url": "https://connectdomain.app", "telephone": "+1-612-...",
"address": {"@type": "PostalAddress", "streetAddress": "...", "addressLocality": "...", "addressRegion": "MN", "postalCode": "..."},
"geo": {"@type": "GeoCoordinates", "latitude": "...", "longitude": "..."},
"openingHoursSpecification": [ ... ], "sameAs": [ ... ] }
Core rule 2 — FAQPage MUST be built from VISIBLE content, deterministically
Google requires that FAQ schema answers are visible on the page. So do NOT invent Q&A for schema — EXTRACT the page's rendered FAQ and mirror it. Handle BOTH common markups (accordion and h3/p) with regex so schema tracks the DOM exactly:
import re, html
# accordion: <button ...>Q</button> ... <div ...>A</div> | or h3/p: <h3>Q</h3><p>A</p>
pairs = []
for q, a in re.findall(r'<h3[^>]*>(.*?)</h3>\s*<p[^>]*>(.*?)</p>', page_html, re.S):
pairs.append((html.unescape(re.sub('<[^>]+>','',q)).strip(),
html.unescape(re.sub('<[^>]+>','',a)).strip()))
# also parse the accordion markup variant the same way and merge
faq = {"@context":"https://schema.org","@type":"FAQPage",
"mainEntity":[{"@type":"Question","name":q,
"acceptedAnswer":{"@type":"Answer","text":a}} for q,a in pairs]}
If a Q&A is not visible on the page, it does not go in the schema. Rebuild the schema whenever the visible FAQ changes so they never drift.
Core rule 3 — aggregateRating/review go on the SERVICE node (Google Dec-2025)
Do not put aggregateRating or review on the LocalBusiness/Organization node. Per Google's December 2025 policy, self-serving aggregateRating on the business entity is ineligible for rich results and risks a manual action. Star ratings belong on a Service (or Product) node the business provides:
{ "@type": "Service", "name": "...", "provider": {"@id": "#business"},
"aggregateRating": {"@type": "AggregateRating", "ratingValue": "4.9", "reviewCount": "37"},
"review": [ {"@type":"Review", "author":{"@type":"Person","name":"<REAL name>"},
"reviewRating":{"@type":"Rating","ratingValue":"5"},
"reviewBody":"<verbatim review text>"} ] }
Use only REAL reviews — a real author and the verbatim body. Fabricated or paraphrased reviews are a policy violation (and an honesty problem — see [[marketing-site-authenticity-audit]]).
Core rule 4 — allow the AI-citation crawlers (robots.txt)
Answer engines cite pages their bots can fetch. Add an explicit allow-list (ship via [[everjust-website-infra-views]] Recipe 5 → website.robots_txt):
User-agent: GPTBot
Allow: /
User-agent: OAI-SearchBot
Allow: /
User-agent: ChatGPT-User
Allow: /
User-agent: ClaudeBot
Allow: /
User-agent: Claude-SearchBot
Allow: /
User-agent: PerplexityBot
Allow: /
User-agent: Google-Extended
Allow: /
Core rule 5 — diagnose bot-challenge INVISIBILITY
A site can be fully up for humans yet invisible to search + AI because a WAF / edge bot-challenge (e.g. Vercel Attack Challenge Mode, Cloudflare challenge) returns 429/403 to bot user-agents. Symptom: site: shows nothing indexed and AI engines don't cite it, though the browser loads fine. Diagnose by fetching as each bot UA and running the index check:
for ua in "Googlebot" "GPTBot" "PerplexityBot"; do
echo "== $ua =="; curl -s -o /dev/null -w '%{http_code}\n' -A "$ua" https://<site>/
done
# 429/403 for bot UAs but 200 in a browser → a bot challenge is hiding you
Also check site:<domain> coverage. Remedy: allow-list verified crawler UAs / turn the challenge off for them at the edge. No amount of schema helps if the crawler gets a 429.
Core rule 6 — the llms.txt reality-check
When asked to 'add llms.txt for AI SEO': it is low value for answer-engine ranking. Research shows ~97% of llms.txt files get zero requests, and Google has said it does not use llms.txt. Don't spend effort there. The real levers are, in order: crawlability (no bot challenge; robots allows the AI bots), structured data (the @graph above), real reviews on the Service node, and answer-first content (lead with the direct answer, then support it). Shipping llms.txt is harmless but not a substitute for these.
Recipes
- Audit — fetch each page, list what schema is present, find dangling
@idrefs, note which FAQs are visible, checksite:coverage, and curl as bot UAs (Core rule 5). - Ship the sitewide
#businessnode into<head>(Core rule 1) via [[everjust-website-infra-views]] Recipe 4. - Per page: add
Service(with real reviews/rating per Core rule 3),FAQPagebuilt from visible Q&A (Core rule 2), andBreadcrumbListmirroring the visible trail. - robots.txt — allow the AI crawlers (Core rule 4) via [[everjust-website-infra-views]] Recipe 5.
- Validate — Google Rich Results Test / Schema Markup Validator; confirm no dangling refs, no aggregateRating on the business node, FAQ answers all visible.
Pitfalls
- aggregateRating/review on LocalBusiness or Organization. Ineligible + manual-action risk per Google Dec-2025. Put them on the
Servicenode withprovider: {@id:#business}. - FAQ schema that isn't on the page. Google requires the answer be visible. Build FAQPage by EXTRACTING the rendered Q&A (Core rule 2); never author FAQ schema by hand from imagination.
- Dangling
@idrefs. Pages reference#businessbut nothing defines it. Ship the ONE sitewide node. - Fake or paraphrased reviews. Only real author + verbatim body. Fabrication is a policy violation and a marketing-honesty problem ([[marketing-site-authenticity-audit]]).
- Running the auto-JSON-LD stance and the hand-authored stance on the same node. everjust-website-seo says feed the auto Org/WebSite blocks and NOT inject your own; this skill injects a graph. Two competing Organization blocks confuse validators. Keep the AUTO Org/WebSite (via [[everjust-website-seo]]) and add the LocalBusiness/Service/FAQ graph — don't emit a SECOND Organization.
- Shipping llms.txt instead of fixing crawlability/schema. ~97% zero requests; Google doesn't use it. Fix the real levers first.
- Publishing schema on a site behind a bot challenge. The crawler gets 429 and never sees your markup. Diagnose (Core rule 5) before blaming the schema.
See also
- [[everjust-website-infra-views]] — SHIPS the sitewide JSON-LD view + robots.txt over XML-RPC (no SSH).
- [[everjust-website-seo]] — per-page meta + AUTO Org/WebSite blocks.
- [[marketing-site-authenticity-audit]] — ensure the reviews you schema-ify are real.