Your crawl reports 4,000 product pages with the wrong canonical. The audit tells you what is wrong. It doesn't tell you why. Was it the template, a CMS field, a routing rule, a CDN setting, a plugin, or a script injected after page load? Fix the wrong layer and the problem comes back with the next deployment.
SEO debugging is the process of moving from a reported symptom to a reproducible problem, narrowing the scope, testing causes one at a time, and identifying the system that produced the behavior. The goal isn't to find an SEO issue. It's to find its root cause.
SEO Auditing vs. SEO Debugging
Audits and debugging often start from the same crawl data, but they do different jobs:
| SEO auditing | SEO debugging |
|---|---|
| Finds problems across the site | Explains one problem in depth |
| Identifies patterns | Tests patterns |
| Reports affected URLs | Determines why those URLs are affected |
| Measures severity | Isolates the failing layer |
| Answers "what is wrong?" | Answers "why is it wrong, and where?" |
Debugging also differs from SEO incident investigation. An investigation explains a traffic or visibility change across a site and weighs external factors like algorithm updates. Debugging works at the level of a page, template, or system: why does this URL return this output?
Step 1: Write a Testable Symptom
Don't start by changing code. Start by describing exactly what's happening.
SEO is broken on product pages.
Product pages under /products/ return a canonical pointing to their category page since the October 3 deployment. Category and blog pages are correct.
Record the affected URL, the expected and actual behavior, when it was found, the page type, and the relevant element: status, canonical, robots directive, title, or heading. A precise symptom is half the diagnosis.
Step 2: Reproduce It, and Know How You Requested It
A problem you can't reproduce is hard to debug. Request the affected URL again, then two or three more affected URLs. If the behavior repeats, you have a reproducible condition.
How you make the request matters as much as the result. The same URL can return different output depending on:
- User agent
- Cache state
- Cookies and login
- Request location
- Accept-Language
- Raw vs. rendered
Use requests you can repeat exactly and share with a developer. Bypass the cache with a unique query string, and test as Googlebot as well as a browser:
curl -sI "https://www.example.com/products/oak-desk/?nocache=1" curl -s -A "Googlebot" "https://www.example.com/products/oak-desk/" | grep -i canonical
Step 3: Find the Scope
The single most useful debugging question is: what's the smallest unit that explains every affected URL?
| Scope | Investigate first |
|---|---|
| One URL | Page-specific CMS fields, manual overrides, URL parameters, malformed markup |
| One template | Template logic, shared components, fallback values, template variables |
| One directory | Routing, rewrite rules, middleware, directory-level CDN or server config |
| Site-wide | Global config, framework changes, CDN, reverse proxy, robots.txt, deployments |
Scope prevents wasted effort. There's no point editing 1,000 URLs by hand if one template generates all 1,000 problems.
Step 4: Compare Affected and Unaffected Pages
Controlled comparison is the fastest way to narrow a problem. Pick one affected URL and one similar unaffected URL. Compare two products, not a product and the homepage. Then look for the first meaningful difference:
| Element | Affected product | Unaffected product |
|---|---|---|
| Template | product-v2 | product-v2 |
| Created | After Oct 3 | Before Oct 3 |
| CMS category field | Empty | "Desks" |
| Canonical | /desks/ | /products/walnut-desk/ |
Same template, different result, and the difference lines up with an empty CMS field. That's a lead: the template may fall back to a category URL when a field is missing.
At scale, the same idea works with groups. If 300 blog posts have broken canonicals and 300 category pages don't, the category pages are your control group. Ask whether they share the same SEO component, data source, deployment, and server response.
Step 5: Trace the Request Path
Every page passes through several layers before a crawler sees it, and any of them can change SEO output. Debugging means finding which layer introduced the wrong value:
- 1CDN and edgeRedirects, cached responses, bot protection, geo or cookie rules, header injection.
- 2Reverse proxy and serverRewrite rules,
X-Robots-Tagheaders, trailing-slash and HTTPS normalization. - 3Application and routingRoute matching, middleware, URL generation, status codes.
- 4TemplateHow titles, canonicals, robots meta, headings, and schema are built.
- 5CMS dataField values, fallbacks, plugins, localization data.
- 6Client-side JavaScriptFramework hydration, tag managers, A/B testing tools, consent scripts.
Work through the layers using elimination. Each check should rule a layer in or out:
| If you observe | You can eliminate |
|---|---|
| The wrong value is already in the raw HTML response | Client-side JavaScript as the sole cause |
| The correct value is in raw HTML but wrong after rendering | Server, template, and CMS |
| Origin server returns correct output, CDN doesn't | Application and template |
| Only one template is affected | Most global infrastructure causes |
| Same template, correct for some CMS entries | Template logic alone; check the data |
Step 6: Read the HTTP Response
Some problems exist before any HTML is parsed. Check the status code, redirect chain, Location, X-Robots-Tag, Link (which can carry a canonical), cache headers, and Vary.
HTTP/1.1 200 OK X-Robots-Tag: noindex Vary: User-Agent, Cookie Cache-Control: max-age=86400
This response tells a lot of the story. The page is noindexed at the header level, not in the HTML, so the template is probably innocent. And Vary: User-Agent, Cookie means different visitors can receive different versions, which explains why one person sees the problem and another doesn't.
Step 7: Inspect the HTML Element Itself
Look at the exact element involved, in the raw source (view-source:), not only in the DevTools Elements panel, which shows the DOM after JavaScript has run.
<link rel="canonical" href="https://www.example.com/desks/">
For that element, confirm whether it exists, how many times it appears, what value it holds, whether that value is consistent across affected pages, and what unaffected pages contain instead. Two canonicals or two robots tags on one page often point straight to two components writing the same element.
Step 8: Check Rendered Output Only When It's Relevant
Rendering isn't the first step in every investigation. If the raw HTML already contains the wrong value, the cause is server-side. Rendering matters when the value is created, changed, or removed after the page loads.
Common client-side culprits are less obvious than framework code: a tag manager injecting meta tags, an A/B testing tool rewriting titles, or a script swapping canonicals for tracking. Compare raw and rendered output for the specific element, and use Search Console's URL Inspection live test to see what Google renders.
Step 9: Diff What Changed
Most SEO bugs have a change somewhere in their history. The fastest route to a cause is often a diff, not a theory:
- The deployment diff for template and routing files
- Version history for robots.txt and redirect rules
- CMS revision history for affected entries
- CDN and server configuration changes
- Plugin, theme, or framework updates
- A crawl comparison from the last known good state
A change that happened before the problem is a candidate cause, not a confirmed one. If you have crawl history, comparing the last good crawl with the broken one often shows exactly which outputs changed and on which templates.
Step 10: Test One Hypothesis at a Time
Changing three things and checking if the problem disappeared tells you nothing about which change mattered. Write a hypothesis, test it, and keep or discard it:
For example: "The template falls back to the category URL when the category field is empty." Test it by filling the field on one affected product in staging. If the canonical corrects itself, and clearing the field on an unaffected product breaks it, you've confirmed the cause in both directions.
Intermittent Problems
If the same URL is sometimes right and sometimes wrong, the problem isn't in static template logic. Request it repeatedly, with and without cache, as different user agents, and from different locations.
Always wrong
- Deterministic
- Template, data, or config
- Easiest to isolate
Sometimes wrong
- Caching or stale edge copies
- Load-balanced servers out of sync
- Personalization or geo rules
- Third-party scripts or race conditions
Now always right
- Temporary or already fixed
- Check crawl history and logs
- Confirm before closing
Document the Root Cause
Write the conclusion as an evidence chain, so someone else can follow how you got there:
| Step | Evidence |
|---|---|
| Symptom | Product pages canonicalize to category URLs |
| Scope | Only products created after Oct 3 |
| Comparison | Affected products have an empty category field |
| Layer | Wrong value present in raw HTML; CDN and JavaScript ruled out |
| Change | Oct 3 import stopped populating the category field |
| Test | Filling the field corrects the canonical; clearing it breaks a good page |
| Root cause | Template fallback plus a broken import mapping |
Note the root cause has two parts. The import broke the data, and the template's fallback turned missing data into a harmful canonical. Fixing only the import leaves the next empty field waiting to cause the same bug, so the prevention step is to change the fallback too.
Verify the Fix at Scale
A fix isn't confirmed because one page looks right. Rerun the exact request that exposed the problem, then widen:
- The original failing URL now returns the expected output
- Other affected URLs across the template or directory are fixed
- Previously unaffected URLs are still unaffected
- The cache has been purged, so you're not testing a stale copy
The third check matters most. A fix that solves one problem and creates another isn't a fix. A follow-up crawl compared against the broken state confirms the whole template, not just the page you tested by hand.
Common Debugging Mistakes
Fixing symptoms page by page
Manual edits hide a template-level cause until the next release.
Trusting only the browser
It shows the final state, not what the server returned.
Trusting only raw HTML
Tag managers and testing tools can change values after load.
Testing a cached copy
A stale edge response makes a fix look broken, or a bug look fixed.
Changing several things at once
You lose the ability to say which change worked.
Stopping at the first cause
Many bugs need two conditions. Fix both, or it returns.
Start From Crawl Patterns
A crawler gives debugging its starting evidence. Instead of 1,250 duplicate titles to fix, break the finding down: are they in one directory, one template, one CMS field? Do the duplicates follow a formula? Did the pattern appear between two crawls?
The useful question is always: what do the affected URLs have in common, and what do the unaffected ones do differently? SiteAuditLint groups issues across crawled URLs and keeps audit history, so you can see which templates are affected and when the pattern began before you open a single file.
Symptom, scope, layer, hypothesis, root cause, verified fix.