Skip to content

fix(seo): give every page its own title, canonical and sitemap entry - #150

Merged
aquie00t merged 1 commit into
mainfrom
fix/seo-title-and-sitemap
Aug 30, 2026
Merged

fix(seo): give every page its own title, canonical and sitemap entry#150
aquie00t merged 1 commit into
mainfrom
fix/seo-title-and-sitemap

Conversation

@aquie00t

Copy link
Copy Markdown
Contributor

Summary

Searching for one of our own articles by name found nothing, and /sitemap.xml advertised ten URLs. Three separate causes, all in the Worker.

1. The <title> was never injected. buildMetaTags produced og:title and twitter:title but left the shell's own tag alone — and a second <title> does not override the first, so every article, post and profile on the site was served as:

<title>TDN - The Developer Network</title>

Google titles a result from <title>, so no page was findable by its own name. Sharing a link looked perfect the whole time because the OG tags were correct, which is exactly why this went unnoticed. injectIntoHead now strips the shell's tag before appending, and an article title is branded … · TDN unless it already carries the name (posts read "… on TDN", profiles "… (@handle) - TDN", so they are left alone).

2. No <link rel="canonical"> reached crawlers. The SEO component sets one client-side, but the crawler-visible HTML had none. Every branch already computes the canonical URL for og:url; it is now emitted as a <link> too.

3. The sitemap carried no posts and no profiles. It asked /posts for limit=100:

GET /api/v1/posts?page=1&limit=100  →  400 Validation Error

Both list endpoints cap limit at 50, and 100 is a rejection rather than a truncation. fetchPostPage swallows a failed page into [], so all three pages came back empty and the sitemap shipped with zero posts and — profiles being derived from post authors — zero profiles, while still returning 200 and looking healthy. Six pages of 50 keep the previous reach of 300.

Verified against production before and after

$ curl -s https://developernetwork.net/articles/v8-motorunun-anatomisi-... | grep '<title>'
<title>TDN - The Developer Network</title>          # og:title was correct; the title was not

$ curl -s https://developernetwork.net/sitemap.xml | grep -c '<loc>'
10                                                   # 4 static + 6 articles, no posts, no profiles

$ curl -s -o /dev/null -w '%{http_code}' 'https://api.developernetwork.net/api/v1/posts?page=1&limit=100'
400

Type of change

  • Bug fix
  • New feature
  • Refactor / cleanup
  • Dependency update
  • Documentation

How was this tested?

pnpm exec tsc -b clean
pnpm lint clean
pnpm test 631 / 631 passing
pnpm test:e2e (full suite) 48 / 48 passing

New coverage:

  • worker/index.test.ts — exactly one <title> and one canonical per page, the branding rule in both directions, and a sitemap test whose /posts handler enforces the 50 cap the real API enforces. The old handler ignored the query string entirely, which is why nothing caught this.
  • e2e/worker/worker.spec.ts — the title and canonical asserted against the real built shell under wrangler dev, where the tag is indented inside <head>. A fixture cannot prove that.

Screenshots (if applicable)

N/A — nothing visual changes; this is all in the crawler-visible head.

Checklist

  • pnpm lint passes
  • pnpm exec tsc -b passes (no type errors)
  • Tested on mobile viewport (or N/A) — N/A
  • No new any types introduced
  • No console.log left in code
  • SEO / OG meta not regressed (if UI change)

Left alone deliberately

  • <html lang="en"> on Turkish articles. A wrong language signal, but fixing it properly means the API exposing an article language; hard-coding per-route would be a guess.
  • x-robots-tag: noindex on /sitemap.xml is ours and is correct — it keeps the sitemap itself out of the index without affecting how it is read.
  • The shell body is <div id="root"></div>. Google renders JS, so pages do get indexed, just more slowly than server-rendered content would. Changing that is an SSR decision, not a bug fix.

🤖 Generated with Claude Code

https://claude.ai/code/session_01MyKUa9kaZDJbC7D1j7w2Fs

Searching for an article by name found nothing, and the sitemap advertised
ten URLs. Three separate causes, all in the Worker.

The `<title>` was never injected. `buildMetaTags` produced og:title and
twitter:title but left the shell's tag alone, and a second `<title>` does
not override the first — so every article, post and profile on the site was
titled "TDN - The Developer Network". Sharing a link looked correct the
whole time, because the OG tags were right; only search was affected, which
is why it went unnoticed. `injectIntoHead` now strips the shell's tag, and
an article title is branded `… · TDN` unless it already carries the name.

No `<link rel="canonical">` reached crawlers either. Every branch already
computes the canonical URL for og:url; it is now emitted as a link too.

The sitemap asked `/posts` for `limit=100`. Both list endpoints cap it at
50 and answer 400, which `fetchPostPage` swallows into an empty page, so
the live sitemap carried no posts and — profiles being derived from post
authors — no profiles, while still returning 200. Six pages of 50 keep the
previous reach.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MyKUa9kaZDJbC7D1j7w2Fs
@cloudflare-workers-and-pages

Copy link
Copy Markdown
Contributor

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
tdn-client c639fd7 Commit Preview URL

Branch Preview URL
Aug 30 2026, 06:09 AM

@aquie00t
aquie00t merged commit 96088ce into main Aug 30, 2026
8 checks passed
@aquie00t
aquie00t deleted the fix/seo-title-and-sitemap branch August 30, 2026 06:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant