What is GEO, minus the folklore?
GEO is not a ranking trick. It is the discipline of making your entities and claims unambiguous to a system that answers with synthesis instead of links.
Classic SEO optimises for a ranked list of documents. A generative engine does something structurally different: it retrieves passages, reranks them against a query, composes an answer from the strongest fragments, and may cite a few sources. Three consequences follow, and they are engineering consequences rather than marketing ones.
Your page is no longer the unit. Passages are. A page that answers one specific question in one specific section survives extraction; a page that circles a topic for four thousand words gets summarised without attribution.
Ambiguity is now a cost. If your site refers to "Will" in one place, "the studio" in another and a legal entity name in a third, a retrieval system has to guess whether those are one entity or three. Guesses are cheap to avoid.
Attribution depends on machine-readable structure, not on how good the prose is. The same paragraph can be quoted accurately with a valid author graph, or paraphrased anonymously without one.
How do you make the entity graph explicit with JSON-LD?
Every distinct thing your site is about — you, the site, each article, each product, each service — should have one stable @id, and every reference to it elsewhere should point at that @id rather than restating the data. One @graph block per page, server-rendered, valid JSON.
{
"@context": "https://schema.org",
"@graph": [
{
"@type": "Person",
"@id": "https://willchan.me/#person",
"name": "Will Chan",
"jobTitle": "Design Engineer & AI Systems Architect",
"url": "https://willchan.me/",
"address": { "@type": "PostalAddress", "addressLocality": "Shenzhen", "addressCountry": "CN" },
"knowsAbout": ["AI workflows", "Model Context Protocol", "design engineering"],
"sameAs": ["https://github.com/willchan", "https://www.linkedin.com/in/willchan"]
},
{
"@type": "WebSite",
"@id": "https://willchan.me/#website",
"url": "https://willchan.me/",
"name": "Will Chan",
"inLanguage": "en",
"publisher": { "@id": "https://willchan.me/#person" }
},
{
"@type": "BlogPosting",
"@id": "https://willchan.me/blog/geo-for-engineers#article",
"headline": "GEO for Engineers: Making Your Site Legible to Language Models",
"description": "A practical engineering guide to generative engine optimization.",
"datePublished": "2026-03-21",
"inLanguage": "en",
"author": { "@id": "https://willchan.me/#person" },
"isPartOf": { "@id": "https://willchan.me/#website" },
"mainEntityOfPage": { "@id": "https://willchan.me/blog/geo-for-engineers#webpage" }
}
]
}
Three details separate a useful graph from decoration. The author is a reference, not a repeated object, so a parser resolves it once and gets the same facts on every page. The sameAs array is the only bridge between your site and external identity graphs — it is how a model decides that the "Will Chan" writing about MCP is the same person as the one with the GitHub account, and the page where I declare that entity is its human-facing counterpart. And the datePublished value must match the date in your content frontmatter, because a contradiction between markup and visible text is a reason to trust neither.
Semantic HTML is your retrieval surface
Extraction pipelines work on the DOM after rendering. Anything you make hard to parse is simply not retrieved.
Write headings as the questions your readers actually type, and put one topic under each. Use <table> for comparisons — a genuine two-column tradeoff table is one of the highest-value extractable structures on a technical site, because it survives reranking intact. Write the definitional sentence for each concept once, plainly, in its own short paragraph. Mark dates with <time datetime>. Put terminology lists in <dl>. Never bake text into an image, since it does not exist for extraction.
Two implementation rules follow for JavaScript-heavy sites. Serve substantive content in the initial HTML response rather than after hydration, so a crawler that does not execute scripts still sees the text. And keep a static HTML route for anything you want cited, even if the interactive experience is richer.
One entity, one canonical URL
Assistants cite URLs they can resolve later. Churning slugs breaks that: rename a published post and the citation points at a redirect, if it survives at all.
Decide the URL pattern once — I use /blog/<slug> with a slug that describes the question the post answers — and treat published slugs as immutable. If a rename is unavoidable, keep a permanent redirect and update the @id values in the same commit, because a stable @id that points at a 404 is worse than a new one.
For bilingual content, declare the relationship explicitly with hreflang pairs (en, zh-Hans, and x-default) plus a self-referencing canonical on each version. Two pages that are near-duplicates in different languages, with no declared relationship, are the single most common way a bilingual site splits its own citation signal.
Machine-readable text endpoints
HTML is for humans and costs tokens to parse. Offer the same content in a form that extraction tools can consume directly — the principle behind the structured tool boundary in a zero-marginal-cost AI workflow.
# /public/robots.txt
User-agent: *
Allow: /
Sitemap: https://willchan.me/sitemap.xml
# Answer-engine and AI crawlers, named explicitly
User-agent: GPTBot
Allow: /
User-agent: OAI-SearchBot
Allow: /
User-agent: ClaudeBot
Allow: /
User-agent: PerplexityBot
Allow: /
Add a Markdown alternate for every article — the same source file served at /blog/<slug>.md with content-type: text/markdown, linked with rel="alternate" type="text/markdown". Publish a JSON feed alongside RSS. Keep lastmod in the sitemap accurate, because a stale timestamp teaches a crawler to deprioritise your whole domain.
| Surface | What it carries | Realistically consumed by | Evidence |
|---|---|---|---|
JSON-LD @graph | Entity identity, authorship, dates | Google, Bing, some assistants | Strong, documented |
| Semantic HTML | Extractable passages, tables, lists | Every retrieval pipeline | Strong |
/blog/<slug>.md alternate | Clean text, no markup overhead | Developer-facing tools, some agents | Moderate, improving |
llms.txt | Curated index of key pages | Almost nobody, so far | Weak — a proposal, not a standard |
Sitemap with lastmod | Change signals | Crawlers on discovery passes | Strong |
The llms.txt row deserves honesty. It is a reasonable idea, it costs an hour, and I publish one. It is not a standard, no major provider has committed to reading it, and anyone presenting it as a ranking lever is selling something unverifiable.
How do you verify that answer engines can actually reach you?
Check reachability from the crawler's side, not yours. A bot that gets a 403 from your WAF or a page whose body arrives after hydration is invisible, and your analytics will show nothing at all.
# Does the crawler get a 200, and does the HTML already contain the text?
curl -sS -o /dev/null -w '%{http_code}\n' \
-A 'Mozilla/5.0 (compatible; PerplexityBot/1.0; +https://perplexity.ai/perplexitybot)' \
https://willchan.me/blog/geo-for-engineers
curl -sS https://willchan.me/blog/geo-for-engineers \
| grep -c 'entity graph' # expect more than 0 in server-rendered HTML
# Is the Markdown alternate real text and not an HTML error page?
curl -sSI https://willchan.me/blog/geo-for-engineers.md | grep -i '^content-type'
Then read your server logs monthly and count hits by user agent. That number, plus a fixed set of twenty questions you ask four assistants every quarter, is the only measurement I trust. Referrer data is too stripped to carry the analysis.
Why thin traffic-bait content loses twice
Retrieval systems embed and deduplicate passages. Ten short posts that paraphrase each other compress into one cluster and compete for the same slot, which means nine of them earn nothing. Worse, each one dilutes the entity signal: a site that appears to be about everything is a site a model has no reason to cite for anything.
The counter-strategy is not "longer". It is specificity that only one page can match: a named tradeoff, a measured number, a table of failure modes, a definition phrased once and clearly. That kind of page is also the kind that gets quoted verbatim, which is the outcome worth optimising for.
What is genuinely unproven about GEO
No provider publishes how its answer engine selects citations, and none of the tactics above is a documented ranking factor except classic structured data for classic search. Citation measurement is noisy because assistant traffic frequently arrives as direct or with referrers stripped. robots.txt compliance by AI crawlers is a stated policy, not an enforced one, and there are documented cases of directives being ignored. Anyone quoting a percentage lift from GEO is extrapolating from a sample you cannot inspect.
What is proven is smaller and still worth acting on: pages that are server-rendered, semantically structured, unambiguous about authorship and stable at their URLs get retrieved and quoted more often than pages that are not. That is an engineering claim, and it is testable in your own logs.
Closing: instrument the retrieval layer like any other service
Treat answer engines as a traffic source with a broken attribution model, and instrument accordingly: server logs by user agent, a valid @graph on every page, Markdown alternates, and a quarterly prompt set that you actually re-run and record. Then leave it alone for ninety days. If the citations do not move, you have lost an hour a week and gained a cleaner site; if they do move, you will know precisely which change caused it, which is more than most GEO advice can offer. If the citations matter to your revenue, instrumenting them is part of the client engagements I take on.
Keep reading
- Server Actions vs API Routes: The Caller Decides, Not the Operation2026-02-198 minEngineering
- MDX Content Architecture: A Malformed Entry Should Fail the Build, Not the Page2026-02-048 minEngineering
- Dynamic Routing on a Static Site: Rewrite the Short URL Internally, 308 the Duplicate2026-01-238 minEngineering
- Observability Without a Platform Team: Three Signals in One Postgres Table2026-02-018 minEngineering
- Automated SEO Regression Testing: The Failures Are Silent, So the Invariants Have to Be Asserted2026-01-149 minEngineering