ReviewOS

stacks/bench-review

seo: no canonical URLs on any indexable page; /judges/{id} → /profile redirect is client-side only

#64
Closed glennmichael123 opened this 9 days ago ·
9 days ago

Problem

Duplicate-content variants (/ vs /home, /judges/{id} vs /judges/{id}/profile, .html variants) have no consolidation signals, splitting whatever ranking equity the pages earn.

Findings

  • Only the four 306-byte auth redirect stubs carry rel="canonical" (pointing to /login); no content page has one.
  • The /judges/{id} → /judges/{id}/profile redirect happens only in client JS (ProfileHeader.stx:48-49, navigate() in onMount) — crawlers see two URLs for the same content.

Acceptance criteria

  • Every indexable page emits an absolute <link rel="canonical">
  • /judges/{id} consolidates to /judges/{id}/profile via build-time redirect or canonical

Split out from the SEO bundle of the 2026-06-10 production-readiness audit. Related: stacksjs/stx#1756 (static head injection).

9 days ago

Half of this is now fixed for judges as well as courthouses.

/judges/:id — the URL in the sitemap — was bouncing client-side to /judges/:id/profile, a shared shell page. It no longer does: the canonical page renders the judge itself from the same three components the profile tab uses, mirroring the courthouse fix in 37f9884. Verified on the built artifact: /judges/1 stays at /judges/1.

Two things turned up while fixing it, both on all 2740 canonical judge pages:

Invented facts. The page carried a hardcoded summary card — "4.8/5.0", "Active", "Superior Court of California", "Appointed 2020", "Criminal Law, Family Law" — identical on every page, for judges those values had nothing to do with. A crawler runs no JS and never saw the redirect, so that is what it read as the judge's record. Gone.

The per-judge head was being overwritten at hydration. Bench/Judge/ProfileHeader called useSeoMeta with a generic "Judge profile — Bench Review" baseline, and because it mounts on the canonical page it clobbered what entitySeo injects at build time — the same regression fixed for CourtHeader in 83d0b6d, quietly undoing 6b2eb19. Static HTML was fine, so crawlers were unaffected, but the browser tab, history entry and share sheet all read the generic title. Removed; /judges/1 and /judges/42 now keep their own titles and descriptions after hydration.

Canonical URLs themselves (the original title of this issue) have been in place since the canonical/OG work; what remains here is whatever is still outstanding for other route families.

9 days ago

Closing: both acceptance criteria are met.

Every indexable page emits an absolute canonical. Enforced, not just present — gate.ts check 3 ("indexable pages have canonical") fails the build otherwise, across all 2,989 pages. dist/judges/1.html carries <link rel="canonical" href="…/judges/1">.

/judges/{id} is the consolidated URL. The split this issue describes came from ProfileHeader.stx bouncing /judges/{id} to /judges/{id}/profile in client JS, so crawlers saw two URLs and the canonical one was a redirect stub. That redirect is gone and views/judges/[id].stx now renders the judge itself, so /judges/{id} stands on its own: 2,743 built pages, each self-canonical, each in the sitemap.

The tab URLs do not compete for it. JudgeTabs builds its links with x-to (client-evaluated), so no href="/judges/1/profile" exists in the static HTML for a crawler to follow, and the _shell pages are absent from the sitemap — zero _shell entries.

One residual worth recording, small enough that it did not hold the close: the four judge shells and three courthouse shells carry no <meta name="robots">, so if one is ever reached directly it self-canonicals to /judges/_shell/profile. Nothing links them today, but marking them noindex would be belt-and-braces. Happy to do it as a one-liner if wanted.

Sign in to comment on this issue.