Methodology
Every check, every threshold, every deduction — published.
The audit is deterministic: the same site on the same day produces the same findings and the same scores. No opinion layer decides what fails, and no language model touches a score. This page lists what the audit fetches, what each check fires on, and exactly how the numbers combine. The limits and threshold bands below are rendered from the same constants the engine imports, so the page cannot drift from the code that produces your report.
Think a finding on your report is wrong? Send the report link — a person re-runs the check and answers.
What we fetch
The crawl contract
One audit fetches up to 20 pages of the submitted site and runs four small probe phases — three after the crawl, and the AI-crawler access test early, before the crawl has had a chance to look like a burst of traffic. Two further requests leave the audited host: the www/non-www counterpart of the homepage, to see whether both serve the same site, and one PageSpeed Insights lookup at Google. Nothing else is fetched.
| Phase | Budget | What happens |
|---|---|---|
| Page crawl | up to 20 requests, 45 s | Homepage first, then robots.txt, llms.txt and the sitemap. The remaining budget goes to same-site pages — sitemap URLs first (the site's own canonical page list), then navigation links. |
| Internal-link probes | up to 10 requests, 10 s | HEAD checks (status and headers only, no body) on same-site link targets the crawl did not fetch. Links the budget never reaches are reported as unchecked — never judged broken. |
| Sitemap sample | up to 5 requests, 6 s | HEAD checks on a sample of sitemap URLs the crawl did not reach. The report names how many were listed and how many were actually checked. |
| Asset sizing | up to 8 requests, 8 s | Sizes same-site CSS, script and image files from response headers (plus one single-byte ranged request when a server hides the size). Assets on other domains are counted but never fetched; assets that could not be sized are never guessed. |
| AI-crawler access test | up to 8 requests, 20 s | Fetches the homepage again once per AI crawler user agent (OAI-SearchBot, PerplexityBot, GPTBot, ClaudeBot, CCBot), skipping any crawler robots.txt already disallows, and compares status and body size with the audit's own fetch. Because a refusal only says something about a crawler once request rate is ruled out, the phase runs after the first four requests of the crawl rather than at its tail, leaves 0.25–0.5 s idle before each request, retries a rate-limited answer once (honouring Retry-After when the wait it asks for is 3 s or less, and reporting rather than sleeping when it is longer), and answers a 401/403/451 with one confirmation fetch using our own normal User-Agent. If that normal fetch is refused too, the refusal is site-wide or rate-based, not a rule about AI crawlers, and it is reported as not judged instead of as a block. A user agent the budget or deadline never reached is reported as unchecked, never judged. This is a user-agent-level test: blocks keyed on a crawler's IP ranges cannot be seen from here. |
| www / non-www check | 1 request, 15 s | Fetches the counterpart host of the homepage (www if you submitted the bare domain, and the other way round) alongside the crawl, to see whether both serve the same site with no redirect and no canonical resolving the pair. If that request does not answer, nothing is judged. |
| PageSpeed Insights | 1 request to googleapis.com | Asks Google's PageSpeed Insights API (v5, mobile strategy, homepage only) for Core Web Vitals field data from the Chrome User Experience Report and one Lighthouse lab run. The lookup starts before the crawl and runs on its own thread, so most of it overlaps work the audit was doing anyway — but it is not free: on a site the crawl finishes quickly, the audit can end up waiting on Google. That wait is capped twice: the lookup is abandoned 25 s after it started, and the extra wait after the crawl and probe phases is capped at the time those phases themselves took (never less than 22 s). A one-page site that crawled in a fraction of a second therefore waits about 22 s, not 25. Only the homepage URL is sent. A timeout, a quota refusal or a missing field-data record means the signal is not measured — see the speed pillar below. |
| Rule on every request | Limit |
|---|---|
| Protocols and ports | http/https only, standard web ports only, no credentials in URLs |
| Address guard | Every hostname is resolved and every address must be publicly routable — private, internal, loopback and reserved addresses are refused. Every redirect hop is re-validated the same way. |
| Redirects | at most 10 hops per fetch |
| Response body | at most 5 MB read per response |
| Time per fetch | at most 15 s wall clock, redirects included |
| Identification | every request sends the User-Agent CorpusPlanBot/1.0 (+https://corpushq.co/plan) |
How the numbers combine
The scoring formula, in plain words
Each pillar starts at 100. Two layers take points off — binary findings and graded quality measurements — and both are written out on the report, line by line.
1. Binary findings
Every finding in the tables below deducts a fixed amount from its pillar. Deductions add up without limit (floor 0), so every additional problem costs something.
| Severity | Deduction |
|---|---|
| Critical | 35 points |
| Major | 15 points |
| Minor | 5 points |
One critical alone caps a pillar at 65.
2. Graded quality curve
Each pillar also measures quality signals scored 0.0–1.0 against the written bands below. The pillar's graded deduction is 40 × (1 − weighted mean of the measured signals). Signals that could not be measured leave the denominator entirely — a pillar with nothing measurable takes no graded deduction. A site whose measured signals all hit their top band deducts nothing and can score 100; no measurement is ever invented to avoid that.
Every graded cost of 0.5 points or more appears on the report as a written note carrying the measured value and the exact point cost — no score moves without a written reason.
Some lines on the report are reference notes, not problems: Google's own Lighthouse SEO score, or an acknowledgement that every noindexed page we found is a legitimate flow step. They deduct nothing, and they are also left out of the report's "N findings" and "N critical" counts — a note whose own fix hint says there is nothing to do must not inflate your problem count. They still appear in the checklist, marked as costing no points.
Pillar score and overall
pillar = 100 − finding deductions − graded deduction, clamped to 0–100 and rounded. The overall score is the weighted sum of the five pillars.
| Pillar | Weight in the overall score |
|---|---|
| SEO | 25% |
| Speed | 15% |
| Trust | 20% |
| Content | 15% |
| AI citability | 25% |
SEO and GEO carry the most weight because search and AI-assistant visibility are what the product is bought for; trust is next because broken TLS or missing contact routes lose visitors you already won; speed and content are real factors but usually the symptom rather than the disease. The weighting is our judgment and is stated here as exactly that.
Pillar · weight 25%
SEO
Classic search fundamentals, checked on the homepage and swept across every crawled page. Each finding names the affected paths as evidence.
| Check | Code | Severity | Fires when |
|---|---|---|---|
| Homepage title | SEO_MISSING_TITLE | Critical | The homepage has no <title> tag. |
| Title length | SEO_TITLE_LENGTH | Minor | The homepage title is outside 15–65 characters. |
| noindex | SEO_NOINDEX_HOMEPAGE | Critical | A robots meta tag with noindex tells engines to drop the homepage from their index. |
| Meta description | SEO_MISSING_META_DESCRIPTION | Major | The homepage has no meta description. |
| Description length | SEO_META_DESCRIPTION_LENGTH | Minor | The meta description is outside 50–165 characters. |
| H1 heading | SEO_MISSING_H1 | Major | The homepage has no H1. |
| Multiple H1 | SEO_MULTIPLE_H1 | Minor | The homepage carries more than one H1. |
| Subheadings | SEO_NO_SUBHEADINGS | Minor | No H2/H3 subheadings on the homepage. |
| Canonical link | SEO_MISSING_CANONICAL | Minor | No rel="canonical" link tag on the homepage. |
| Image alt text | SEO_IMG_ALT_MISSING | Major / Minor | Under 50% of sampled homepage images carry alt text (major); 50–80% (minor). Sample: the first 30 images. |
| robots.txt blocks all | SEO_ROBOTS_BLOCKS_ALL | Critical | robots.txt disallows the whole site for every user-agent. |
| robots.txt is HTML | SEO_ROBOTS_NOT_TEXT | Minor | /robots.txt answers with an HTML page instead of a plain-text file. |
| robots.txt missing | SEO_MISSING_ROBOTS | Minor | /robots.txt answers 404. |
| Sitemap invalid | SEO_SITEMAP_INVALID | Minor | A sitemap responds but is not valid XML. |
| Sitemap missing | SEO_MISSING_SITEMAP | Minor | The sitemap answers 404. |
| Pages without titles | SEO_PAGES_MISSING_TITLE | Major | Crawled inner pages with no <title>, each path named. |
| Page title lengths | SEO_PAGES_TITLE_LENGTH | Minor | Inner-page titles outside 15–65 characters. |
| Pages without descriptions | SEO_PAGES_MISSING_META_DESCRIPTION | Minor | Crawled inner pages with no meta description. |
| Pages without H1 | SEO_PAGES_MISSING_H1 | Minor | Crawled inner pages with no H1. |
| Pages with multiple H1 | SEO_PAGES_MULTIPLE_H1 | Minor | Crawled inner pages with more than one H1. |
| Duplicate titles | SEO_DUPLICATE_TITLES | Major | The same title on two or more crawled pages (post-redirect duplicates of the same document are excluded). |
| Duplicate descriptions | SEO_DUPLICATE_META_DESCRIPTIONS | Minor | The same meta description shared by two or more crawled pages. |
| Broken internal links | SEO_BROKEN_INTERNAL_LINKS | Major | Probed internal links answering HTTP 404/410. Only links that were actually probed and answered are judged; the finding states how many were checked out of how many exist. |
| Dead sitemap URLs | SEO_SITEMAP_DEAD_URLS | Major | Sampled sitemap URLs answering HTTP 404/410 — same only-what-was-checked rule. |
| Pages set to noindex | SEO_PAGES_NOINDEX | Major | A crawled page carries noindex in a robots (or googlebot) meta tag or in the X-Robots-Tag response header — it cannot appear in search results at all. Each page is named with which of the two carries the directive. Pages whose path looks like a flow step (cart, checkout, thank-you, login, account, unsubscribe…) are not charged at all — noindex is the correct setting there — and are listed separately in the finding so the count above is only pages that should be findable. |
| Noindexed pages, all of them flow steps | SEO_PAGES_NOINDEX_UTILITY_ONLY | Informational — 0 points | Every noindexed page the crawl found is a utility or confirmation step (cart, checkout, thank-you, login, account, unsubscribe…). That is the correct configuration, so it deducts nothing; the note exists so the pages are named. We judge this from the path alone — if one of them is a page you want found, remove its noindex. |
| Canonical to another domain | SEO_CANONICAL_OFF_HOST | Major | A crawled page's canonical URL resolves to a genuinely different host, handing credit for the content to that domain. The www/non-www counterpart of the audited site is the same site and is never listed here — pointing every page at a preferred host is exactly the fix SEO_DUPLICATE_HOST asks for. The finding names the two cases where an off-host canonical is intended (the page is a syndicated copy of one published on that domain, or the site deliberately consolidates onto another host) so the listed pages get checked rather than changed on sight. |
| Canonical to http | SEO_CANONICAL_HTTP | Major | The site is served over HTTPS but a canonical names the insecure http:// URL. |
| Canonical to another page | SEO_CANONICAL_NOT_SELF | Minor | A page's canonical names a different page on the same site. Correct for deliberate duplicates (print views, paginated series, parameter variants) and wrong elsewhere — the finding says so and names the pages rather than assuming. |
| www and non-www both live | SEO_DUPLICATE_HOST | Major | Both hosts answer 2xx with the same homepage (identical title and within 90% the same size), neither redirects to the other, and no canonical in the HTML we fetched resolves the pair — a canonical injected later by JavaScript cannot be seen from here, and the finding says so. Different content on the two hosts, a redirect, a canonical, or a counterpart that did not answer: no finding. The counterpart is only fetched for a host we can resolve to a registered domain, which includes multi-label endings such as .co.uk and .com.au; a host under a second-level suffix we do not list is treated as a subdomain and simply not checked. |
| Duplicate H1 headings | SEO_DUPLICATE_H1 | Minor | The same H1 text on two or more crawled pages, each group named with its paths. |
| Page language | SEO_MISSING_LANG | Minor | The homepage's <html> tag carries no lang attribute. |
| hreflang consistency | SEO_HREFLANG_INCONSISTENT | Minor | The homepage publishes hreflang alternates but none points back at the homepage itself, or a value is not shaped like a language (or language-region) code. Sites with no hreflang at all are not judged. |
| Lighthouse SEO score | SEO_PSI_LIGHTHOUSE_SEO | Informational — 0 points | Not a finding against the site: when the PageSpeed Insights lookup succeeds, the report carries Google's own basic-SEO category score for cross-reference. Our SEO score is measured independently from our crawl and is never derived from it. It deducts nothing whether it is 100 or 40. |
| Graded signal | Code | Weight | Scored against | Basis |
|---|---|---|---|---|
| Title length | SEO_GRADE_TITLE | 1.0 | missing → 0; 30–60 characters → 1.0; else within 15–65 → 0.75; outside → 0.4 | Google truncates result titles around 60 characters (published guidance); the partial-credit steps are our judgment. |
| Meta description length | SEO_GRADE_META_DESCRIPTION | 1.0 | missing → 0; 70–155 characters → 1.0; else within 50–165 → 0.75; outside → 0.4 | Google cuts descriptions around 155–160 characters on desktop results; steps our judgment. |
| Heading structure | SEO_GRADE_HEADINGS | 1.0 | one H1 with ordered levels → 1.0; −0.25 for no H2/H3; −0.25 for skipped levels; multiple H1 → 0.5; no H1 → 0.2 | Our judgment: an ordered outline is what crawlers and screen readers parse best. |
| Canonical link | SEO_GRADE_CANONICAL | 0.5 | present → 1.0; absent → 0 | Plain presence check. |
| Internal links from the homepage | SEO_GRADE_INTERNAL_LINKS | 1.0 | ≥ 8 path(s) → 1.0; ≥ 4 path(s) → 0.85; ≥ 1 path(s) → 0.6; below every band → 0 | Our judgment: a normal navigation plus footer links eight or more distinct pages. |
| Title and H1 agreement | SEO_GRADE_TITLE_H1_MATCH | 0.5 | mean across crawled pages of (words shared by the title and the H1) ÷ (the title's word count), counting words of three or more characters and dropping common stopwords: ≥ 0.6 → 1.0; ≥ 0.35 → 0.9; ≥ 0.15 → 0.75; below every band → 0.5 | Our judgment, and deliberately forgiving: a title written for the search result and an H1 written for the visitor should not be identical, so the floor is 0.5 rather than 0. Low agreement is a hint that one of the two is generic, not a defect. The question is how much of what the search result says the page's own heading echoes, so the title's words are the denominator; dividing by the smaller of the two sets inverted that — a one-word brand H1 scored a perfect 1.00 while a descriptive H1 that added words scored 0.20. An H1 with fewer than 2 meaningful words says too little to measure agreement at all and is left out rather than rewarded. Where a site wraps its logo in an H1 inside <header> or <nav>, the first H1 outside those wrappers is used instead. Pages missing a title or an H1 are not measured here — the dedicated checks above report those. |
Pillar · weight 15%
Speed
Server response time, page weight, compression and mobile setup — measured from the actual fetches, never estimated from a traffic database.
One signal in this pillar does come from Google: a single PageSpeed Insights lookup for the homepage, run alongside the crawl. When Chrome has enough real traffic to this site, that returns Core Web Vitals field data — what actual visitors experienced — and it is the heaviest-weighted measurement here. When it does not, the Lighthouse lab score stands in at a lower weight, and when the lookup fails or is refused for quota, nothing from Google is scored at all. Absence is never a deduction: a site with no field data cannot score lower than one with good field data.
Known limitation — two speed scores are not always comparable. Everything else in this audit is deterministic: the same site checked twice the same way produces the same number. The PageSpeed Insights signal is the exception. In production today there is no API key, so every lookup is refused for quota and contributes nothing to any score. On the day a key is added, speed scores will shift across every site we have audited, with nothing about those sites having changed. An audit measured with PageSpeed Insights and one without it are therefore not directly comparable, and neither is a before/after pair that straddles that change. A second reason to read the lab number cautiously: Lighthouse's performance score is itself a composite of load-timing signals, several of which this pillar already measures independently from our own fetch, so when it is used the two overlap rather than corroborate.
| Check | Code | Severity | Fires when |
|---|---|---|---|
| Server response time | SPEED_SLOW_RESPONSE | Major / Minor | The homepage takes 2.0 s or more to start responding (major); 0.8 s or more (minor). |
| Homepage HTML weight | SPEED_HEAVY_HTML | Major / Minor | The homepage HTML alone is 3072 KB or more (major); 1536 KB or more (minor). |
| Text compression | SPEED_NO_COMPRESSION | Major | The homepage response carries no gzip/brotli/zstd/deflate Content-Encoding header. |
| Mobile viewport | SPEED_NO_VIEWPORT | Major | No viewport meta tag — the site renders as a shrunken desktop page on phones. |
| Slow inner pages | SPEED_SLOW_PAGES | Minor | The median response time across three or more crawled inner pages is 2.0 s or more. |
| Redirect chain | SPEED_REDIRECT_CHAIN | Minor | The homepage settles only after more than 2 redirects; one hop (http to https, or apex to www) is normal. |
| Oversized assets | SPEED_HEAVY_ASSET | Minor | A sampled homepage asset transfers 1024 KB (1 MB) or more, named with its measured size. Assets the budget never sized are not judged. |
| Images reserving no space | SPEED_IMG_NO_DIMENSIONS | Minor | At least 3 homepage <img> elements, and at least 25% of them, reserve no space: no width and height attributes, and no inline style carrying a real height or an aspect-ratio. The inline style is parsed into declarations rather than searched for the words "width" and "height", because max-width:100%;height:auto — the common responsive idiom — reserves nothing at all and used to read as sized. SVG icons, images inside <noscript> and 1x1 tracking pixels are excluded from both the count of unsized images and the total they are measured against. Without reserved space the page reflows as each image arrives (web.dev names this the main cause of layout shift). Read from the HTML only: an image sized entirely in a stylesheet cannot be seen from here, and the finding says so and names the images instead of assuming. |
| Graded signal | Code | Weight | Scored against | Basis |
|---|---|---|---|---|
| Server response time | SPEED_GRADE_TTFB | 1.0 | ≤ 0.2 s → 1.0; ≤ 0.4 s → 0.9; ≤ 0.6 s → 0.75; ≤ 0.8 s → 0.6; ≤ 1.5 s → 0.35; ≤ 2.5 s → 0.15; past every band → 0 | Lighthouse's server-response-time audit flags responses over 600 ms; the Chrome UX Report "good" threshold is 800 ms. Band steps are our interpolation. |
| Homepage HTML weight | SPEED_GRADE_HTML_WEIGHT | 1.0 | ≤ 50 KB → 1.0; ≤ 100 KB → 0.85; ≤ 200 KB → 0.6; ≤ 500 KB → 0.35; ≤ 1024 KB → 0.15; past every band → 0 | HTTP Archive Web Almanac: the median HTML document transfers around 30 KB. |
| Inline CSS/JS in the HTML | SPEED_GRADE_INLINE_BLOAT | 1.0 | ≤ 15 KB → 1.0; ≤ 40 KB → 0.75; ≤ 100 KB → 0.5; ≤ 250 KB → 0.25; past every band → 0 | Critical-CSS guidance keeps inlined assets near the ~14 KB first TCP round trip; band steps our judgment. JSON data blocks (including the JSON-LD the GEO pillar asks for) are excluded. |
| Text compression | SPEED_GRADE_COMPRESSION | 0.5 | compressed → 1.0; none → 0 | Plain presence check on the Content-Encoding header. |
| Homepage image count | SPEED_GRADE_IMG_COUNT | 0.5 | ≤ 15 image(s) → 1.0; ≤ 30 image(s) → 0.85; ≤ 50 image(s) → 0.6; ≤ 80 image(s) → 0.35; past every band → 0.15 | HTTP Archive Web Almanac media chapter: the median page carries around 20–25 images. |
| Core Web Vitals (real visitors) | SPEED_GRADE_CWV | 2.0 for page-level data; 1.5 when only origin-level data exists, and scaled by n/3 when Google has fewer than three metrics | mean over the metrics Google has: each of LCP, INP and CLS scores 1.0 in Google's "good" band, 0.5 in "needs improvement" and 0 in "poor". Thresholds: LCP good at or under 2.5 s, poor over 4 s; INP good at or under 200 ms, poor over 500 ms; CLS good at or under 0.1, poor over 0.25. | Google's published Core Web Vitals thresholds (web.dev); the good/half/zero split across the three bands is our judgment. Measured by Chrome on real visits over the last 28 days, which is why page-level data carries the heaviest weight in this pillar. When Google has no field data for the audited page but does have it for the origin, the origin-level record is used instead — that is the aggregate across every page of the domain, so it describes the site's typical experience and not the page we audited. It is weaker evidence, and it is treated that way: the weight drops to the lab-run weight (1.5), and the finding's own title says the numbers are for the whole domain rather than this page. Google has no field data for most small sites — that is normal, and it is then not measured at all: absence leaves the denominator, so a site Google knows nothing about can never score lower here than a site with good numbers. |
| Lighthouse performance (lab run) | SPEED_GRADE_LAB_PERFORMANCE | 1.5 | Lighthouse's performance score for the homepage ÷ 100. Used only when there is no field data; field data always wins. | Google's own composite lab score, on a simulated mobile connection. One simulated load from Google's machine is weaker evidence than what real visitors measured, so it weighs less than the field-data signal. Not measured when the PageSpeed Insights lookup failed, timed out or was refused for quota. |
| Modern image formats | SPEED_GRADE_IMG_FORMATS | 0.5 | WebP/AVIF ÷ classifiable same-origin homepage images, counted once per image element or <picture> group however many srcset variants it names: ≥ 0.8 share → 1.0; ≥ 0.5 share → 0.85; ≥ 0.25 share → 0.7; below every band → 0.5 | web.dev's modern-formats guidance: a WebP file runs roughly a quarter to a third smaller than a comparable JPEG. The band steps are our judgment and the floor is 0.5, not 0 — a JPEG is heavier, not broken. A <picture> group offering a modern format in any of its sources counts once as modern; its JPEG fallback is the correct way to write that markup, not a legacy image. Formats are read from the URL, which is evidence about the request and not about the bytes: images on other domains, URLs without an extension, and URLs bearing the marks of a format-negotiating CDN (/cdn-cgi/image/, /_next/image, f_auto, a format= parameter) are unclassifiable — they leave the count entirely and the finding names how many were left out. Under 3 classifiable images, nothing is measured. |
| Estimated page weight with assets | SPEED_GRADE_PAGE_WEIGHT | 1.0 | ≤ 512 KB → 1.0; ≤ 1024 KB → 0.9; ≤ 2048 KB → 0.7; ≤ 4096 KB → 0.4; ≤ 8192 KB → 0.2; past every band → 0.1 | HTTP Archive: the median page transfers about 2–2.5 MB in total; Lighthouse flags multi-MB payloads. Extrapolated from the sampled assets and labeled as an estimate on the report; omitted entirely when assets exist but none could be sized. |
Pillar · weight 20%
Trust
TLS, security headers, and the signals buyers look for before they call.
| Check | Code | Severity | Fires when |
|---|---|---|---|
| TLS certificate | TRUST_TLS_ERROR | Critical | HTTPS failed with a certificate or TLS error and the site was only reachable over insecure HTTP. |
| HTTPS | TRUST_NO_HTTPS | Critical | The site is served over plain HTTP — it never answered over HTTPS, or the homepage lands on an http:// URL. |
| Homepage status | TRUST_HOMEPAGE_HTTP_ERROR | Critical | The homepage answers an HTTP error status (400 or higher). |
| HSTS | TRUST_MISSING_HSTS | Minor | An HTTPS site without a Strict-Transport-Security header. |
| Security headers | TRUST_MISSING_SECURITY_HEADERS | Minor | X-Content-Type-Options, or X-Frame-Options/Content-Security-Policy, missing from the homepage response. |
| Mixed content | TRUST_MIXED_CONTENT | Major | Scripts, images, iframes, audio or video, or a stylesheet, preload or icon <link>, loading over plain http:// on HTTPS pages — every crawled page checked, affected pages named. Metadata the browser never loads (an og:image tag, canonical or alternate links) is not counted. |
| Contact routes | TRUST_NO_CONTACT_INFO | Major | No phone number, email address or contact link found on the homepage. |
| Tap-to-call near the top (local businesses) | TRUST_NO_TAP_TO_CALL_TOP | Minor | A local business's homepage shows a phone number but has no tel: link in its header, on a sticky call bar, or in the first 1,200 characters of text (footer links excluded) — our HTML stand-in for the first 390px phone screen. Checked against a headless-browser render of 288 such sites, it agreed on 78%; 12% of the times it fired, the rendered page did show a call link, usually a floating button. A site counts as a local business when its homepage has LocalBusiness-type structured data, a street address with state and ZIP, or a tap-to-call link plus a service word in its title; these three checks never run on other sites. |
| Rating near the top (local businesses) | TRUST_NO_RATING_TOP | Minor | No rating with a review count (such as “4.9 from 120 reviews”) in the first 1,200 characters of a local business's homepage text and image alt text. Agreed with the rendered first screen on 306 of 309 sites. |
| Booking link (local businesses) | TRUST_NO_BOOKING_LINK | Minor | No link or button on a local business's homepage that says book, schedule, appointment or reserve, or that points at a scheduling service. Quote and estimate buttons do not count. Agreed with the rendered page on 307 of 309 sites. |
| H1 names a service and a place (local businesses) | TRUST_H1_NO_SERVICE_PLACE | Minor | None of a local business's homepage H1 headings names both a service (plumbing, HVAC, roofing, dental and so on) and a place: a city or town of 10,000 or more people in the 2023 Census list (1,000 or more for a name of several words), a state, county or named region, or a smaller town the page's own address or structured data names. Word-like town names (Mobile, Union, Center) count only when the address names them. A homepage with no H1 at all is reported by SEO_MISSING_H1 instead. Agreed with the rendered page on 260 of 266 local homepages; of 196 times it fired, 1 was wrong (an H1 capitalised by the stylesheet, not the HTML). |
| Call to action | TRUST_NO_CTA | Major | No book/call/quote/buy/contact wording in any homepage link or button. |
| Legal pages | TRUST_NO_LEGAL_PAGES | Minor | No privacy policy link, or no terms link. |
| Broken pages | TRUST_BROKEN_PAGES | Major | Crawled pages answering HTTP errors (the robots.txt/llms.txt/sitemap probes are reported by their own pillars, not here). |
| Graded signal | Code | Weight | Scored against | Basis |
|---|---|---|---|---|
| Trust-signal completeness | TRUST_GRADE_SIGNALS | 2.0 | signals present ÷ 8, across: phone, email, contact link, call to action, privacy link, terms link, a social profile link, Open Graph tags | A plain ratio; the eight-signal list is our judgment. The report names exactly which signals are missing. |
| Security headers | TRUST_GRADE_SECURITY_HEADERS | 1.0 | headers set ÷ headers expected (X-Content-Type-Options; X-Frame-Options or CSP; plus Strict-Transport-Security on HTTPS) | A plain ratio over baseline headers. |
Pillar · weight 15%
Content
Depth, freshness and presentation of the actual copy — counted from the fetched pages.
| Check | Code | Severity | Fires when |
|---|---|---|---|
| Homepage copy depth | CONTENT_THIN_HOMEPAGE | Major / Minor | Under 120 words of visible text on the homepage (major); under 300 (minor). |
| Thin inner pages | CONTENT_THIN_PAGES | Minor | Most crawled inner pages carry under 120 words. |
| Cloned location pages (local businesses) | CONTENT_CLONED_LOCATION_PAGES | Minor | Two or more crawled pages about different named places whose main text is the same once place names are masked: each names its own place more often than the other does, and they share 80% or more of their 5-word phrases. Header, footer and menu text, and lines the homepage shares with half the crawled pages, are left out; a page with under 150 words left is not compared. The finding names the pages. Why 80%: over 439 pairs of pages about different places on 79 sites, the 18 clone pairs (same copy, only the place and a line or two such as the phone number changed) scored 83% to 98%, and every other pair 66% or less. |
| Cloned location pages, not checkable | CONTENT_CLONED_PAGES_UNVERIFIED | Informational — 0 points | The crawl reached two or more pages about a named place, but fewer than two had 150 words of readable text to compare (often text drawn by a script). We say we couldn't verify rather than call them unique. |
| Site footprint | CONTENT_FEW_PAGES | Minor | The sitemap lists fewer than 5 pages. |
| Copyright freshness | CONTENT_STALE_COPYRIGHT | Minor | The newest copyright year on the homepage is two or more years old. No year on the page: no finding. |
| Open Graph tags | CONTENT_MISSING_OG | Minor | og:title is missing, or present without og:description or og:image — shared links render bare. |
| Graded signal | Code | Weight | Scored against | Basis |
|---|---|---|---|---|
| Homepage copy depth | CONTENT_GRADE_HOME_WORDS | 1.0 | ≥ 800 word(s) → 1.0; ≥ 500 word(s) → 0.9; ≥ 300 word(s) → 0.75; ≥ 120 word(s) → 0.5; below every band → 0.2 | Our judgment, anchored on the 120/300-word binary thresholds; full credit needs a genuinely substantial page. |
| Site copy depth | CONTENT_GRADE_SITE_WORDS | 1.0 | ≥ 2500 word(s) → 1.0; ≥ 1200 word(s) → 0.85; ≥ 600 word(s) → 0.65; ≥ 250 word(s) → 0.4; below every band → 0.2 | Our judgment — the same anchoring, scaled to the handful of pages one audit fetches. |
| Image alt coverage | CONTENT_GRADE_ALT_COVERAGE | 1.0 | images with alt text ÷ sampled images (first 30 per page) | A plain ratio. No images on the crawled pages: not measured, no judgment. |
| Copyright-year freshness | CONTENT_GRADE_FRESHNESS | 0.5 | current or last year → 1.0; two years old → 0.5; older → 0.2 | Our judgment (a January page still saying last year is normal). |
Pillar · weight 25%
AI citability
Whether AI assistants can fetch, parse and cite the site. The robots.txt check reads two kinds of AI crawler and prices them differently. Answer crawlers fetch pages live to cite them in AI answers — blocking them is the citability problem: OAI-SearchBot, PerplexityBot, Claude-SearchBot, Applebot. Training crawlers collect text for model training — blocking them is a deliberate choice that does not by itself remove a site from live answers: GPTBot, ClaudeBot, Google-Extended, CCBot, Bytespider, Applebot-Extended. Each crawler's role comes from its operator's published documentation, cited in the pillar source.
The live access test then fetches the homepage as 5 of those crawlers (OAI-SearchBot, PerplexityBot, GPTBot, ClaudeBot, CCBot) and compares the answer with the audit's own fetch. It sees user-agent rules only: a CDN or firewall that blocks a crawler by IP range passes this test, and the report says so.
On llms.txt we say plainly what the evidence shows: an Ahrefs study of 137,210 domains with a valid llms.txt (May 2026) found 97% received zero requests for the file, the top requesters of the rest were SEO audit tools (21.7%) rather than AI crawlers (GPTBot 4.5%, ClaudeBot 0.8%), no major AI provider has committed to the proposed standard, and Google says such files are not needed — so we check it as a no-cost parity item that other tools grade you on, not as a proven visibility lever.
| Check | Code | Severity | Fires when |
|---|---|---|---|
| llms.txt | GEO_MISSING_LLMS_TXT | Minor | /llms.txt answers with an error — the proposed-standard file does not exist. Minor by the evidence above: most AI crawlers never request it; it stays checked because publishing costs nothing and other audit tools grade it. |
| llms.txt structure | GEO_LLMS_TXT_THIN | Minor | An llms.txt exists but lacks a markdown H1 title, or has neither links nor 200+ characters of substance. |
| Structured data | GEO_MISSING_JSONLD | Major | No schema.org JSON-LD found on any crawled page. |
| JSON-LD validity | GEO_JSONLD_INVALID | Minor | JSON-LD blocks exist but none parse as valid JSON. |
| Business identity schema | GEO_NO_ORG_SCHEMA | Minor | JSON-LD parses but no entity identifies the business itself (Organization, LocalBusiness, WebSite or similar). |
| All AI answer crawlers blocked | GEO_ALL_AI_BOTS_BLOCKED | Critical | robots.txt disallows every answer crawler (OAI-SearchBot, PerplexityBot, Claude-SearchBot, Applebot) — the site cannot be read into ChatGPT search, Perplexity, Claude search or Siri answers. The finding says plainly this may be intentional. |
| Some AI answer crawlers blocked | GEO_AI_BOTS_BLOCKED | Major | robots.txt disallows some of the answer crawlers, each named. |
| AI training crawlers blocked | GEO_TRAINING_BOTS_BLOCKED | Minor | robots.txt disallows only training crawlers (GPTBot, ClaudeBot, Google-Extended, CCBot, Bytespider, Applebot-Extended) while the answer crawlers stay allowed. Worded as the deliberate choice it usually is: the site stays citable in live answers; models trained without it may know less about the business. |
| AI crawlers blocked at the edge | GEO_AI_UA_BLOCKED | Major / Minor | The live test: fetching the homepage as a crawler answers 401/403/451 or a bot-challenge page while the audit's own fetch got a 2xx, robots.txt allows that crawler, and a confirmation fetch with our own normal User-Agent was served — so the refusal is keyed on who the crawler says it is, and sits in the CDN, firewall or server. A 429 is never this finding (it is a rate limit, about request volume), and neither is a refusal the normal User-Agent also got. Major when an answer crawler is turned away; minor when only training crawlers are. Each finding names the crawlers, the status codes and the user-agent-only scope. |
| Different page for AI crawlers | GEO_AI_UA_DIFFERENT_PAGE | Minor | A crawler user agent gets a 2xx whose body is under 30% of the regular fetch's — a stripped or substitute page. Not applied when the regular page is under 2048 bytes. |
| Business name consistency | GEO_NAME_INCONSISTENT | Minor | The business name differs across the title tag, og:site_name, JSON-LD name and any H1 that carries the brand — beyond case, punctuation and legal suffixes (LLC, Inc, Ltd…). The variants are listed. |
| Contact details (NAP) consistency | GEO_NAP_INCONSISTENT | Minor | A telephone or street address in the JSON-LD appears on no crawled page. Basis: Google's structured-data guidelines ask that marked-up details match what visitors see. Only compared when both sides exist. |
| Extractable content | GEO_LOW_TEXT_RATIO | Major / Minor | The static HTML exposes under 120 readable words alongside a low text-to-markup ratio — usually JavaScript-rendered content many AI crawlers cannot execute (major); under 250 words (minor). |
| Question-shaped headings | GEO_NO_QUESTION_HEADINGS | Minor | No H2/H3 on any crawled page is phrased as a question and no FAQPage entry was found. Calls to action and section labels ("Questions?", "Ready to get started?", headings of 2 words or fewer) are not counted: a question mark does not make them a question anyone asked, and reporting them as unanswered would be wrong on its face. |
| Answer structure | GEO_NO_ANSWER_STRUCTURE | Minor | The homepage carries 150+ words but no list or table — nothing structured for an assistant to quote. |
| Graded signal | Code | Weight | Scored against | Basis |
|---|---|---|---|---|
| AI-crawler access (live test) | GEO_GRADE_AI_UA_ACCESS | 1.0 | Penalty only. Nothing is measured at all unless at least one crawler was turned away, challenged or served a much smaller page. When it does fire: per judged crawler, served like a regular visit → 1.0; a training crawler turned away, or any crawler served a much smaller page → 0.5; an answer crawler turned away → 0. Mean over the crawlers judged; unchecked, unclear and rate-limited answers leave the denominator. | Our judgment. Every crawler answering normally is not a measurement of anything — not being blocked is the default state of a web server, and crediting a site for it was inflating sparse pages that pass this for free while failing the signals that need real content. So the component simply does not exist on a site nothing was turned away from. A rate limit (HTTP 429) is not judged either way: it says "too many requests", which is about request rate, not about who asked. The weight is 1.0 — a genuine block is priced like the other signals rather than dominating the pillar on the strength of one live test. Not measured when the audit's own fetch was not a 2xx HTML page or no crawler was judged. |
| Structured-data depth | GEO_GRADE_JSONLD_COMPLETENESS | 1.5 | LocalBusiness-type entity: name 0.25 + address 0.25 + telephone 0.2 + url 0.2 + openingHours 0.1 (a PostalAddress without streetAddress earns 60% of the address mark, a plain-text address 40%). Organization-type entity: name 0.25 + url 0.2 + logo 0.15 + sameAs 0.2 + contact 0.2. The best-described entity counts; only a WebSite/Person/Product/Service entity → 0.4. | Google's LocalBusiness structured-data documentation (name and PostalAddress required; telephone, url, opening hours recommended) and Organization documentation (name, url, logo, sameAs, contact route recommended); the point split is our judgment. The note names the missing properties. |
| Entity consistency | GEO_GRADE_ENTITY_CONSISTENCY | 1.0 | signals that agree ÷ signals compared, across: JSON-LD name(s), og:site_name, the title-tag segment carrying the brand, an H1 carrying the brand, and each JSON-LD telephone and street address checked against the pages | A plain ratio; case, punctuation and legal suffixes are tolerated (our judgment). Fewer than two comparable signals: not measured. |
| Direct answers under questions | GEO_GRADE_ANSWERABILITY | 1.0 | questions answered directly ÷ questions found (H2/H3 question headings plus FAQPage entries). "Directly" is a first-sentence rule: the text in the question's own section opens with a sentence of 40 words or fewer that is not itself another question. A bare "Yes." or "No." counts, and so does a list, table or definition list opening the section — those are answer forms in their own right. Under 5 words is a label, not an answer; 300 words is a far outer bound on the section. | Our judgment: the question-plus-short-answer shape is the fragment assistants quote whole. What makes an answer quotable is that it OPENS with the answer — so the length of the whole paragraph is not the test, only its first sentence. A thorough answer is not a worse answer. No questions anywhere: not measured (the question-heading signal already prices that). |
| Question-shaped headings | GEO_GRADE_QUESTION_HEADINGS | 1.0 | ≥ 4 question(s) → 1.0; ≥ 2 question(s) → 0.75; ≥ 1 question(s) → 0.5; below every band → 0 | Our judgment: one lone question heading is a token gesture; a working FAQ carries four or more. |
| Lists and tables | GEO_GRADE_ANSWER_STRUCTURE | 0.75 | ≥ 3 element(s) → 1.0; ≥ 1 element(s) → 0.7; below every band → 0 | Our judgment: structured fragments are what AI engines lift verbatim, but weaker evidence than a direct answer. |
| Freshness signals | GEO_GRADE_FRESHNESS | 0.5 | age of the newest machine-readable date (JSON-LD dateModified/datePublished, dated meta tags, a content Last-Modified header, sitemap lastmod): ≤ 90 day(s) → 1.0; ≤ 365 day(s) → 0.75; ≤ 730 day(s) → 0.5; past every band → 0.25; no machine-readable date at all → not measured, so the signal leaves the denominator entirely | Our judgment. Scoring the absence of a date was a perverse incentive: publishing nothing scored better than publishing an honest three-year-old date, so a silent site outranked a candid one. Nothing measured now means nothing judged — the report still says plainly that no date signal was found. A Last-Modified or lastmod stamped within 60 s of the response's own Date header is a request-time stamp, not a content date, and is ignored; so is a Last-Modified header on a near-empty page with no in-document date, where it is usually a cache-fill timestamp. |
| llms.txt quality | GEO_GRADE_LLMS_QUALITY | 0.5 | H1 title 0.3 + markdown links 0.3 + "##" sections 0.2 + 200+ characters 0.2 | The llms.txt spec shape; the mark weights are our judgment. Absent or unfetched file: not measured here (the binary finding prices absence). Lightest weight, per the evidence above. |
Why these weights: structured-data depth carries the most because those fields are what assistants lift verbatim; consistency, answerability and question headings are the signals that let an engine merge the pages into one entity and quote it; lists and tables, freshness and llms.txt are real but softer. The live access test is not on that ladder at all: it only ever takes points off, and only when a crawler was actually turned away — a site nothing was refused on is not measured on it and is not credited for it either. The split is our judgment and is stated here as exactly that.
Honesty rules
What we refuse to do
These rules are enforced in the code, not promised in the copy.
Couldn't verify means no judgment
When a check could not gather its evidence, it stays silent instead of guessing — in all three layers. A pillar check whose probe never completed emits no finding. A graded signal that could not be measured leaves the scoring denominator entirely, so an unmeasured signal can neither help nor hurt. A URL the probe budget never reached is reported as unchecked — never counted as broken.
No invented numbers
Every count, size and timing in a finding is measured from the bytes the audit actually fetched, and the finding names its evidence (paths, sizes, character counts). Sampled and extrapolated values are labeled as exactly that. The benchmark median on reports renders only once at least 20 distinct real sites have been audited — below that, nothing renders, because a median of a handful of sites is noise.
The checks never ask a model
Findings and scores come from deterministic code with no language model in the loop. A model only phrases the action plan (and, on Pro, fix hints), grounded solely in the findings it is handed — it cannot add a finding or move a score, and if it fails, a deterministic plan built straight from the findings renders instead.
One model, one moment
The Pro AI-visibility check asks one language model the kinds of questions your customers ask and records whether your business appears. It is labeled as one model at one moment — a snapshot, never a guarantee of rankings or AI mentions. Scores describe what the checks found on the day they ran, nothing more.
Accountability
Report an inaccurate finding
If a finding on your report is wrong, email the report link — a person re-runs the check and answers.
Use the subject line "AUDIT ACCURACY". Because the audit is deterministic, the disputed check re-runs against your site exactly as it ran the first time — the answer you get is the re-run's evidence, not an opinion.