Skip to content

feat: add feed location directory - #259

Draft
Alessandro100 wants to merge 11 commits into
mainfrom
feat/location-directory
Draft

Alessandro100 wants to merge 11 commits into
mainfrom
feat/location-directory

Conversation

@Alessandro100

@Alessandro100 Alessandro100 commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Summary:

Adds a feed location directory at /locations. Depends on MobilityData/mobility-feed-api#1884, which adds location lookup, children and feed counts to the locations API and location filters to /v1/search, built on the OSM geopolygon data.

  • Header: "Feeds" is now a dropdown like "Tools", with "Search" (/feeds) and "Locations" (/locations). Clicking "Feeds" on desktop still goes to /feeds. On mobile, the drawer shows "Feeds" as an accordion.

  • Footer: "Locations" link next to "Feeds".

  • /locations/[[...location]]: /locations/{path...} at any depth, with ?page=N. For example: /locations/north-america/canada/quebec.

    • Segments: the URL after /locations/ is exactly the API's path, from the continent down, so depth follows each country's geopolygon hierarchy.
    • Countries without a continent: these start at the country (/locations/clipperton).
    • Each page shows:
      • a back button and breadcrumb (Feeds / Locations / one crumb per level, continent first);
      • the location's GTFS / GTFS-RT / GBFS counts as chips;
      • child locations as cards, most feeds first, with a name filter in a scrollable grid; each card shows its total and per-type counts with the numbers in bold, and a flag for countries;
      • the location's feeds in the same compact table as /feeds (SearchTable), 20 per page.
  • Data: every page's API calls are built from the URL alone and sent at the same time, so a page waits for one round trip.

    • /locations: continents from /v1/locations?location_type=continent&has_feeds=true&include=feed_counts&sort=feed_count, and the newest feeds.
    • Any other page, continents included:
      • /v1/locations/lookup?path=<path>&include=feed_counts,children&has_feeds=true&sort=feed_count&limit=100 returns the location and the first 100 children that have feeds.
      • /v1/search?location_path=<path>&status=active,inactive,development,future&sort=location returns the feeds, those with most of their stops in the place first. These statuses are the ones the counts include.
    • More than 100 children: the remaining pages come from /v1/locations/{location_id}/children, fetched in parallel on the server.
  • Loading states:

    • Clicking a child card shows a progress bar on that card (useLinkStatus, shown after 150ms so fast navigations don't flash).
    • loading.tsx renders a skeleton of the page while the next location streams in.
  • SEO:

    • Pages are server-rendered, so the child, feed and pagination links are in the HTML.
    • Per-location generateMetadata (title, description, canonical URL, OpenGraph).
    • Malformed paths, unknown paths (lookup returns 404) and out-of-range pages render the not-found page.
    • Child cards and pagination links use prefetch={false}, since a location can list hundreds of children.
  • Caching:

    • Data is fetched server-side with the guest token, like the static feed pages.
    • It's cached for 6 hours under the location-directory and guest-feeds tags, so revalidateAllFeeds() also clears it.
    • A missing location is never cached. Neither is an empty feed page, because the feed search runs in parallel with the lookup and gets an empty answer for an unknown path.
  • i18n: new locations namespace in en and fr. Headings don't name a level ("Places"), since depth varies by country.

  • Mocks: MSW mocks for:

    • the lookup, single-location and children endpoints;
    • /v1/locations;
    • /v1/search with location_id or location_path.

    The data is fictional:

    • continents with ids -1 to -7;
    • variable depth;
    • two same-named siblings with suffixed slugs;
    • a country without a continent;
    • Canada > Quebec > Montreal with 22 feeds, so pagination shows.
  • ApiError: in services/feeds/index.ts, it now carries the HTTP status (the message is unchanged), so the page can map 400/404 to notFound().

  • Fix: getDataTypeElement in SearchTable called useTranslations once per row. That crashed when the row count changed between renders, for example moving from a partial last page to a full one. The hook now lives in a small DataTypeElement component.

  • CI: cypress-io/github-action bumped to v7 (fixes the "Socket closed" e2e failures; same change as in feat: unsubscribe page #255).

Before merging: src/app/services/feeds/types.ts was generated from the API PR's spec. After the API change is released, run yarn generate:api-types again, and merge this after the API PR.

Known limitations:

  • Coverage: feeds without reverse-geolocated stops are not in the directory (they are still counted in the root page's feed list). GTFS Realtime feeds take the locations of the schedule feeds they reference.
  • Countries without a continent (e.g. Clipperton) aren't linked from the root page, which only lists continents. Their pages work by direct URL.
  • Bad URLs still trigger a search: on an unknown path, the parallel feed search is still sent (and returns nothing).
  • French pages: place names come from OSM in English (or the local name), so /fr pages show "North America".
  • Root page: no count chips, since the API has no catalog-wide counts per data type; the feed list shows the total.
  • Not-found status: not-found pages return HTTP 200, because loading.tsx starts streaming before notFound() runs (already the case before this change).
  • Canonical URL: it uses a hard-coded https://mobilitydatabase.org base.
  • Sitemap: location pages aren't in the sitemap yet.
  • Loading indicator: pagination and breadcrumb links don't show the card progress bar.
  • Tests: no Cypress test yet.

Expected behavior:

  • /locations shows the continents with counts.
  • /locations/north-america lists its countries that have feeds, most feeds first.
  • /locations/north-america/canada/quebec/montreal?page=2 shows feeds 21 onward.
  • /locations/North-America, /locations/atlantis, /locations/europe/canada and ?page=0 render the not-found page.

Testing tips:

  • yarn start:dev:mock, then browse /locations and /fr/locations.

  • Against real data: the API PR is deployed to dev, so the Vercel preview of this branch shows real data. On dev, a page nobody had opened loaded in 0.56 to 0.93 s (lookup 0.21 to 0.62 s, search 0.05 to 0.45 s); cached pages load in about 0.3 s.

  • Checks: yarn lint (0 errors; 3 warnings in files this PR doesn't touch), yarn test:ci (661 passed) and npx tsc --noEmit pass.

  • Run the unit tests with yarn test to make sure you didn't break anything

  • Add or update any needed documentation to the repo

  • Format the title like "feat: [new feature short description]". Title must follow the Conventional Commit Specification(https://www.conventionalcommits.org/en/v1.0.0/).

  • Linked all relevant issues

  • Include screenshot(s) showing how this pull request works and fixes the issue(s)

Generated with Claude Code

Turn the Feeds header item into a dropdown (Search, Locations), add a
Locations footer link, and add the /locations directory: continent >
country > subdivision > municipality with GTFS / GTFS-RT / GBFS counts,
breadcrumbs, and a paginated, relevance-sorted feed list using the search
page's compact view.

Also move the translation hook in SearchTable's data type cell into a
component, so changing the number of rows no longer breaks the hook order.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vercel

vercel Bot commented Nov 2, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
mobilitydatabase-web Ready Ready Preview Nov 6, 2026 1:39pm UTC

Request Review

A 400 or 404 from the location directory API was cached as a missing
location for a day, so a page requested before the API route was deployed
kept returning 404 after it was. Throw on a miss inside the cached
functions, which unstable_cache never stores, and resolve it to null
outside the cache.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Use the feed detail page's container and breadcrumb navigation, with its
back button, and show GTFS / GTFS-RT / GBFS counts as chips. Child
locations are now sorted by feed count, shown as shadowed cards like the
advanced search results, and listed in a scrollable box with a search
field that filters them by name, ignoring accents and case.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Country segments use name slugs (/locations/europe/portugal) through the
  API's new `country` parameter; API types regenerated.
- Child cards show per-type feed counts as plain text instead of chips.
- Child search field gets a white background.
- No viewport prefetch on child cards and pagination links.
- Directory data cache lowered from 1 day to 6 hours.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Clicking a child location shows a pending bar on its card
  (useLinkStatus, delayed 150ms to avoid flashing on fast navigations).
- loading.tsx renders a skeleton of the location page while it streams.
- Feed counts on child cards show their numbers in bold.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The feeds of a location node come from /v1/search with the node's
location parameters, the directory statuses and sort=location, replacing
the removed /v1/locations/directory/feeds endpoint. The root page has no
location, so it lists the newest feeds first. API types regenerated and
the search mock serves directory feeds for location queries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Pages use /v1/locations (continents, countries of a continent, a location
  by path, its children by parent_location_id) and /v1/search with continent
  or location_id, replacing /v1/locations/directory.
- /locations/{continent}/{path...} works at any depth; segments are the API
  slugs, and the breadcrumb comes from path_names and path_slugs.
- Children are fetched page by page on the server (100 per call).
- Mocks cover variable depth and suffixed slugs; level-specific wording is
  now level-agnostic.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The location filters and feed counts now locate GTFS Realtime feeds
through the schedule feeds they reference; only the descriptions change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Nov 5, 2026 •

Copy link
Copy Markdown

*Lighthouse ran on https://mobilitydatabase-7kyufzses-mobility-data.vercel.app/ * (Desktop)
⚡️ HTML Report Lighthouse report for the changes in this PR:

Performance Accessibility Best Practices SEO
🟠 61 🟢 90 🟢 96 🟢 100

*Lighthouse ran on https://mobilitydatabase-7kyufzses-mobility-data.vercel.app/feeds * (Desktop)
⚡️ HTML Report Lighthouse report for the changes in this PR:

Performance Accessibility Best Practices SEO
🟠 83 🟠 87 🟢 96 🟢 100

*Lighthouse ran on https://mobilitydatabase-7kyufzses-mobility-data.vercel.app/feeds/gtfs/mdb-2126 * (Desktop)
⚡️ HTML Report Lighthouse report for the changes in this PR:

Performance Accessibility Best Practices SEO
🔴 39 🟢 95 🟢 96 🟢 100

*Lighthouse ran on https://mobilitydatabase-7kyufzses-mobility-data.vercel.app/feeds/gtfs_rt/mdb-2585 * (Desktop)
⚡️ HTML Report Lighthouse report for the changes in this PR:

Performance Accessibility Best Practices SEO
🟢 100 🟠 84 🟢 96 🟢 100

*Lighthouse ran on https://mobilitydatabase-7kyufzses-mobility-data.vercel.app/feeds/gbfs/gbfs-flamingo_porirua * (Desktop)
⚡️ HTML Report Lighthouse report for the changes in this PR:

Performance Accessibility Best Practices SEO
🟢 99 🟢 94 🟢 96 🟢 100

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every directory page now resolves its URL with
/v1/locations/lookup?path=...&include=feed_counts,children and lists its
feeds with /v1/search?location_path=..., both sent at the same time, so a
page waits for one round trip. Continents are ordinary locations with
negative ids, and a location's path is its URL after /locations/, so the
continent special cases are gone. Empty feed pages are never cached, so an
unknown URL leaves nothing in the data cache.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch was successfully deployed

1 active deployment
Preview — d5cc0481 Deployed Nov 6, 2026 by vercel[bot]
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.

1 participant