GEO Scanner Reference
This page details the scoring logic behind the GEO Scanner in the Universal Visual Editor. It is intended for developers and content strategists who want to understand precisely why a page received a particular score and what changes will move specific signals.
For a conceptual overview of the scanner and how to run it, see the UVE documentation.
Note: The scanner analyses rendered HTML. Pages that rely entirely on client-side rendering may return a partial or inaccurate score because content injected after the initial HTML response is not available to the scanner. Server-side rendering is recommended for pages intended to be discoverable by AI answer engines and search crawlers.
Score Calculation#
The composite GEO score (0–100) is a weighted sum of four category scores:
GEO Score = Citability × 0.30 + Structured Knowledge × 0.25 + Content Authority × 0.25 + Discoverability × 0.20
Each category score is the simple average of its five signal scores (each 0–100). The composite is rounded to the nearest integer.
Issue severity is assigned per signal: a score of 0 is high; below 40 is medium; below 70 is low. Issues are surfaced sorted high → medium → low.
8 of 20 signals are binary pass/fail with no partial credit: author attribution, publish date, HTTPS, indexability, language declaration, and the zero-count floor of citations, statistics, and quotations. A page that is "almost there" on one of these still scores 0 on that signal.
Citability (30%)#
Signals in this category are scanned over the detected mainContent region only. Navigation, header, footer, sidebar, and ARIA landmark regions are stripped before analysis.
| Signal | What it checks | Score bands |
|---|---|---|
| Source citations | <a href> elements in main content whose hostname differs from the page's own | 0 → 0; 1 → 20; 2 → 40; 3 → 55; 4 → 70; 5 → 80; 6 → 90; 7+ → 100 |
| Statistics / data | Regex over main-content text matching: percentages, currency values, year ranges ("since 2024"), numbers with thousands separators, "X million/billion", "5x" multipliers — deduplicated across matches | 0 → 0; 1 → 20; 2 → 40; 3 → 55; 4 → 70; 5 → 76; 6 → 82; 7 → 88; 8 → 94; 9+ → 100 |
| Expert quotations | <blockquote> count, plus curly-quote spans of ≥20 characters followed by an em/en-dash and a word, or followed by: said, says, according to, wrote, noted, explained, stated | 0 → 0; 1 → 40; 2 → 65; 3–4 → 85; 5+ → 100 |
| Content depth | Word count of main content (whitespace-split) | <100: linear 0–20; <300: 20–40; <600: 40–65; <1000: 65–80; <2000: 80–95; 2000+: 100 |
| Answer structure | Two sub-checks: (a) whether the first <p> is ≥40 characters ("has intro"); (b) H1–H6 walked in document order, any level jump greater than 1 flagged as "partial" | hasIntro: +50; hierarchy valid: +50 / partial: +25 / missing: +0 |
Structured Knowledge (25%)#
| Signal | What it checks | Score bands |
|---|---|---|
| Schema present | Count of <script type="application/ld+json"> blocks in <head> or <body> | 0 → 0; 1 → 60; 2–3 → 80; 4+ → 100 |
| Schema validity | Each block must parse as valid JSON and contain a string @type field | (valid blocks / total blocks) × 100 |
| Schema completeness | The first block whose @type matches a type with a recommended-property checklist (see below) | (present properties / total recommended) × 100; matched type with no checklist = 50 |
| Heading hierarchy | H1 count and level-skip detection across the full <body> | Exactly one H1: +40 / more than one: +20; two or more headings present: +30 / one heading: +15; no skipped levels (with ≥2 headings): +30 / any skip: +10 |
| Internal linking | <a href> in main content whose hostname matches the page's own (or non-http hrefs); excludes # anchors and javascript: links | 0 → 0; 1–2 → 30; 3–5 → 60; 6–10 → 80; 11+ → 100 |
Recognised Schemas#
The scanner accepts the following 26 @type values for validity and type-matching:
Article, NewsArticle, BlogPosting, TechArticle, ScholarlyArticle, Report, WebPage, AboutPage, FAQPage, QAPage, CollectionPage, ProfilePage, ItemPage, Product, SoftwareApplication, Organization, Person, Event, Course, Recipe, HowTo, Review, LocalBusiness, BreadcrumbList, VideoObject, ImageObject.
Recommended-Property Checklists#
The Schema completeness signal works by finding the first JSON-LD block on the page whose @type is one of the 26 recognised types, then checking how many of that type's "recommended" properties are present and scoring (present / recommended) × 100.
This checklist only exists for 13 of those 26 types — the content-heavy ones like Article, WebPage, Product, FAQPage, etc. The other 13 (e.g., ScholarlyArticle, SoftwareApplication, Recipe, Review, etc.) are accepted as valid schema, but no one has defined what "complete" looks like for them. Instead of measuring their completeness, the scanner awards a flat 50 rather than penalizing the page for having schema that happens to be a less common type.
| Type | Recommended properties |
|---|---|
| Article, NewsArticle, BlogPosting | headline, author, datePublished, dateModified, image, publisher, description |
| TechArticle | headline, author, datePublished, dateModified, image, description |
| WebPage | name, description, url, datePublished, dateModified |
| Product | name, description, image, offers, brand, sku, review |
| Organization | name, url, logo, description, sameAs |
| Person | name, url, image, jobTitle |
| FAQPage | mainEntity |
| HowTo | name, step, description, image |
| Event | name, startDate, endDate, location, description, image |
| BreadcrumbList | itemListElement |
| VideoObject | name, description, thumbnailUrl, uploadDate, duration |
Content Authority (25%)#
| Signal | What it checks | Score bands |
|---|---|---|
| Author attribution | Any of: a author field in JSON-LD; <a rel="author">; an element matching .author, .byline, [class*="author"], [class*="byline"], or [itemprop="author"] with non-empty text; <meta name="author"> with content | Found → 100; not found → 0 (binary) |
| Publish date | First match of: JSON-LD datePublished or dateModified → <time datetime> → <meta property="article:published_time"> or article:modified_time → a recognisable date pattern in visible body text | Any match → 100; no match → 0 (binary) |
| Image alt text | All <img> elements in <body>, checking for a non-empty alt attribute | (images with alt / total images) × 100; zero images on the page = 100 (not penalised) |
| Content uniqueness (title) | <title> checked against a generic blocklist (home, homepage, welcome, index, untitled, blog, news, page, default); length target 30–60 characters | Non-generic: +40; length 30–60 chars: +40 / 15–29 or 61–80 chars: +20; any non-empty title: +20 |
| HTTPS | Whether the page is served over HTTPS | True → 100; false → 0 (binary) |
Discoverability (20%)#
| Signal | What it checks | Score bands |
|---|---|---|
| Meta description | <meta name="description"> content length | 120–160 chars → 100; 80–119 or 161–200 → 70; 50–79 or >200 → 40; other non-empty → 20; missing → 0 |
| Canonical tag | <link rel="canonical"> resolved URL compared to the page's final (post-redirect) URL | Matches → 100; present but mismatched → 60; missing → 0 |
| Indexability | <meta name="robots"> content checked for a noindex token | Indexable → 100; noindex present → 0 (binary) |
| Open Graph tags | Required set: og:title, og:description, og:image — each must carry non-empty content | (tags present / 3) × 100 |
| Language declaration | lang="..." attribute on the <html> element (matched against raw HTML) | Present → 100; absent → 0 (binary) |