One site, eight audit issues reproduced. Each section maps to one rule so it can be fixed and verified independently.
<h2>, not an <h1>.
The page has zero <h1> tags. Audit tools will flag this as h1_missing (Error).
Audit rule: h1_missing | This issue is live on this page (index.html). View source — the first heading is <h2>, not <h1>.
<h1> that states the page's primary topic.
A missing H1 removes the strongest on-page topical signal from Google, AI Overviews, and LLM retrieval systems.
Imagine a newspaper article with no headline — just subheadings and body copy.
Readers have to guess what the article is about.
Google, AI Overviews, and LLMs do the same thing: they look at the <h1> first to understand the page's topic.
Without it, all three have to infer the topic from body text — which is slower, less reliable, and often wrong.
View source on this page. The opening heading is:
<h2 style="font-size:1.6rem">SEO Issues Lab — 8 Technical Issues Demo</h2> ← h2, not h1 — the page has zero <h1> tags <!-- Fix: change h2 to h1 --> <h1>SEO Issues Lab — 8 Technical Issues Demo</h1>
If a page has <h1></h1> (tag exists but contains no text), audit tools flag it as h1_empty (Warning). Both errors remove the primary topical signal.
<h1></h1> ← h1_empty (warning) — tag present but blank <!-- page with no <h1> at all --> ← h1_missing (error) — this page reproduces this
| Channel | Impact |
|---|---|
| SEO | H1 is the strongest on-page topical signal. Google uses it to confirm the page's topic matches the query. Missing H1 weakens relevance scoring for all target keywords. |
| AEO (AI Engines) | Answer engines weight H1s heavily when attributing facts to pages. Pages without H1 are less likely to be cited as the source for direct answers. |
| GEO (Generative Engine) | LLMs use H1 as the primary topic signal when deciding whether a page is authoritative on a subject. No H1 = weaker topical authority in retrieval. |
Audit rule: links3xx | Demo page: /link-issues.html — four internal links whose href values redirect before reaching the final destination.
/old-page and that URL returns a 301 or 302 to /new-page,
every visitor and crawler pays the cost of two HTTP requests per click.
Each redirect hop dilutes the PageRank passed through internal links.
Every internal link is a direct road to a destination. A redirect turns that road into a detour sign:
"this way doesn't work — follow the arrow instead."
Every detour costs crawl budget, adds latency, and leaks link equity.
The fix is simply updating the href to point directly to the final URL.
<!-- link-issues.html — all four hrefs redirect before reaching their destination --> <a href="/old-about">About Us</a> ← 301 → /about.html <a href="/old-services">Services</a> ← 302 → /services.html <a href="/legacy-contact">Contact</a> ← 301 → /link-issues.html <a href="/temp-landing">Special Offer</a> ← 302 → /about.html <!-- Fix: update each href to the final destination URL --> <a href="/about.html">About Us</a> <a href="/services.html">Services</a> <a href="/link-issues.html">Contact</a> <a href="/about.html">Special Offer</a>
| Channel | Impact |
|---|---|
| SEO | Each 3XX adds a PageRank tax. Internal link equity is partially lost at every redirect hop. Crawl budget is spent on intermediate redirect URLs instead of discovering new content. |
| AEO / GEO | Each redirect hop adds latency. AI crawlers with tight timeouts may abandon the chain before reaching the final page content. |
Audit rule: redirect_temporary |
Three 302/307 redirects in the _redirects file:
/sale (302), /promo (302), /flash-offer (307).
A 301 tells the post office: "I've moved permanently — update my address in your system." A 302 says: "I'm away temporarily — forward mail for now, but don't change anything." If you've moved permanently but used a 302, Google keeps the old URL as the canonical. The destination page never inherits the source page's ranking history or link equity.
| Code | Name | PageRank passed | Correct use case |
|---|---|---|---|
301 | Moved Permanently | ~99% (confirmed by Google) | Permanent URL change |
302 | Found / Temporary | Withheld by Google | Genuine temporary redirect (A/B test, maintenance) |
307 | Temporary Redirect | Withheld | Same as 302 but preserves HTTP method (POST → POST) |
303 | See Other | Withheld | After form POST — redirect to GET result page |
# _redirects file — three temporary redirects (should be 301) /sale /services.html 302 ← sale ended months ago — still using 302 /promo /services.html 302 ← campaign over — PageRank not flowing to /services /flash-offer /link-issues.html 307 ← permanent page, wrong redirect code # Fix: change 302/307 to 301 /sale /services.html 301 /promo /services.html 301 /flash-offer /link-issues.html 301
| Channel | Impact |
|---|---|
| SEO | 302/307 don't consolidate link equity. The old URL retains ranking credit; the destination page never inherits it. Google may continue indexing and ranking the source URL. |
| AEO | AI engines weight permanent redirects higher when choosing the authoritative URL to cite. A 302 signals "check back at the original," reducing the destination's authority. |
| GEO | LLM retrieval systems treat permanently redirected URLs as more stable citations. 302 sources are weighted lower because they signal transient URLs. |
Audit rule: extlinks4xx | Demo page: /about.html — four external links pointing to URLs that return 4XX errors.
External links are citations — you're telling readers "this external source confirms what I'm saying." If that source returns 404, it's like citing a book that's been pulled from all libraries and destroyed. It makes your page look stale and damages its credibility as a quality resource.
Common real-world causes:
| Cause | Example |
|---|---|
| Linked site shut down | A partner or vendor that closed; their domain expired |
| Article deleted or moved | External blog post was removed; URL now 404s |
| Domain restructured | External site changed URL patterns; old links 404 |
| Added years ago, never audited | Links written in 2019 to resources that no longer exist |
<!-- about.html — four external links to dead URLs --> <a href="https://defunct-seo-blog.example/seo-guide-2019">SEO best practices</a> ← 404 <a href="https://closed-partner.example/case-studies">Case studies</a> ← 404 <a href="https://old-agency.example/our-work">Our work</a> ← 404 <a href="https://httpstat.us/404">Industry report</a> ← guaranteed 404 <!-- Fix options --> <!-- A: Remove the link entirely if no replacement exists --> <!-- B: Replace href with a live, equivalent resource --> <!-- C: Add rel="nofollow" as an interim measure -->
| Channel | Impact |
|---|---|
| SEO | Dead outbound links are a content quality signal. Google interprets pages with many dead external links as poorly maintained. Crawl budget is wasted following dead outbound links. |
| AEO / GEO | AI engines verify linked sources when generating answers. Dead external links undermine the page's credibility as a citable source for AI-generated responses. |
Audit rule: css3xx | Demo page: /services.html — two <link rel="stylesheet"> hrefs that redirect before serving the actual CSS.
<link rel="stylesheet" href="/old-style.css"> redirects to /css/main.css,
the browser fires two HTTP requests per stylesheet — doubling render-blocking time.
CSS is render-blocking, so this directly delays first paint and worsens Core Web Vitals (LCP, FCP).
CSS is render-blocking — the browser freezes all page rendering until every stylesheet is loaded. A redirected CSS file doubles the wait: first it fetches the old URL, gets a "go here instead" response, then fetches the actual file. Users see a blank or unstyled page for longer on every page load. Google measures this and it directly impacts Core Web Vitals scores.
<!-- services.html <head> — both stylesheet hrefs redirect --> <link rel="stylesheet" href="/css/old-style.css"> ← 301 → /css/main.css <link rel="stylesheet" href="/css/legacy-theme.css"> ← 302 → /css/main.css <!-- _redirects file that causes this --> /css/old-style.css /css/main.css 301 /css/legacy-theme.css /css/main.css 302 <!-- Fix: update the href in HTML to point directly to the final URL --> <link rel="stylesheet" href="/css/main.css">
| Channel | Impact |
|---|---|
| SEO | Redirected CSS delays first paint and increases render-blocking time. Hurts LCP and FCP — both are Google ranking factors via Core Web Vitals. CSS redirect adds ~100–300ms on every page load. |
| AEO / GEO | AI crawlers that render pages to extract structured content may receive unstyled or partially rendered snapshots when CSS is delayed, causing layout-dependent extraction failures. |
Audit rule: hreflang_xdefault |
Three pages form a hreflang cluster — each missing the x-default entry:
EN,
FR,
ES.
hreflang="x-default" tells Google which URL to serve when no language/region tag matches the user's locale.
Without it, Google guesses — and may serve the wrong language page to users in countries not covered by specific tags.
Imagine a website with English, French, and Spanish versions.
A user in Japan visits — which version should they see?
x-default answers exactly that: "when no locale matches, show this page."
Without x-default, Google picks an arbitrary language version for all unmatched users.
<!-- All 3 pages have this cluster — structurally valid, but missing x-default --> <link rel="alternate" hreflang="en" href="https://seo-issues-lab.pages.dev/hreflang-en"> <link rel="alternate" hreflang="fr" href="https://seo-issues-lab.pages.dev/hreflang-fr"> <link rel="alternate" hreflang="es" href="https://seo-issues-lab.pages.dev/hreflang-es"> MISSING: <link rel="alternate" hreflang="x-default" href="..."> ← ❌ no fallback defined
<!-- Add x-default to every page in the cluster — typically points to the default language --> <link rel="alternate" hreflang="en" href="https://example.com/hreflang-en"> <link rel="alternate" hreflang="fr" href="https://example.com/hreflang-fr"> <link rel="alternate" hreflang="es" href="https://example.com/hreflang-es"> <link rel="alternate" hreflang="x-default" href="https://example.com/hreflang-en"> ← ✅ EN as fallback
| Channel | Impact |
|---|---|
| SEO | Without x-default, Google makes an arbitrary locale choice for unmatched users. Visitors from Japan, Brazil (pt-BR), or any uncovered market may see the wrong language — hurting CTR, engagement, and conversions. |
| AEO / GEO | AI engines use x-default as the canonical fallback when no locale match exists. Missing x-default causes AI engines to arbitrarily choose a locale page as the "main" source for generic queries. |
Audit rule: viewport_device_width | Demo page: /services.html — the viewport is set to a fixed pixel width (width=1024) instead of width=device-width.
<meta name="viewport" content="width=device-width, initial-scale=1"> tells mobile browsers
to render at the device's actual screen width — enabling responsive design.
A fixed width like width=1024 forces the browser to render at 1024px regardless of screen size,
then scale it down — making text tiny and buttons untappable on phones.
Think of the viewport as telling a printer "match the paper to what you're printing."
A fixed width=1024 on a 375px iPhone renders the full desktop layout at 37% zoom.
Text is unreadable. Buttons are untappable. Google's mobile-first indexing penalises this directly.
| Viewport value | Result on mobile | Status |
|---|---|---|
width=device-width, initial-scale=1 | Responsive — adapts to actual screen width | ✅ Correct |
width=1024 | Fixed 1024px layout, scaled down to screen size | ❌ Broken (this site) |
width=320 | Fixed 320px — broken on any phone wider than 320px | ❌ Broken |
| Missing viewport tag entirely | Desktop layout rendered at ~980px, then scaled down | ❌ Broken |
initial-scale=1 only (no width) | Width undefined — browser guesses, unpredictable | ❌ Broken |
<!-- services.html <head> --> <meta name="viewport" content="width=1024"> ← ❌ fixed pixel width — defeats responsive design <!-- Fix: replace with --> <meta name="viewport" content="width=device-width, initial-scale=1">
| Channel | Impact |
|---|---|
| SEO | Google uses mobile-first indexing. Fixed-width viewport is a mobile usability failure — flagged in Google Search Console. Pages with poor mobile usability are demoted in mobile search results which is the majority of searches. |
| AEO / GEO | AI crawlers that render pages may get a mis-scaled render, causing layout-dependent content extraction (tables, structured data, above-the-fold signals) to fail. |
Audit rule: no_www_redirect | This is a DNS/hosting configuration issue: both www.yourdomain.com and yourdomain.com serve the same content with no 301 redirect between them.
www.example.com and example.com return 200 OK with the same content,
Google treats them as two separate websites with duplicate content.
Link equity, backlinks, and ranking signals split between the two hostnames.
One must redirect to the other with a permanent 301.
Imagine two identical coffee shops on the same street — one is called "Joe's Bakery" and the other "www.Joe's Bakery." Customers (and Google) can't tell which is the real one. Reviews (backlinks), reputation (PageRank), and crawl history split between the two. The fix: pick one canonical host and 301-redirect the other to it permanently.
# Test whether both variants return 200 (broken) or one redirects to the other (correct) curl -I https://yourdomain.com curl -I https://www.yourdomain.com # ❌ Broken — both return 200 OK: # HTTP/2 200 ← yourdomain.com # HTTP/2 200 ← www.yourdomain.com (same content — duplicate URLs) # ✅ Correct — apex redirects to www (or www to apex): # HTTP/2 301 Location: https://www.yourdomain.com/ # HTTP/2 200 ← www.yourdomain.com serves content # To reproduce this on Cloudflare Pages: # 1. Add both www.yourdomain.com AND yourdomain.com as custom domains # (Cloudflare Pages → Custom Domains → add both) # 2. Do NOT configure a redirect rule between them # 3. Both return 200 → audit tool flags no_www_redirect
# Option A — Cloudflare Redirect Rules (recommended for Pages) # Dashboard → yourdomain.com → Rules → Redirect Rules → Create rule # When: Hostname equals "www.yourdomain.com" # Then: Static redirect → https://yourdomain.com${uri} → 301 # Option B — Cloudflare Workers (most reliable, any hostname pattern) addEventListener("fetch", event => { const url = new URL(event.request.url) if (url.hostname === "www.yourdomain.com") { url.hostname = "yourdomain.com" event.respondWith(Response.redirect(url.toString(), 301)) } else { event.respondWith(fetch(event.request)) } })
| Choice | Pros | Cons |
|---|---|---|
Apex → wwwyourdomain.com → www.yourdomain.com |
Most common; CDNs handle www via CNAME easily; email deliverability benefits from no subdomain conflict | Extra DNS lookup; more complex DNS setup at the apex |
www → Apexwww.yourdomain.com → yourdomain.com |
Shorter, cleaner URLs in SERPs and citations; no subdomain confusion for end users | Some CDN/load balancer setups can't use CNAME at apex — need ALIAS or ANAME record support |
| Channel | Impact |
|---|---|
| SEO | Duplicate URLs split all ranking signals. Backlinks to example.com don't count for www.example.com and vice versa. Classic canonical hygiene bug with measurable PageRank loss across all pages. |
| AEO | Answer engines may cite either form as the source, creating inconsistent attribution for the same content across two different hostnames. |
| GEO | LLMs that deduplicate by URL may index identical content twice under different hostnames, diluting domain authority signals used in retrieval ranking. |
Option A — Drag and drop (no CLI needed):
# 1. Go to https://pages.cloudflare.com # 2. Click "Create a project" → "Upload assets" # 3. Drag the entire seo-issues-lab/ folder into the upload area # 4. Click "Deploy site" # 5. Your site is live at https://<project-name>.pages.dev
Option B — Wrangler CLI:
cd examples/seo-issues-lab npx wrangler pages deploy . --project-name seo-issues-lab
Verify each issue after deployment:
# ① h1_missing # curl https://<site>/ | grep -i "<h1" → no match # Or: view source → search for <h1 → not found # ② links3xx # curl -I https://<site>/old-about → HTTP/2 301 # ③ redirect_temporary # curl -I https://<site>/sale → HTTP/2 302 # curl -I https://<site>/flash-offer → HTTP/2 307 # ④ extlinks4xx # Load /about.html in Screaming Frog / SE Ranking # Outbound link to httpstat.us/404 will return 404 # ⑤ css3xx # curl -I https://<site>/css/old-style.css → HTTP/2 301 # curl -I https://<site>/css/legacy-theme.css → HTTP/2 302 # ⑥ hreflang_xdefault # curl https://<site>/hreflang-en.html | grep "x-default" → no match # ⑦ viewport_device_width # curl https://<site>/services.html | grep "viewport" → width=1024 # ⑧ no_www_redirect # Requires custom domain — see Cloudflare custom domain setup below
Required to test issue ⑧ (no_www_redirect). Here is the full step-by-step:
STEP 1 — Your domain must be on Cloudflare (or use a domain registrar with Cloudflare DNS) STEP 2 — Go to Cloudflare Dashboard → Pages → your project → Custom Domains Click "Set up a custom domain" → enter: yourdomain.com → Continue Cloudflare auto-creates the DNS CNAME record pointing to Pages STEP 3 — Add www as a second custom domain (to reproduce ⑧) Custom Domains → "Set up a custom domain" → enter: www.yourdomain.com Now BOTH return 200 — issue ⑧ is reproduced STEP 4 — Fix issue ⑧: add a Cloudflare Redirect Rule Dashboard → yourdomain.com → Rules → Redirect Rules → Create rule Field: Hostname | Operator: equals | Value: www.yourdomain.com Action: Static redirect → https://yourdomain.com${uri} → 301 (Permanent) STEP 5 — Verify the fix curl -I https://www.yourdomain.com # Should now return: HTTP/2 301 Location: https://yourdomain.com/