Repository navigation
Filterable /apps browse page and header navigation - #58
Merged
Merged
Conversation
Discovery was split across tags, categories, ecosystems, lexicons and search pages, none reachable from the header. Replace them with one /apps list with a filter sidebar (tags, shared data, rating, extras, category) whose state lives in the URL. - Header gets Browse / Tags / Shared data links and an inline search box; mobile gets the hamburger menu back. - /apps/all, /search, /apps/$tag, /categories/$categoryId and /ecosystems/$app/all 301 to the equivalent /apps preset; internal links point there directly. - New getBrowseListings server fn returns every non-protocol listing with the fields the facets need; filtering/sorting is client-side. - Remove the server fns and helpers only the old pages used. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The page used to ship every listing (~170 KB gzipped) and filter in the browser. Now getBrowsePage runs the same pure logic server-side and returns one page of trimmed cards plus facet counts (~30 KB gzipped for the whole HTML). The catalog is cached in memory for 60s so filter clicks don't each hit the database, and text search covers the full description again since it no longer has to be shipped to the client. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
searchListings now accepts the same filters as the browse page (tag, category, data, minRating, has) and sorts (rating, reviews, favorites, updated), and runs on the shared, cached browse logic instead of a separate ilike query. Only the requested page's rows are loaded for the full card shape. All new params are optional, so existing callers keep working. Tag params (web and API) now accept raw tag text or slugs and are canonicalized to slugs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CI's format and lint jobs were already failing on main. Run oxfmt and oxlint --fix, then fix the rest by hand: - Replace non-null assertions in generate-listing-copy with nullable "good copy" values (same behavior: not-weak implies non-empty). - Use Boolean-free null filters, named Anthropic import, replaceAll with a string. - site-brand-icon: oxfmt lowercases hex literals while unicorn/number-literal-case wants uppercase; disable the rule for that file, matching the existing og.* override. - lex:gen now formats the generated bundle so regenerating it doesn't break format:check. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Feedback from a user: "instead of having different app categories making it hard to find things outside of a few… like the list with filters on the side!" (à la modrinth.com).
Discovery was split across
/apps/all,/apps/tags,/apps/$tag,/categories/…,/ecosystems/…,/apps/lexiconsand/search, and the header had no navigation links, only a search icon./apps/allalso hid every ecosystem tool (Bluesky clients, feeds, …).What
/apps: one list with a filter sidebarq), sort (trending, top rated, most reviewed, most favorited, newest, recently updated, A–Z), page and every filter are in the URL, with readable comma-joined params (/apps?tag=social,games&in=apps/bluesky)./searchused.Header
/apps?q=.Redirects (301) to
/appspresets/apps/all,/search/apps(keepssortandq)/apps/<tag>/apps?tag=<tag>(keeps the tag OG image and title)/categories/<id>/apps?in=<id>/ecosystems/<app>/all/apps?in=apps/<app>Internal links (home "See all", footer, tag cards, ecosystem pages, product-page badges) now point straight at
/apps./apps/tags,/apps/lexiconsand/ecosystems/<app>remain as index pages.Data
getBrowsePageserver fn: takes the URL search params and returns one page of 24 trimmed cards plus facet counts. Filtering, facet counts, sorting and pagination run on the server through pure, unit-tested logic insrc/lib/browse-listings.ts./appsHTML is about 30 KB gzipped on the dev server; data embedded in the page is about 23 KB. Text search covers the full description.getAllListings,getAllApps,getAppsByTagPage,getDirectoryBrowsePath(it returned a dead/categories/alllink), and other helpers only the old pages used.Public API:
fyi.atstore.directory.searchListingstag,category,data(all repeatable),minRating(3,4or4.5) andhas(open-source,donations,official).rating,reviews,favoritesandupdated./appsand loads full card rows only for the requested page.category=protocol), and only listings with a publishedlisting.detailrecord are returned, as before./appsmatching rules: it also covers the developer handle and category names, and search terms can match category labels rather than only the raw category paths.developer tool) or slugs (developer), and the web URL is rewritten to the slug.pnpm atproto:publish-lexicons; this PR doesn't do it.Notes / follow-ups
/categories/protocol%2F…) now land on an empty/appsview.Testing
tsc --noEmitpasses.vitest: 126 tests pass, 18 of them new for the browse logic.pnpm lex:lintpasses.oxlintis clean on changed files. The remaining errors are in files this PR doesn't touch.searchListingswith every new param, repeated and comma-joined values, cursor paging and an invalid cursor.🤖 Generated with Claude Code