ReviewOS

stacks/bench-review

routing: courthouse tab routes have no shared shell, so they 404 offline (judges do)

#83
Closed glennmichael123 opened this 10 days ago ·
10 days ago

Part of #79. Found while building the offline bundle.

Problem

Judge tab routes resolve offline; courthouse tab routes do not.

app/Helpers/staticPaths.ts has judgeShellPath(), which pre-renders one shared shell per tab instead of one per judge:

dist/judges/_shell/profile.html
dist/judges/_shell/reviews.html
dist/judges/_shell/rulings.html
dist/judges/_shell/cases.html

preview.ts rewrites /judges/:id/reviews onto /judges/_shell/reviews.html, and the components resolve the judge id from location.pathname rather than a build-time param (they have to — stx leaves window.stx._rp unset on a hard reload). So one file serves all 2,740 judges.

Courthouses have no equivalent. They are pre-rendered per id:

dist/court-houses/1/profile.html
dist/court-houses/1/bench.html
dist/court-houses/1/reviews.html
… ×202

scripts/build-offline-bundle.ts strips per-entity trees, which is what takes the app from 443 MB to 13 MB — so in the offline bundle every courthouse tab route 404s, while every judge tab route still works.

Why this is worth fixing beyond the app

The asymmetry predates the mobile work and costs the web build too: 808 courthouse pages (143 MB of dist/) exist only because there is no shell, and they contain no per-courthouse content that is not fetched client-side anyway. A shell would cut the web artifact as well as fix the offline gap.

What to do

Mirror the judge approach:

  1. Add courtShellPath() to app/Helpers/staticPaths.ts returning { params: { id: '_shell' } }
  2. Point the three courthouse tab views at it via getStaticPaths
  3. Add preview.ts SHELL_REWRITES entries capturing the tab, not the id — the judge rules already do this, and getting it backwards was a real bug once (all four tab URLs returned byte-identical responses)
  4. Confirm the courthouse components resolve the id from location.pathname, as the judge ones do, and not from a build-time param
  5. Re-run bun run gate — "every dynamic route is pre-rendered or rewritten" and the sitemap checks both cover this

Checks

  • /court-houses/42/profile resolves in the offline bundle
  • Hard reload (not just SPA nav) renders the right courthouse
  • Gate still 18/18, and the sitemap advertises nothing that 404s
  • dist/ page count and size drop measurably
9 days ago

Starting on this turned up two things the description does not account for.

Bench/Court/CourtTabs.stx interpolated the id into x-to as a template literal, which stx evaluates once at build time while the id is still its initial 0. The built artifact contains only:

href="/court-houses/undefined/bench"
href="/court-houses/undefined/profile"
href="/court-houses/undefined/reviews"

All three tabs on all 202 courthouse pages lead to /court-houses/undefined/…. This is the same defect that was fixed in Bench/Judge/JudgeTabs.stx earlier — the courthouse equivalent was never done. Classification: bench, not stx; the judge fix proved a derived works correctly through x-to.

Fixed separately from the shell work, since it is a shipped navigation bug rather than an offline one.

2. The shell conversion is blocked on the sitemap, which this issue did not mention

app/Helpers/sitemap.ts:105 advertises one URL per courthouse:

<loc>${base}/court-houses/${c.id}/profile</loc>

202 of them. If profile becomes a shared shell, those files stop existing and the gate's "every sitemap URL has a built file" check fails — which is exactly the trap that caught the judge version (the sitemap advertised 2,740 nonexistent /judges/:id/profile URLs).

For judges the fix was repointing the sitemap at /judges/:id, which is a real pre-rendered page. Courthouses have no equivalent: resources/views/court-houses/[id].stx is a client-side redirect shell that bounces to /profile. Advertising that as canonical would re-create the client-side-redirect problem in #64.

So doing this properly means one more step than the description implies:

  • Make /court-houses/:id a real canonical page — render CourtHeader + CourtProfile there instead of redirecting, mirroring /judges/:id
  • Then convert the three tab views to courtShellPath()
  • Repoint the sitemap from /court-houses/:id/profile to /court-houses/:id
  • preview.ts SHELL_REWRITES capturing the tab, not the id (getting that backwards once made all four judge tab URLs return byte-identical responses)

Confirmed already fine: CourtHeader.stx and CourtProfile.stx both resolve the id from window.location.pathname with the same _rp fallback the judge components use, so no component changes are needed for the shell to work.

