Skip to content

Make library search and category filters deep-linkable#50

Merged
mberman84 merged 1 commit into
Forward-Future:mainfrom
HMAKT99:feat/url-filter-state
Jun 22, 2026
Merged

Make library search and category filters deep-linkable#50
mberman84 merged 1 commit into
Forward-Future:mainfrom
HMAKT99:feat/url-filter-state

Conversation

@HMAKT99

@HMAKT99 HMAKT99 commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Summary

The homepage library's filter state (search query, active category, page) lived only in memory. As a result:

  • a filtered or searched view couldn't be shared or bookmarked,
  • a reload dropped the filters back to the default view, and
  • the browser back/forward buttons didn't restore previous filter states.

This wires that state into the URL query string so a slice of the library is directly linkable — a natural fit for a catalog that's explicitly built for SEO/GEO and agent discovery (llms.txt, catalog.json, etc.).

Behavior

  • Search → ?q=…, category → ?category=engineering, page → ?page=2. Defaults (empty query, all, page 1) are omitted so the canonical homepage URL stays clean.
  • On load and on popstate, state is restored from the URL. The category is validated against the available filter chips (unknown values fall back to all) and the page is parsed/clamped by the existing pagination logic, so hand-edited or stale URLs degrade gracefully.
  • Uses history.replaceState (guarded for environments without it), so typing in the search box doesn't flood the history stack.

Implementation notes

  • Extracted the category-activation logic into a reusable applyCategory() (used by both the click handler and URL restore).
  • syncUrlState() is called at the end of updateLibrary(), which already runs on every search/category/pagination change, so there's a single write path. It only calls replaceState when the URL actually changes.
  • No-ops cleanly on pages without the library (the searchInput/results guards already short-circuit updateLibrary).

Verification

  • node scripts/check.mjsLoop Library checks passed.
  • node --check site/script.js
  • Builders (build-skill-catalog, build-loop-pages, build-social-images) → no artifact drift.
  • Manual: searching, switching categories, and paging update the URL; reloading and back/forward restore the same view; ?category=bogus / ?page=99 normalize to a valid state.

@HMAKT99

HMAKT99 commented Jun 21, 2026

Copy link
Copy Markdown
Contributor Author

This one's a feature rather than a bug fix, so completely understand if it's not a direction you want — feel free to close it, no hard feelings.

The idea: the homepage filter state (search, category, page) lives only in memory, so a filtered view can't be shared or bookmarked, a reload drops it, and back/forward don't restore it. This reflects it in the URL (?q=, ?category=, ?page=), omitting defaults to keep canonical URLs clean, and restores + validates it on load and popstate. It felt like a natural fit for a catalog that's explicitly built for linking and agent discovery, but I get that it adds surface area.

If you're open to it but want it scoped differently (e.g. query-only, no page param), I'm glad to trim. Check suite + builders pass with no artifact drift.

@mberman84 mberman84 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The URL state is bookmarkable, but three blocking issues remain:

  1. syncUrlState() starts with a fresh URLSearchParams, so loading ?utm_source=newsletter&q=agent immediately deletes utm_source and any other URL state the library does not own. Initialize from window.location.search, then set/delete only q, category, and page.
  2. Every interaction uses replaceState. Browser verification confirmed that after changing category or page, Back leaves the library instead of restoring the prior filter state, so the new popstate handler cannot deliver the behavior claimed by the PR. Use pushState for meaningful category/pagination navigation boundaries; reserve replaceState for initial canonicalization or transient search typing.
  3. This changes site/script.js without bumping the shared versioned script URL. Update the emitted/validated asset version and regenerate affected pages.

@HMAKT99

HMAKT99 commented Jun 21, 2026

Copy link
Copy Markdown
Contributor Author

All three addressed:

  1. syncUrlState() now seeds from window.location.search and only sets/deletes q/category/page, so utm_source and any other foreign params survive.
  2. Category and pagination changes use pushState (Back now restores the prior filter state); replaceState is reserved for initial canonicalization and transient search typing. popstate stays read-only.
  3. Bumped script.js to 20260621-url-state everywhere + regenerated the loop pages.
    Check suite + builders green.

@HMAKT99

HMAKT99 commented Jun 21, 2026

Copy link
Copy Markdown
Contributor Author

@mberman84 ready for re-review whenever you have time — foreign params preserved, pushState on navigation boundaries, version bumped. Appreciate the detailed feedback.

@mberman84 mberman84 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Superseded by the corrected review immediately below.

@mberman84 mberman84 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correction to my immediately preceding review, whose inline code formatting was stripped by the shell:

The three earlier blockers are fixed. Exact-head browser verification confirmed that foreign query params survive, category navigation pushes a history entry, and Back restores the prior query/filter state. The exact head also passes node scripts/check.mjs, syntax checks, and all builders.

This branch now needs a rebase onto current main before it can be approved. Since this PR branched, #61/#62 moved loop detail pages to the database-backed proxy and removed scripts/build-loop-pages.mjs plus site/loops/**. GitHub reports this head as conflicting, and the stale generated-page edits must not restore files intentionally removed by that migration.

Please rebase onto the latest main, keep the focused URL-state logic, update only the surviving shared script references/check assertions with a fresh version, and leave the removed generator/static loop pages deleted. Then rerun node scripts/check.mjs and the browser history checks.

@mberman84 mberman84 force-pushed the feat/url-filter-state branch from a2725f0 to 4795758 Compare June 22, 2026 18:20

@mberman84 mberman84 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rebased onto the database-backed main architecture and integrated with the newly merged clear-filters action. Exact-head browser verification confirms foreign query params survive, clearing pushes a history entry, Back restores the prior query/category, and focus returns to search. The full current CI-equivalent suite also passes locally (41/41 Worker tests).

@mberman84 mberman84 merged commit 4514a21 into Forward-Future:main Jun 22, 2026
1 check passed
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.

2 participants