What changed
Classic SEO optimised for a ranked list of ten links. AI answer engines — ChatGPT search, Perplexity, Copilot, Google's AI overviews, Claude's web search — synthesise a single answer from a few retrieved sources and cite some of them. That shifts the target from rank higher to be the passage a model can lift verbatim and attribute confidently.
The practices go by two names: GEO (generative engine optimization) and AEO (answer engine optimization). They overlap almost entirely, and neither requires abandoning normal SEO — a page that is crawlable, fast, and structured still wins both. What changes is the emphasis: unambiguous entities, self-contained passages, and machine-readable facts.
Entity consolidation: one @id, referenced everywhere
The single highest-leverage technical move is making a retrieval system certain who the site is about. If ten pages each declare a separate Person node, a model sees ten weakly-connected entities. If ten pages reference one canonical @id, it sees one entity with ten pieces of evidence.
// on the homepage only — the canonical definition
{
"@context": "https://schema.org",
"@type": "Person",
"@id": "https://example.com/#person",
"name": "Full Name",
"url": "https://example.com/",
"jobTitle": "...",
"knowsAbout": ["...", "..."],
"sameAs": [
"https://github.com/handle",
"https://www.linkedin.com/in/handle",
"https://orcid.org/0000-..."
]
}
// on every other page — reference, never redefine
"author": {"@id": "https://example.com/#person"},
"publisher": {"@id": "https://example.com/#person"}
- Define once, reference everywhere. Re-stating the full Person object on every page invites contradictions, and contradictions are what make a model hedge instead of cite.
sameAs is the disambiguation lever. Link the profiles that already have independent authority — GitHub, LinkedIn, ORCID, a publisher DOI page. This is how an entity gets tied to an existing graph rather than floating alone.
- Keep names byte-identical across schema, page text, and external profiles. 'J. Smith' in one place and 'John Smith' in another is two entities as far as a matcher is concerned.
- Use
knowsAbout deliberately. It is a direct statement of topical scope, and it is one of the few places you get to declare it rather than have it inferred.
- Validate every deploy. A JSON-LD syntax error means the block is skipped entirely and silently.
A small entity graph, worked
One entity rarely stands alone. A realistic graph links the person, the site, and each piece of content with @id references, so a retrieval system can walk from an article to its author to the rest of their work in a single hop instead of guessing at the connection.
{
"@context": "https://schema.org",
"@graph": [
{
"@type": "Person",
"@id": "https://example.com/#person",
"name": "Full Name",
"url": "https://example.com/",
"sameAs": ["https://github.com/handle", "https://www.linkedin.com/in/handle"]
},
{
"@type": "WebSite",
"@id": "https://example.com/#website",
"url": "https://example.com/",
"name": "Site Name",
"publisher": {"@id": "https://example.com/#person"}
},
{
"@type": "TechArticle",
"@id": "https://example.com/guides/example.html#article",
"headline": "Example Guide",
"url": "https://example.com/guides/example.html",
"author": {"@id": "https://example.com/#person"},
"publisher": {"@id": "https://example.com/#person"},
"isPartOf": {"@id": "https://example.com/#website"}
},
{
"@type": "BreadcrumbList",
"@id": "https://example.com/guides/example.html#breadcrumb",
"itemListElement": [
{"@type": "ListItem", "position": 1, "name": "Guides", "item": "https://example.com/guides.html"},
{"@type": "ListItem", "position": 2, "name": "Example Guide"}
]
}
]
}
Four @id values, four cross-references, zero duplicated definitions. Every subsequent article on the site adds one more TechArticle node pointing at the same two @id values — the graph gets denser with every page published instead of wider with every page repeating itself.
Which schema type for which page
Not every page needs the same markup, and forcing one type onto a page it does not describe is as unhelpful to a model as having none.
| Page type | Primary schema type | Notes |
| Homepage or about page | Person or Organization | The canonical node, defined once, referenced from every other page's author/publisher |
| Guide or long-form article | TechArticle or Article | author and publisher point at the canonical @id, never a redefinition |
| FAQ section on a guide | FAQPage | Must mirror the visible text exactly — the rule this guide keeps returning to |
| Product or store listing | Product, with an Offer | Price and availability only if genuinely accurate; do not mark up placeholders |
| App page | SoftwareApplication | applicationCategory and operatingSystem where they are actually true |
| Any page below the root | BreadcrumbList | Cheap to add and a directly supported rich result on its own |
| Site-wide | WebSite | Ties every page's schema back to the same publisher @id |
JSON-LD is, in effect, structured output aimed at a machine reader instead of a human one — the same discipline of naming fields precisely and never improvising a shape covered generally in the structured LLM output guide.
llms.txt and llms-full.txt
llms.txt is a proposed convention: a markdown file at the site root that gives a language model a clean, navigable summary of the site without HTML, navigation, or scripts. llms-full.txt is the expanded version containing the actual content.
# Site Name
> One-sentence description of who this is and what the site covers.
## Guides
- [Offline PWA](https://example.com/guides/offline-pwa.html): building an
installable offline app with no backend.
- [RAG explained](https://example.com/guides/rag-explained.html): chunking,
embeddings, reranking, and where pipelines fail.
## Projects
- [Project name](https://example.com/project.html): what it does, in one line.
## Contact
- email@example.com
Two honest caveats. First, adoption is not universal — no major crawler has committed to it as a requirement, so treat it as cheap insurance rather than a guaranteed channel. Second, it is not a place to say something different from the site. Contradiction between llms.txt and the rendered pages is worse than having no file, because it introduces exactly the ambiguity you are trying to eliminate.
The reason to ship it anyway: it costs an hour, it is trivially machine-parseable, and it forces you to write a clean one-line description of every page — which is useful regardless of who reads it.
robots.txt and the named AI crawlers
AI companies operate separately-named agents, often splitting training crawlers from live-retrieval crawlers. Those are different decisions: blocking training while allowing retrieval is a coherent position, and so is the reverse.
| Agent | Operator | Primary purpose |
| GPTBot | OpenAI | Training data collection |
| OAI-SearchBot | OpenAI | Live search retrieval and citation |
| ChatGPT-User | OpenAI | Fetches a page a user asked about |
| ClaudeBot | Anthropic | Crawling for model training |
| Claude-User / Claude-SearchBot | Anthropic | User-initiated fetch and search indexing |
| PerplexityBot | Perplexity | Search index used for cited answers |
| Google-Extended | Google | Controls Gemini training use; does not affect Search ranking |
| Applebot-Extended | Apple | Controls Apple Intelligence training use |
| Bingbot | Microsoft | Bing index, which also feeds Copilot |
| CCBot | Common Crawl | Open crawl corpus used by many trainers |
User-agent: GPTBot
Allow: /
User-agent: OAI-SearchBot
Allow: /
User-agent: ClaudeBot
Allow: /
User-agent: PerplexityBot
Allow: /
User-agent: Google-Extended
Allow: /
User-agent: Applebot-Extended
Allow: /
Sitemap: https://example.com/sitemap.xml
Blocking Google-Extended does not remove you from Google Search, and blocking a training crawler does not remove you from that company's live search results — the agents are separate. Equally, if you want to be cited, blocking the retrieval bots guarantees you will not be. Decide training and retrieval independently, and know which name controls which.
robots.txt is a request, not enforcement. Crawlers that ignore it will ignore it. If content genuinely must not be read, it needs authentication, not a directive.
A crawler-policy decision matrix
Training and retrieval are two independent decisions, and stating both explicitly beats leaving either to a bot's default behaviour.
| Retrieve: allow | Retrieve: block |
| Train: allow | Maximum reach. Allow GPTBot, ClaudeBot, Google-Extended, Applebot-Extended, and CCBot alongside OAI-SearchBot, Claude-SearchBot, PerplexityBot, and Bingbot. No control over how the content is used later. | Rare and usually accidental. Content feeds training runs but nothing user-initiated fetches it live, which is an odd split since the same company typically operates both agents. |
| Train: block | The common, defensible middle position. Block GPTBot, ClaudeBot, Google-Extended, Applebot-Extended, and CCBot; allow OAI-SearchBot, Claude-SearchBot, PerplexityBot, and Bingbot. Keeps content out of future model weights while staying citable today. | Full opt-out. Block every named agent. The content will not be cited by any of them and will not train anything — the cost is accepting zero AI-search-referred traffic. |
The static-hosting case for staying retrievable while limiting training is made at more length in the static-site AI search guide, which covers the same crawler list from the hosting side rather than the schema side.
FAQPage schema — and the rule that breaks sites
FAQPage markup is well-suited to answer engines: it is a list of question-answer pairs in exactly the shape a retrieval system wants. There is one hard rule.
Every question and answer in FAQPage markup must appear as visible text on the same page. Schema-only FAQ content — questions that exist in the JSON-LD but not on screen — violates Google's structured data guidelines and is a well-known cause of manual actions. Generate the visible FAQ and the schema from the same source so they cannot drift.
- Write answers that stand alone. 'It depends on the above' is unquotable; a model lifting that sentence out of context produces nothing useful.
- Two to four sentences per answer. Long enough to be complete, short enough to be lifted whole.
- Answer in the first sentence, then qualify. Never build to the answer.
- Use the question form a person would actually type or say, not a keyword-stuffed variant.
- Do not mark up promotional copy as an FAQ. It is the most common misuse and it does not survive review.
Answer-first writing and question-shaped headings
Retrieval works on passages, not pages. A model chunks your page, embeds the chunks, and pulls the ones that match the query. That has direct consequences for structure.
- Make headings questions where it is natural — 'How do you detect scene changes?' rather than 'Detection'. The heading is often the strongest signal for what the chunk beneath it answers.
- Answer in the first sentence under the heading. A paragraph that spends three sentences setting up context gets chunked away from its own conclusion.
- Make each section self-contained. Assume the reader has only that chunk, because the model might.
- Use tables and lists for comparisons. They survive chunking better than prose and are easier to extract cleanly.
- Put specifics in the text. Numbers, versions, exact command names, and dates are what makes a passage worth citing over a generic competitor.
- State facts plainly and once. Hedged, repetitive prose gives a model nothing crisp to quote.
The underlying test is simple: if someone pasted one section of your page into a chat with no other context, would it answer the question? If not, that section will not get cited.
Chunking, worked: a bad section versus a good one
Retrieval systems generally split pages into passages by heading boundary or by a fixed token window, embed each passage, and rank passages against the query — the mechanics are covered fully in the RAG explained guide and the embedding model guide. What that means for writing is concrete enough to show side by side.
BAD — the answer is buried past the likely chunk boundary:
"## Detection
There are many ways to think about how a system might notice change
in a video, and the right approach depends a lot on the footage, but
generally speaking one method that has proven useful over time involves
comparing consecutive frames using some kind of scoring function, which
is roughly what happens here, tuned per source."
GOOD — the answer is the first sentence, the heading is the query:
"## How do you detect scene changes with ffmpeg?
Threshold the select filter's per-frame scene score on a 0 to 1 scale:
select='gt(scene,0.4)'. 0.3 is sensitive and fires on camera movement;
0.5 or above catches hard cuts only."
A fixed-size chunker slicing the bad version at roughly 40 words is likely to split before the method is ever named, producing a retrievable passage that says footage varies and approaches must be tuned — true, and useless to quote. The good version fits inside almost any chunk size whole, names the method in the first sentence, and answers the literal query in the heading before the passage is even embedded. Nothing about the underlying facts changed between the two versions — only the order they are said in.
IndexNow for fast recrawl
IndexNow is a protocol supported by Bing, Yandex, Seznam and others that lets you push URLs the moment they change instead of waiting for a crawl. That matters for AI search because Copilot is built on the Bing index — faster Bing recrawl means faster Copilot freshness.
# 1. host a key file at the site root
# https://example.com/<key>.txt containing exactly <key>
# 2. ping on publish
curl "https://api.indexnow.org/indexnow?url=https://example.com/guides/new.html&key=<key>"
# or submit a batch
curl -X POST https://api.indexnow.org/indexnow \
-H "Content-Type: application/json" -d '{
"host": "example.com",
"key": "<key>",
"keyList": ["https://example.com/guides/new.html"]
}'
Google does not participate in IndexNow; for Google, keep the sitemap current and submit it in Search Console. Do not spam either — pinging unchanged URLs repeatedly is the fastest way to have the signal ignored.
How to actually tell whether you are being cited
This is the honest weak point of the whole practice: there is no unified citations dashboard across providers, so measurement is assembled from partial signals rather than read off one report.
- Ask directly. Query ChatGPT search, Perplexity, and Claude's web search with questions your pages actually answer, and check whether your domain appears in the cited sources. Crude, but it is the most direct signal available.
- Watch server logs for the named retrieval agents, not the training ones — OAI-SearchBot, Claude-SearchBot, PerplexityBot, Bingbot. A hit proves the page was fetched for a live query, though not that it was ultimately quoted.
- Check referrer strings in analytics, though treat this as a lower bound. Many AI-answer clickthroughs arrive with no referrer at all, so an undercount here does not mean zero citations.
- Watch for a traffic pattern with no obvious search-ranking cause — a spike in direct or referrer-less traffic to one specific page is a common fingerprint of an answer engine sending readers to the source.
- Use Bing Webmaster Tools alongside Google Search Console. Copilot runs on the Bing index, so Bing-side indexing status is a real proxy signal that Search Console alone will not show.
None of this is precise, and providers change what they surface without notice. Treat citation tracking the way you would treat any early-stage measurement problem: directionally useful, not a number to optimise to the decimal.
A pre-publish validation checklist
The site-wide checklist below is the architecture. This one is what to check on the specific page in front of you before it ships.
- Run the page's JSON-LD through a structured-data validator. One syntax error silently discards the entire block, with no visible symptom on the page itself.
- Confirm every FAQPage question and answer appears verbatim as visible text on the same page — not paraphrased, not reordered.
- Confirm
author and publisher resolve to the canonical @id, not a redefinition of the Person object.
- Read each section as if it were the only text retrieved. If it needs the section before it to make sense, it will not survive chunking.
- Confirm the page is in
sitemap.xml, and that llms-full.txt, if it references this page, says nothing that contradicts it.
- Confirm
robots.txt has not accidentally blocked a crawler this specific page is meant to reach.
- Once genuinely live, ping IndexNow, or accept it will be picked up on the next scheduled crawl.
A checklist that fits on one screen
- One canonical Person or Organization node with a stable
@id and a full sameAs list, defined once and referenced from every page.
- Article or TechArticle schema on every content page, with
author and publisher pointing at that @id.
- BreadcrumbList on every page below the root.
- FAQPage that mirrors visible on-page text exactly, generated from the same source.
llms.txt and llms-full.txt at the root, consistent with the site.
robots.txt with an explicit, deliberate decision for each named AI agent, plus a sitemap line.
- Question-shaped headings, answer-first paragraphs, self-contained sections.
- A current
sitemap.xml listing every real page, submitted to Search Console.
- IndexNow ping wired into the publish step.
- Validate JSON-LD on every deploy — one syntax error silently discards the whole block.
This site implements the full list, which is the only reason I can describe it concretely: the canonical Person node lives on the homepage and every page references it, /llms.txt and /llms-full.txt are generated alongside the pages, /ai.txt and robots.txt state the crawler policy explicitly, and every guide — including this one — carries TechArticle plus BreadcrumbList plus an FAQPage generated from the same Python list as its visible FAQ section below, so the two cannot drift because they are, literally, the same data rendered twice. The entity page is here →
Tools referenced in this guide
- About — the canonical entity page every schema node on this site points at.
- RAG explained — how retrieval actually chunks and ranks your pages — useful context for all of this.
- My stack — the generator and tooling behind the structured data here.
- Apps hub — an example of the same schema pattern applied to product pages.