ADR-063: Work Lanes as Routes

Replaces the CSS-only radio filter on the work index with one static page per lane, so a lane can be linked to and the selected lane is exposed to assistive technology. Written after the change shipped; accepted on review.

Last updated

Status

Accepted (2026-10-11, retroactive — the change shipped in PR #101 on 2026-10-10 before this record existed; accepted by the owner on review)

Context

The work index (/work/) filtered case studies by lane with a radio group and :has() sibling selectors. It needed no JavaScript, but it had two problems:

  • A lane had no URL. The homepage names three lanes and could not link to any of them, and a filtered view could not be shared.
  • The selected lane was visual state only on a set of visually hidden radios. The cards that did not match were hidden with display: none.

At the same time the Web & Brand lane was advertised on the homepage, the About page and the work index, and led nowhere: its only case study was a draft and the sites gallery was disabled.

This record was written after the fact. The change altered route shapes and component props, which ADR-038 names as triggers for the three-pass workflow, and .claude/workflow.md says architectural calls are not made inline. Neither was followed in PR #101. Issue #103 tracks the gap.

Decision Drivers

  • Linkable lanes: the homepage and other pages need a destination per lane
  • Zero JavaScript: constitution rule 7 prefers CSS and static HTML over script
  • Accessible state: the selected lane should be announced, not only painted
  • No empty views: a lane with no published work must not produce a blank page

Considered Options

Option 1: Keep the radio filter

Leave /work/ as it was and link the homepage lanes to /work/ unfiltered.

Pros: no change; one page. Cons: lanes stay unlinkable; the homepage strip cannot route.

Turn each pill into a link to a fragment (#lane-tools) and filter with :has(:target).

Pros: linkable; still one page; CSS only. Cons: the selected state cannot be expressed in markup, so it is not exposed to assistive technology; hidden cards remain in the DOM.

Option 3: Radios plus :target

Keep the radios and add :target as a second trigger for deep links.

Pros: keeps the announced radio state for in-page use. Cons: the two triggers conflict. Arriving on a fragment and then choosing a different radio applies both filters and hides every card.

Option 4: One static route per lane (chosen)

Generate /work/lane/<lane>/ for each lane that has published work, rendered by a component shared with /work/.

Pros: real URLs; aria-current="page" on the selected pill; only matching cards are rendered; no script. Cons: more pages in the build; a navigation instead of an in-page filter.

Decision

Option 4.

Implementation Details

  • src/components/structural/WorkIndex.astro renders the listing for the index and for every lane page. It takes activeLane, defaulting to all.
  • src/pages/work/lane/[lane].astro generates one page per lane with at least one published case study. A lane with none gets neither a page nor a pill.
  • src/utils/work.ts holds the lane labels, laneHref() and pickRelatedProjects(). “More work” on a case study prefers the same lane, then sortOrder.
  • The homepage lanes strip links AI Engineering and Dev Tools to their lane pages and Web & Brand to /sites/.
  • The sites gallery (/sites/) is a live route again. A site entry may name a caseStudy slug; the card links to it only when that case study is published.

Consequences

Positive

  • A lane can be linked to, shared, and indexed as its own page
  • The selected lane is announced through aria-current
  • The Web & Brand lane has a destination

Negative

  • Two more pages in the build today (ai, tools), three once the web lane has a published case study
  • Choosing a lane is a page navigation; the view transition softens it but it is not instant
  • The listing markup now lives in a shared component, one step removed from the page files

Neutral

  • The lane route sits under /work/lane/ so it cannot collide with a case-study slug under /work/

Validation

  • src/utils/__tests__/work.test.ts covers laneHref() and pickRelatedProjects()
  • e2e/work.spec.ts covers the lane link, the rendered cards, aria-current, and the related-work order
  • e2e/index.spec.ts covers the homepage lane links
  • e2e/sites.spec.ts covers the gallery and the case-study link gating
  • The decision is wrong if lane pages draw no visits once the site is indexed, or if the extra pages push a build or Lighthouse budget

References

  • PR #101: the change itself
  • Issue #96: the discovery review that prompted it
  • Issue #103: this record, and the workflow that was skipped (closed on acceptance)
  • ADR-038: the three-pass workflow