AI search optimisation, or GEO (Generative Engine Optimization), is a technical discipline: making a site readable, identifiable and citable by answer engines such as ChatGPT, Claude, Perplexity or Google AI Mode. Readable, because their crawlers must be able to get in and extract the text. Identifiable, because they must know who is speaking. Citable, because an answer points to a dated, structured source. The six dimensions below are exactly the ones our free GEO test scores, in order of weight.
Every section follows the same plan: what the engines need, with a link to the vendor's documentation; how to check it; how to fix it in the code; what we did on seoforge.fr. No claim about a crawler's behaviour comes from memory: each one links to the vendor page we read. Where a behaviour is undocumented, we say so.
1. AI crawler access (weight 35)
An answer engine can only cite what it managed to read. The first dimension checks that the assistants' crawlers are allowed to browse the site, and tells two families apart: answer crawlers, which fetch a page to answer a user or index it for AI search, and training crawlers, which build model corpora. Blocking the second is an editorial choice; blocking the first removes the site from answers.
What the engines need
Each vendor documents its crawlers. OpenAI separates OAI-SearchBot, "used to surface websites in search results in ChatGPT's search features", noting that sites opted out of it "will not be shown in ChatGPT search answers"; GPTBot, which may be used in training, so that disallowing it means the content should not be used for training; and ChatGPT-User, which acts on a user's request and for which, in OpenAI's words, "robots.txt rules may not apply" (OpenAI documentation). Anthropic publishes three agents: ClaudeBot for training, Claude-User when a user asks a question, Claude-SearchBot for search; all three "respect 'do not crawl' signals by honoring industry standard directives in robots.txt", and blocking Claude-User or Claude-SearchBot "may reduce your site's visibility" in answers (Anthropic documentation). Perplexity separates PerplexityBot, "designed to surface and link websites in search results" and "not used to crawl content for AI foundation models", from Perplexity-User, which "generally ignores robots.txt rules" because a user initiated the request (Perplexity documentation). Google has no separate AI crawler: Google-Extended is a control token that governs whether content may be used "for training future generations of Gemini models" and for grounding, and "does not impact a site's inclusion in Google Search" (Google documentation). For Amazonbot, Applebot-Extended, meta-externalagent, Bytespider, CCBot and MistralAI-User, which are in our list, we have no equivalent verified documentation: the test treats them by their declared role without asserting their behaviour.
Reading the file follows RFC 9309: a crawler obeys only the group whose user-agent token matches its own most specifically, falls back to the * group when none matches, and a file must remain parseable up to 500 KiB. Two practical consequences: a dedicated group for one crawler with a bare Allow: / cancels, for that crawler, every disallow of the general group; and a robots.txt answering with a server error (5xx) means "everything disallowed" for a compliant crawler, whereas a 404 means "everything allowed".
How to check it
Run the GEO test on your homepage. The "AI crawler access" dimension reports "AI answer crawlers allowed: x of 8" and "Training crawlers allowed: x of 8", crawler by crawler, with the robots.txt rule behind each verdict. "robots.txt read and compliant" flags malformed lines; "Our crawler allowed on the homepage" confirms the test itself could get in. On the command line the same check is one request: curl -A "OAI-SearchBot" -sI https://your-site.com/, then a read of your robots.txt with the crawler's exact name.
How to fix it in the code
One group shared by every named crawler and by *: every crawler listed then obeys the same rules, and a disallow added later applies to all. Crawlers you deliberately refuse get their own group with Disallow: /.
# robots.txt: one group for all, same rules
User-agent: OAI-SearchBot
User-agent: ChatGPT-User
User-agent: Claude-SearchBot
User-agent: Claude-User
User-agent: PerplexityBot
User-agent: Google-Extended
User-agent: *
Allow: /
Disallow: /admin/
Disallow: /api/
# editorial choice: no training
User-agent: GPTBot
User-agent: ClaudeBot
Disallow: /
Sitemap: https://your-site.com/sitemap.xml
The file is not enough when a CDN or firewall filters crawlers upstream: the rule must also exist in the CDN console, otherwise robots.txt allows a crawler that the firewall answers with a 403. The test only sees the final response, which is enough to detect it.
What we did on seoforge.fr
Our robots.txt lists every AI crawler by name in a single group shared with *, and an automated test forbids a crawler from getting a separate group by accident. The crawler-by-crawler detail is in the article on our implementation.
2. Content extractability (weight 20)
Getting in is not enough: the crawler must find text in the HTML it receives. Google documents that JavaScript rendering is queued and "can take longer" than a few seconds, and recommends server-side rendering because "not all bots can run JavaScript" (Google documentation). The assistant vendors document no JavaScript rendering for their crawlers; absent documentation, the only safe assumption is that they read the HTML as the server sends it.
What the engines need
A page whose text, title and headings are present in the HTTP response, with no script execution. One h1, h2 headings that split the topic, a title tag and a meta description that summarise the page, a canonical pointing at the reference URL, a lang attribute declaring the language. These are the same fundamentals as on-page SEO; the difference is the reader, which does not render the page.
How to check it
The GEO test reports "Text readable without JavaScript: n characters (x% of the HTML)", "Single h1: n found", "h2 headings: n", "<title> tag of 10 to 70 characters", "Meta description of 50 to 170 characters", "Canonical tag" and "lang attribute on <html>". On the command line, curl -s https://your-site.com/ | sed 's/<[^>]*>//g' | wc -w gives the order of magnitude of the text served; near zero means a shell filled by JavaScript.
How to fix it in the code
The fix is architectural: render on the server, or pre-render at deploy time. On WordPress and Shopify it is the default; it gets lost with a theme that loads content over AJAX or a page builder that injects it on load. On a Next.js, Nuxt or SvelteKit application, server rendering is configured per page. The expected pattern:
<!-- shell: nothing to extract without JavaScript -->
<body><div id="app"></div><script src="/bundle.js"></script></body>
<!-- server-rendered: the text is in the response -->
<body>
<main>
<h1>Technical SEO audit for SMEs</h1>
<p>We go through your site and ship the fixes…</p>
<h2>What we analyse</h2>
</main>
<script src="/bundle.js" defer></script>
</body>
What we did on seoforge.fr
The site is server-rendered page by page, with one h1 and inline structured data; JavaScript only adds behaviour. Details are in the implementation article.
3. Structured data and entity identity (weight 15)
Reading is not enough either: the engine must know who is speaking, and not confuse the company with a namesake. JSON-LD is the documented way to tell it.
What the engines need
Google documents for Organization the properties name, url, logo, sameAs, legalName, vatID, taxID, iso6523Code and foundingDate, describes sameAs as "the URL of a page on another website with additional information about your organization", and notes that some properties "are used behind the scenes to disambiguate your organization from other organizations" (Google documentation). The assistant vendors publish no equivalent documentation on their use of JSON-LD, so we assert nothing about how they read it. What we know is that these properties are the only machine-readable identity on the page, and that namesakes exist: other products trade as SEOForge.
How to check it
The test reports "Valid JSON-LD blocks: n (invalid: n)", "Identity entity (Organization, LocalBusiness, Person)", "name and url present on the entity", "Official sameAs profiles on the entity: n", "Legal identifier (SIREN/vatID) present", "WebSite node present" and "Content structured data". Invalid JSON-LD is counted, not guessed: one extra comma makes it unreadable for everyone.
How to fix it in the code
An Organization node with a stable @id, the registry identifiers, the founder and the official profiles, on every page:
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Organization",
"@id": "https://your-site.com/#organization",
"name": "Your company",
"legalName": "YOUR COMPANY LTD",
"url": "https://your-site.com",
"logo": "https://your-site.com/logo.png",
"description": "What you do, where - not to be confused with…",
"identifier": {"@type": "PropertyValue", "propertyID": "Company number", "value": "12345678"},
"vatID": "GB123456789",
"foundingDate": "2020-03-01",
"founder": {"@type": "Person", "name": "First Last", "url": "https://your-site.com/en/about"},
"sameAs": ["https://www.linkedin.com/company/your-company", "https://find-and-update.company-information.service.gov.uk/company/…"]
}
</script>
Values come from configuration, never typed by hand into a template: a company number typed twice ends up diverging.
What we did on seoforge.fr
An Organization, LocalBusiness and WebSite graph on every page, with the French registry identifiers, founding date, founder and a description that names the namesake problem; the detail and the choices are in the implementation article. The technical SEO audit checks this graph on the pages that matter.
4. The llms.txt file (weight 10)
llms.txt is a convention proposed by Jeremy Howard in September 2024: a Markdown file at the root of the site that presents the site to language models (llmstxt.org). The format is short: "an H1 with the name of the project or site" is the only required section, followed by "a blockquote with a short summary" and "zero or more markdown sections delimited by H2 headers" containing lists of links.
What the engines need
Nothing documented: none of the vendors cited above has published that it reads this file. We recommend it for a more modest reason: it costs an hour, cannot hurt, and it is the only place where the site describes itself in plain text, with its disambiguation sentence and its priority pages. The test gives it a low weight for that reason.
How to check it
The test reports "/llms.txt present (HTTP n)", "'# Name' heading on the first line", "Blockquote summary present", "'##' sections: n", "Markdown links to your pages: n (3 expected at least)", "Reasonable size" and the optional presence of /llms-full.txt. A file served as HTML, such as a 404 page disguised as a 200, is detected as absent.
How to fix it in the code
A skeleton is enough to start; our generator produces one from your sitemap and your real tags, inventing nothing:
# Your company
> Your company - what you do, for whom, where. Not to be confused with…
## Services
- [Main service](https://your-site.com/en/main-service): one sentence that answers the question.
- [Second service](https://your-site.com/en/second-service): same.
## Resources
- [Reference guide](https://your-site.com/en/guides/…): what it covers.
## Contact
- [Contact](https://your-site.com/en/contact) - reply time.
Serve it as text/plain, without a redirect, and generate it from the site's routes rather than maintaining it by hand: a stale llms.txt is worth less than none.
What we did on seoforge.fr
Our /llms.txt is an application route built from the site map and the public prices; it cannot go stale. The implementation article explains that choice.
5. Freshness, dates and page identity (weight 10)
An answer readily cites a dated source. This dimension checks that the page carries a machine-readable date, plus the tags that identify the page itself: Open Graph and, on a multilingual site, hreflang.
What the engines need
No assistant vendor documents its use of dates; we stick to what is observable: a date in time datetime, in article:modified_time or in dateModified in JSON-LD is readable without interpretation, a date written in the text is not. The one rule we impose is honesty: an update date must match a real change, or it eventually turns against the site.
How to check it
The test reports "Machine-readable date: … (source: …)", or "Date found only in the text", or "No publication or update date detectable"; then "Open Graph title, description, image: complete", "og:site_name or og:url present" and "hreflang tags". hreflang is informative on a monolingual site, and a homepage without dated content is not penalised for lacking a date.
How to fix it in the code
The date must come from a real source: the CMS update field, or the date of the last commit that touched the template. Never the current date at render time, never the deploy date.
{
"@context": "https://schema.org",
"@type": "Article",
"headline": "Page title",
"datePublished": "2026-03-12T09:00:00+01:00",
"dateModified": "2026-08-31T14:20:00+02:00"
}
<!-- and, visible: -->
<p>Last updated: <time datetime="2026-08-31">31 August 2026</time></p>
What we did on seoforge.fr
Page dates come from the git history of the templates, computed at deploy time, and article dates from their update field; when nothing reliable is known, no date is emitted. It is explained in the implementation article.
6. Server response time, TTFB (weight 10)
A crawler has a budget per site; a page that takes two seconds to send its first byte uses more of it than one that answers in 200 milliseconds. TTFB, measured here on a single request to the homepage, is an availability indicator, not a Core Web Vitals measurement.
What the engines need
No assistant vendor publishes a response-time threshold for its crawlers. Our thresholds, 800 ms for "good" and 1800 ms for "acceptable", are deliberately generous and published on the test page; they reflect what a properly cached server reaches without effort.
How to check it
The test reports "Response time: n ms" with the thresholds, and "Redirects before the final page: n". On the command line: curl -o /dev/null -s -w "%{time_starttransfer}\n" https://your-site.com/. For field metrics, LCP, CLS and INP, use the speed test.
How to fix it in the code
Three levers, in this order: an HTTP cache for anonymous pages, at most one redirect (http to https, then nothing), and a server or CDN close to the visitors. On WordPress, a page cache; on Symfony or Laravel, a reverse-proxy cache with s-maxage.
# anonymous page response: cacheable by the CDN for 5 min
Cache-Control: public, s-maxage=300, stale-while-revalidate=60
# one redirect, never a chain
http://your-site.com/ -> https://your-site.com/ (301)
What we did on seoforge.fr
An application-level HTTP cache on public pages and a single redirect from the root to the default language, described in the implementation article. When TTFB or Core Web Vitals are the problem, the Core Web Vitals optimisation service handles it.
What GEO does not guarantee
The test scores prerequisites, not results. It does not measure whether an assistant cites you, and we assert nothing about how an engine ranks or selects its sources: none of the vendors cited documents it. A good score means an engine can read you, identify you and date you; a bad score means a citation is very unlikely. In between, the only honest measurement is to fix a set of questions representative of your market, ask the assistants at regular intervals, and record who gets cited. That is what the measurement phase of the GEO Audit does; our own follow-up on seoforge.fr is announced in the implementation article, with its method and its deadline.
What is the difference between SEO and GEO, in the code
The two disciplines share most of their foundation: a site open to crawlers, indexable pages, clean HTML, valid structured data, decent response times. A site that ranks well on Google has already done most of the work, and the technical SEO audit covers that foundation.
Three differences matter in the code. The first is crawler access: in SEO one crawler matters and it is always allowed; in GEO, a dozen documented crawlers, split between answer and training, each with its token in robots.txt and often a rule to add in the CDN. The second is extractability without JavaScript: Google eventually renders the page, late; assistant crawlers document no rendering, and the text must be in the server response. The third is entity identity: SEO tolerates an implicit organisation; a generated answer must know which company it cites, which requires the registry identifiers, the official profiles and a disambiguation sentence, in the JSON-LD as in the llms.txt.
GEO, AEO, AI SEO: what the names do and do not distinguish
Nothing, in the code. GEO, Generative Engine Optimization, names optimisation for engines that generate an answer: ChatGPT, Claude, Perplexity, Google AI Mode. AEO, Answer Engine Optimization, stresses the answer served without a click, and the phrase predates assistants, when it targeted Google's featured snippets. AI SEO, AI search optimisation, LLM SEO or AISEO are the same ideas with other words, and one site satisfies all of them: allowed crawlers, text readable without a script, an identifiable entity, real dates. The vocabulary is not settled and every agency picks its own; we write "AI search optimisation (GEO)" and describe what we check, so that the name does not matter. The only distinction that holds is the one with classic SEO, covered in the previous section: same foundations, three differences in the code.
Where to start
Run the free GEO test on your site: each of the six dimensions is scored with its evidence, and the actions are ranked by priority. If you want us to do the work, the GEO Audit, at €490 excl. VAT, covers these six dimensions on your priority pages, adds the analysis of your server logs and the citation measurement, and ends with the fixes implemented in your code.