Payoff is larger than offline alone: 606 per-tab pages collapse to 3, which takes roughly 143 MB out of dist/ and gives courthouses the canonical page they currently lack.

9 days ago

Second blocker found on starting the shell conversion, and it is live today on the judge side.

Converting tabs to shells silently destroys their per-entity SEO

app/Helpers/entitySeo.ts writes the rich per-courthouse head — real title, description, and the CivicStructure JSON-LD node — to the tab files:

put(`court-houses/${c.id}/${tab}.html`, { … })   // profile | bench | reviews

Those are exactly the 808 files this issue proposes to replace with 3 shells. A shell is one file serving 202 courthouses, so it cannot carry a per-courthouse head. Convert naively and all 202 sitemap'd courthouse URLs lose their titles.

The judge side already did this, and nobody noticed

Measured in the current artifact:

page<title>
court-houses/1/profile.html (tab)District Court, D. Oregon in OR | Bench Review
judges/1803.html (canonical, in the sitemap)Judge Profile

2,740 of 2,740 judge pages ship the identical generic title. When the judge tabs became shell-served, put('judges/<id>/<tab>.html', …) stopped addressing a real file — every call became a silent no-op — and the canonical page the sitemap advertises was left with the view's @section('title', 'Judge Profile').

The conformance gate did not catch it because it checks that a head and a canonical exist, not that they differ per entity. 2,740 identical titles is exactly the duplicate-content signal the SEO issues in this repo were opened about.

Fixed first, separately

entitySeo.ts now writes the per-judge head to judges/<id>.html — the canonical URL the sitemap actually advertises — and the Person node's @id/url moved from /judges/<id>/profile to /judges/<id> to match. The per-tab puts stay, emptied of the Person node, so they are correct again if a tab is ever pre-rendered per id.

Revised plan for this issue

The shell conversion must now also move the courthouse head to the canonical page, which makes the real change set:

  • entitySeo.ts — courthouse head to court-houses/<id>.html
  • staticPaths.ts — add courtShellPath()
  • three tab views → courtShellPath()
  • court-houses/[id].stx — real canonical page instead of a client-side redirect
  • sitemap.ts — /court-houses/:id/profile → /court-houses/:id
  • preview.ts — tab rewrite capturing the tab, not the id
9 days ago

Done in 37f9884, with the two prerequisites this issue turned up landing first (6b2eb19, 83d0b6d).

beforeafter
pages3,5932,990 (−603)
dist430.3 MB376.1 MB
court-houses/143 MB80.6 MB
offline bundle45 pages48 pages (the 3 shells)

Verified at runtime against the built artifact, both page types:

routeuseRoute().paramsresult
/court-houses/42/bench (shell){"id":"_shell"}tab hrefs resolve to 42, no undefined
/court-houses/42 (canonical){"id":"42"}redirected: false, canonical /court-houses/42, place @id …/court-houses/42#place

All four courthouse routes return 200 through preview.ts, including the three now served by shells.

That first row is worth keeping in mind for #87: on a shell-served route useRoute().params.id is the literal string "_shell", so the path fallback kept in 8a83bff is load-bearing. A pure useRoute() migration would have broken all 606 of these.

What this issue did not originally account for

The canonical page had to stop redirecting. Once the tabs are shell-served the sitemap needs a per-courthouse target, and /court-houses/[id].stx was a client-side redirect whose only content was "Opening courthouse profile…". Advertising that as canonical is the complaint in #64. It now renders the courthouse, mirroring /judges/:id.

The per-entity SEO had to move with it, or all 202 pages would have dropped to the generic "Court House - Bench Review" — which is exactly what had already happened to 2,740 judge pages (fixed separately in 6b2eb19).

CourtHeader was overwriting the head at hydration with a hardcoded generic baseline, left over from when per-courthouse values "needed server-side data resolution (follow-up)". That follow-up is this work, so the call had become an override. Removed in 83d0b6d.

The gate caught the one thing I got wrong — its every getStaticPaths id has a built page check still probed court-houses/<id>/profile.html and reported 202 of 202 missing. Repointed to the canonical page, as the judge entry already was.

Gate 18/18, app tsc 0 errors, pickier clean.

Still open, noted rather than widened into: the build emits og:type: article for courthouses where place would be correct.

Sign in to comment on this issue.