A technical SEO audit can report 97% health and still miss the one defect that should stop a release.
An accidental noindex
on a key template, an empty application shell, a canonical pointing at a redirect, or a production firewall blocking required resources does not become less serious because 43 other checks are green.
That is the problem with audit scores: they compress different risks, evidence levels, and unknowns into one reassuring number.
I would rather have an audit that answers six harder questions:
This article presents the decision model behind an open technical SEO protocol we use at Edikka. It contains 44 replayable checks across 13 domains, but the number is not the point. The point is to make every conclusion falsifiable and every release decision explicit.
It does not predict rankings, traffic, rich results, or AI citations.
Every applicable check needs two separate classifications:
Then apply a rule that people can challenge:
IF an applicable Blocking check is Non-compliant
OR an applicable Blocking check is Not tested
THEN NO-GO
ELSE IF a Major non-compliance remains
THEN ARBITRATION REQUIRED
ELSE GO WITH RESERVATIONS
This is an Edikka governance rule, not an industry standard. A team may choose stricter thresholds. What matters is that the rule is written before the result is known.
The uncomfortable part is intentional: Not tested is not the same as Compliant.
Technical audits often merge facts observed from outside a site with private states that only the site owner or search engine can reveal.
Keep them separate.
| Evidence level | Examples | What it can establish | What it cannot establish |
|---|---|---|---|
| Public | HTTP response, robots.txt , source HTML, rendered DOM, sitemap, JSON-LD |
||
| What a documented client observed at a documented time | What Google crawled, selected, or indexed | ||
| Search Console | URL Inspection, Google-selected canonical, Pages and Core Web Vitals reports | The state reported for the property and inspected sample | The single cause of a ranking change |
| Server logs | Verified crawler requests, response codes, frequency, bytes | An interaction received by the infrastructure | How the fetched content was subsequently used |
| Configuration | CDN, WAF, CMS, deployment and routing rules | The configured intent, once tested | That every edge, cache key, and route behaves identically |
A public audit can establish that a page declares a coherent canonical. It cannot honestly claim that Google selected that canonical without Search Console evidence.
That boundary improves the audit. It turns “Google has indexed the right page” into two testable statements:
“SEO-friendly” is too vague to debug. A URL passes through distinct stages:
| Stage | Diagnostic question | Useful evidence |
|---|---|---|
| Discovery | Does a public path lead to the URL? | HTML links, sitemap, referring URLs in logs |
| Crawling | May the crawler request it? | |
robots.txt , HTTP response, verified logs |
||
| Rendering | Does critical content exist after execution? | Source HTML, rendered DOM, URL Inspection |
| Indexing | Which URL and content did the engine retain? | URL Inspection and indexing reports |
| Serving | Is the page selected for this query and context? | Search performance and observed results |
A 200
response proves that the server returned a successful representation. It does not prove discovery, indexation, or selection for a query.
A sitemap declares candidate URLs. It is not an indexation certificate.
Do not rely on a HEAD
request alone. Applications, CDNs, and firewalls can handle HEAD
and GET
differently.
Record the final status, complete redirect count, effective URL, and duration:
TARGET_URL="https://example.com/important-page"
curl --location --silent --show-error --output /dev/null \
--write-out 'status=%{http_code}\nredirects=%{num_redirects}\nfinal=%{url_effective}\ntime=%{time_total}s\n' \
"$TARGET_URL"
Replay more than the happy path:
The expected result is not “everything returns 200.” The expected result is that each case returns the status and destination intended for that resource.
Many technical SEO failures begin with the right intent and the wrong mechanism.
| Intent | Primary mechanism | Evidence to retain | Dangerous shortcut |
|---|---|---|---|
| Reduce crawling | |||
robots.txt for compliant crawlers |
|||
| Parsed rule plus logs | Treating it as access control | ||
| Remove a page from an index | Accessible noindex directive |
||
| Source/header plus URL Inspection after recrawl | Blocking the page before the crawler can read noindex |
||
| Consolidate duplicates | Redirect or rel="canonical" , depending on the case |
||
| Converging signals plus selected canonical | Canonicalising genuinely different pages | ||
| Protect private data | Server-side authentication and authorisation | Anonymous request denied with no sensitive body | Publishing the data and hiding it from robots |
Google's robots directives documentation explicitly notes that crawlers must be allowed to access a page to read its noindex
rule. The Robots Exclusion Protocol also defines crawling rules, not a security boundary.
Avoid arguments such as “React is bad for SEO” or “SSR solves SEO.” Framework labels are not evidence.
For every priority template, compare at least:
This small Playwright probe records a few invariants from the rendered page:
import { chromium } from "playwright";
const url = process.argv[2];
if (!url) throw new Error("Usage: node inspect-page.mjs <url>");
const browser = await chromium.launch();
const page = await browser.newPage();
const response = await page.goto(url, { waitUntil: "networkidle" });
const evidence = await page.evaluate(() => ({
title: document.title,
h1: [...document.querySelectorAll("h1")].map((node) =>
node.textContent?.trim(),
),
canonical:
document.querySelector('link[rel="canonical"]')?.getAttribute("href") ?? null,
robots:
document.querySelector('meta[name="robots"]')?.getAttribute("content") ?? null,
crawlableLinks: document.querySelectorAll("a[href]").length,
jsonLdBlocks: document.querySelectorAll(
'script[type="application/ld+json"]',
).length,
mainTextCharacters:
document.querySelector("main")?.textContent?.trim().length ?? 0,
}));
console.log(JSON.stringify({
requestedUrl: url,
finalUrl: page.url(),
status: response?.status() ?? null,
...evidence,
}, null, 2));
await browser.close();
This does not reproduce Googlebot or prove indexation. It provides replayable browser evidence that can be compared with the raw response and Search Console.
The complete protocol is larger, but these seven checks expose why severity matters more than a percentage:
| Gate | Evidence expected | Typical severity |
|---|---|---|
| Final public URL returns the intended response after a controlled redirect chain | URL, timestamp, final status, effective URL | Blocking |
Required resources are crawlable and no accidental noindex exists |
||
robots.txt , headers, source HTML |
||
| Blocking | ||
| Critical content exists in source HTML or observable rendered output | Raw response, rendered DOM, inspection | Blocking |
| Canonical signals converge | Final URL, canonical, sitemap, internal links | Major |
| Important pages receive crawlable HTML links | Source URL, destination, anchor, status | Major |
| JSON-LD describes visible entities without invented claims | Parsed graph plus visible-content comparison | Major |
| Field Core Web Vitals are read separately from laboratory scores | LCP, INP, CLS at the 75th percentile by device/group | Major |
Notice the last row: a Lighthouse score is diagnostic laboratory evidence. It is not field evidence, and a score of 100 does not guarantee good real-user Core Web Vitals.
If a conclusion cannot be exported, compared, and replayed, it will be difficult to govern after the next deployment.
A minimal result might look like this:
{
"id": "TS25",
"url": "https://example.com/product/42",
"observed_at": "2026-08-25T13:00:00Z",
"status": "Non-compliant",
"severity": "Blocking",
"access": "Public",
"evidence": {
"raw_html": "main content absent",
"rendered_dom": "main content present after API response"
},
"limitation": "Local Chromium is not Google URL Inspection",
"owner": "Frontend platform",
"decision": "NO-GO"
}
The limitation
field is as important as the observation. It prevents a local browser result from silently becoming a claim about Google.
Not every audit check belongs in CI. The stable ones often do:
noindex
changes;Use representative fixture URLs for each important template. Preserve the timestamped report as a build artifact. Link it to the deployment. Replay it after release.
Automation detects a defined regression. It does not own the business exception or the final release decision. Every exception still needs an owner, a reason, and an expiry date.
The full method currently contains 44 checks across 13 domains: HTTP, robots, indexation, canonicalisation, sitemaps, internal discoverability, URL spaces, internationalisation, JavaScript, semantic HTML, structured data, performance, edge behaviour, CI, and crawler policies.
The assets are open under CC BY 4.0:
Primary references used by the protocol include RFC 9110 for HTTP semantics, RFC 9309 for the Robots Exclusion Protocol, Google's canonicalisation guidance, JavaScript SEO documentation, and the Core Web Vitals threshold methodology.
The question I would ask before your next deployment is not:
What technical SEO score did we get?
It is:
Which applicable failure would stop this release, and do we have the evidence to detect it?
What is the technical SEO invariant that should be a NO-GO in your stack?
AI-assistance disclosure: this DEV edition was adapted from my original Edikka protocol with AI assistance for structure and English editing. The audit model, controls, evidence boundaries, examples, verification, and publication decision remain my responsibility.