<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: MobilityData</title>
    <description>The latest articles on DEV Community by MobilityData (@mobilitydata).</description>
    <link>https://dev.to/mobilitydata</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F10512%2Ffb0ca4b4-261b-47a6-9670-95fe4f69e8eb.png</url>
      <title>DEV Community: MobilityData</title>
      <link>https://dev.to/mobilitydata</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mobilitydata"/>
    <language>en</language>
    <item>
      <title>Making Open Transit Data Visible: How a Next.js Migration Changed What Google and LLMs See</title>
      <dc:creator>Alessandro Kreslin</dc:creator>
      <pubDate>Mon, 23 Mar 2026 17:58:12 +0000</pubDate>
      <link>https://dev.to/mobilitydata/making-open-transit-data-visible-how-a-nextjs-migration-changed-what-google-and-llms-see-3gh6</link>
      <guid>https://dev.to/mobilitydata/making-open-transit-data-visible-how-a-nextjs-migration-changed-what-google-and-llms-see-3gh6</guid>
      <description>&lt;p&gt;&lt;a href="https://mobilitydata.org/" rel="noopener noreferrer"&gt;MobilityData&lt;/a&gt; maintains the &lt;a href="https://mobilitydatabase.org/" rel="noopener noreferrer"&gt;MobilityDatabase&lt;/a&gt;, an open repository of transit data used by developers and agencies worldwide. Our pages were public. Our data was open. And none of it was properly visible to search engines.&lt;/p&gt;

&lt;p&gt;This is the story of how we migrated from a React SPA to Next.js, what we gained beyond performance numbers, and what surprised us along the way.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem: Invisible Public Data
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://mobilitydata.org/" rel="noopener noreferrer"&gt;MobilityData&lt;/a&gt; is a global non-profit dedicated to making transportation systems interoperable through open data standards. We maintain the &lt;a href="https://mobilitydatabase.org/" rel="noopener noreferrer"&gt;MobilityDatabase&lt;/a&gt;, an open catalog of over 6,000 GTFS, GTFS Realtime, and GBFS feeds across 99+ countries, used by developers, transit agencies, and researchers to build tools and services that help people get around without driving alone.&lt;/p&gt;

&lt;p&gt;Our application was a standard React single-page application. It worked well for users who landed on it directly, but it had a fundamental problem: when Google crawled our pages, it would do its best to parse the JavaScript, but had a low success rate. We ended up with many incorrectly parsed pages that Google classified as "low quality" and excluded from search results. For a platform whose entire purpose is making transit data discoverable and accessible, this was working directly against our mission.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjdcng4r3xf10fx9g7e5b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjdcng4r3xf10fx9g7e5b.png" alt="Google Search Console test of old SPA MobilityDatabase feed detail page" width="800" height="842"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Source: Google Search Console. Page being indexed, but not showing correct content&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Results Of The Migration
&lt;/h2&gt;

&lt;p&gt;Before diving into the how, here's what the migration actually delivered:&lt;/p&gt;

&lt;h3&gt;
  
  
  Improved Performance: Core Web Vitals
&lt;/h3&gt;

&lt;p&gt;Our methodology was simple.  The old and new applications were both tested under the same conditions: a 3-run average score using Lighthouse in the same environment.&lt;/p&gt;

&lt;p&gt;Lighthouse scores of the landing page, on desktop (left), and mobile (right). The landing page content loads &lt;strong&gt;over 1 second faster&lt;/strong&gt; on desktop and &lt;strong&gt;over 5 seconds faster&lt;/strong&gt; on mobile.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fepv7a7c0o0khvdrr3udu.png" alt="Landing page desktop" width="374" height="463"&gt;&lt;/th&gt;
&lt;th&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmoyu6jrdzfqxd46j6f7z.png" alt="Landing page mobile" width="370" height="467"&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Lighthouse scores of a sample feed detail page, on desktop (left), and mobile (right). The feed detail page content loads &lt;strong&gt;over 1 second faster&lt;/strong&gt; on desktop and &lt;strong&gt;over 3 seconds faster&lt;/strong&gt; on mobile.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fevknakdpb48cifzka8z8.png" alt="detail page page desktop" width="371" height="489"&gt;&lt;/th&gt;
&lt;th&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc6hwcuslotlwfl8ztsf9.png" alt="detail page mobile" width="369" height="486"&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Search Visibility
&lt;/h3&gt;

&lt;p&gt;Currently, we're tracking any indexing changes in Google Search Console and plan to share detailed metrics in a follow-up post once we have enough data. But early results are already clear.&lt;/p&gt;

&lt;p&gt;More telling than raw index counts is what Google actually displays. Searching &lt;code&gt;site:mobilitydatabase.org&lt;/code&gt; returned roughly 15 pages before the migration. One week after the launch of Next.Js, that number jumped to ~100 and is still climbing.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpbtrqbkj7t01nlf8b08k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpbtrqbkj7t01nlf8b08k.png" alt="100 indexed pages" width="800" height="592"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Open Graph Previews
&lt;/h3&gt;

&lt;p&gt;When someone shared a link to our platform on Slack, LinkedIn, or Twitter, the preview was either blank or generic. Now, shared URLs render with proper titles and descriptions.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F67cqd2wa26cw09zdpqsj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F67cqd2wa26cw09zdpqsj.png" alt="First URL is old SPA, the beta URL is the new Next.js state" width="620" height="193"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;First URL is old SPA, the beta URL is the new Next.js state&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  LLM Discoverability: An Under-appreciated Benefit
&lt;/h2&gt;

&lt;p&gt;This result surprised us the most, and it’s also the one we think matters increasingly going forward.&lt;/p&gt;

&lt;p&gt;An increasing share of how developers discover tools, APIs, and data sources is through AI assistants — ChatGPT, Claude, Perplexity, and others. These systems rely on being able to read and understand web content. LLM crawlers are less forgiving than Google's crawler and will refuse to visit pages that require JavaScript to render.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffjtk6p314m0emawg86k5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffjtk6p314m0emawg86k5.png" alt="llm discoverability spa" width="753" height="348"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Source: Claude Sonnet 4.6&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;After the migration, we tested how LLMs interact with our pages:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuelge55m1655nuzxa8g3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuelge55m1655nuzxa8g3.png" alt="llm discoverability ssr" width="752" height="314"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Source: Claude Sonnet 4.6&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If the LLM cannot read the url, it can’t parse its data or recommend the source of the data. Meaning, we've lost a discovery channel. One that's only going to grow. Server-side rendering isn't just about SEO anymore, it's about making your content accessible to the next generation of information retrieval systems.&lt;/p&gt;




&lt;h2&gt;
  
  
  How We Approached the Migration
&lt;/h2&gt;

&lt;p&gt;We evaluated the migration as both a rendering architecture change and an opportunity to address accumulated tech debt. The two goals reinforced each other, moving to Next.js forced us to reconsider patterns that had calcified in the SPA.&lt;/p&gt;

&lt;p&gt;Our approach was to drop the entire existing application into a Next.js catch-all route. This let us keep running the old stack: Redux, React Router, Firebase, while gradually adding new SSR pages through the App Router, which would override the catch-all as we went.&lt;/p&gt;

&lt;p&gt;But this process introduced its own problems. Running two routers in parallel caused navigation sync issues. URLs would update but page content wouldn't, and navigating from new pages back to legacy ones introduced performance delays.&lt;/p&gt;

&lt;p&gt;The lesson: gradual migration is viable, but it does introduce tech debt that can easily be forgotten about once the immediate pages are working.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reducing Redux's Role
&lt;/h3&gt;

&lt;p&gt;The core issue was &lt;code&gt;&amp;lt;PersistGate/&amp;gt;&lt;/code&gt; , Redux's standard hydration pattern. It blocks rendering until the store initializes, meaning every page paid a performance cost, even those that never touched the store.&lt;/p&gt;

&lt;p&gt;For components that relied heavily on the store, wrapping them was straightforward. The harder call was the Header component, which lived on every SSR page and contained a logout action. We chose to render these pages without waiting for store initialization, accepting that logout wouldn't work during the brief window before hydration. Faster LCP for every user was worth the edge case.&lt;/p&gt;

&lt;p&gt;For isolated features like Feeds Search, we replaced Redux with &lt;a href="https://swr.vercel.app/" rel="noopener noreferrer"&gt;SWR&lt;/a&gt;, significantly reducing boilerplate by colocating data fetching with the components that needed it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Technical Details
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Firebase Authentication for SSR
&lt;/h3&gt;

&lt;p&gt;The SPA used Firebase's client-side library to generate authentication tokens for our API. Moving to SSR meant server-side API calls needed user identity, but passing Firebase's long-lived refresh token in a cookie was too risky, and even the 1-hour Firebase ID token contained more data than our server actually needed.&lt;/p&gt;

&lt;p&gt;Instead, we created a custom short-lived JWT (HS256, 1-hour TTL) containing only the user's uid and email, stored in an httpOnly cookie. This minimizes the data exposed if a cookie is ever compromised. Server-side, this JWT is decoded and passed to Firebase Admin SDK to generate the token needed for API calls. Client-side calls still use Firebase's standard auth flow.&lt;/p&gt;

&lt;p&gt;Two parallel auth paths add complexity, but it keeps server-side authentication secure without exposing long-lived credentials or unnecessary user data.&lt;/p&gt;

&lt;h3&gt;
  
  
  Caching Strategies — Gains and Limits
&lt;/h3&gt;

&lt;p&gt;Server-side rendering is inherently more expensive than hosting static files for a default React SPA. An effective caching strategy keeps costs down while delivering real performance gains.&lt;/p&gt;

&lt;h4&gt;
  
  
  Static Pages
&lt;/h4&gt;

&lt;p&gt;Landing, About, Contact, these were straightforward wins using SSG (Static Site Generation). Because the new architecture eliminated the startup delays of the old app (Redux store initialization, Firebase auth checks), these pages now load nearly instantly due to the CDN.&lt;/p&gt;

&lt;p&gt;However, there is one complication: every page includes a Header component that changes based on authentication status. Partial Pre-rendering would have been the ideal solution. Instantly display the main content, then stream in the header. We discovered PPR too late to rework the architecture &lt;strong&gt;(more on that later in this article)&lt;/strong&gt;. Instead, the Header is a client component that picks up auth state during hydration. This is what causes the brief "Login" → "Account" flicker on first visit. Since it only occurs on initial page load, it's a low-priority fix but something we plan to address.&lt;/p&gt;

&lt;h4&gt;
  
  
  Feed Detail Pages
&lt;/h4&gt;

&lt;p&gt;These had the most to gain from caching, but also the most complexity. The feed detail page renders different content depending on authentication: non-authenticated, authenticated, and authenticated-admin. A single caching strategy couldn't cover all three.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjmr88i0a9frwg3pv0kd8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjmr88i0a9frwg3pv0kd8.png" alt="Example of feed detail page" width="800" height="668"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Example of feed detail page&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Our solution was to create two internal routes pointing to the same public URL, with middleware reading the auth cookie to route users to the correct version. We use public routes with header-level protection rather than Next.js route groups, because our catch-all route (which supports the legacy application) created conflicts with route groups.&lt;/p&gt;

&lt;h5&gt;
  
  
  Non-Authenticated
&lt;/h5&gt;

&lt;p&gt;All non-authenticated users see identical content, making the entire page safe to cache with ISR (Incremental Static Regeneration). We chose ISR over build-time SSG because there are 6,000+ feeds, so building them all at deploy isn't reasonably feasible.&lt;/p&gt;

&lt;h5&gt;
  
  
  Authenticated
&lt;/h5&gt;

&lt;p&gt;Because content is tied to a specific user ID, page-level caching isn't practical. Our current approach uses &lt;code&gt;unstable_cache&lt;/code&gt; to briefly cache API responses on the server, giving returning users a faster experience without bloating cache with per-user entries.&lt;/p&gt;

&lt;p&gt;This is a deliberate starting point, not the final answer. Our caching maturity path looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;unstable_cache&lt;/code&gt;&lt;/strong&gt; (current) — fast to ship, limited hit rate
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Client-side caching with SWR&lt;/strong&gt; (next) — better UX for returning users with no additional server cost
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Role-based ISR&lt;/strong&gt; (ideal) — with defined roles like &lt;code&gt;authenticated&lt;/code&gt; and &lt;code&gt;authenticated-admin&lt;/code&gt;, we could ISR cache these pages the same way we do for non-authenticated users, dramatically improving hit rates&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We held off on step 3 because our role system is still early-stage. Each step unlocks better performance without requiring infrastructure we haven't built yet.&lt;/p&gt;

&lt;h4&gt;
  
  
  Revalidation
&lt;/h4&gt;

&lt;p&gt;For our MVP release, a daily cron job revalidates the cache for all feeds, giving our ISR pages a ~34% cache hit rate. The next step is an on-demand revalidation endpoint that triggers whenever a feed is updated. This would extend cache lifetimes to a maximum of two weeks, significantly improving hit rates.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmr3bmz43cbtr3kyd7umq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmr3bmz43cbtr3kyd7umq.png" alt="ISR cache data" width="605" height="213"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Overall, the application sits at a ~91% cache hit rate. At this level, the majority of traffic is served from the edge without hitting the origin. This keeps our compute costs closer to what you'd expect from a static SPA than a fully server-rendered application. There's room to grow, but for a first release we're satisfied with it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkt2780xwnfkf195lqly6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkt2780xwnfkf195lqly6.png" alt="Total cache data" width="610" height="169"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Measuring Performance Improvements
&lt;/h3&gt;

&lt;p&gt;Early in our migration, performance data was misleadingly optimistic. Next.js &lt;strong&gt;prefetching&lt;/strong&gt; made navigations feel instant, but it masked underlying page weight. On our search page, pre-loading 20 detail pages (each triggering 2–3 API calls) created a "request storm" that strained our backend. We eventually &lt;strong&gt;disabled prefetching on the search page&lt;/strong&gt; (&lt;code&gt;prefetch={false}&lt;/code&gt;) to balance perceived speed with server load.&lt;/p&gt;

&lt;p&gt;To find a reliable baseline, we moved away from generic scores and focused on &lt;strong&gt;Largest Contentful Paint (LCP)&lt;/strong&gt; as our primary metric. We refined our toolkit to separate synthetic noise from real-world behavior:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;What it Caught&lt;/th&gt;
&lt;th&gt;Why we used it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;JS-Off Check&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;SSR failures / PersistGate issues&lt;/td&gt;
&lt;td&gt;If a page is blank with JS disabled, it's invisible to search engines. This was the catalyst for moving data fetching to the server.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Browser Performance Tab&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Unused CSS &amp;amp; "Request Storms"&lt;/td&gt;
&lt;td&gt;Our most reliable source of truth. It revealed actual load behavior and identified unneeded CSS chunks that Lighthouse missed.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Screaming Frog&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Crawler visibility&lt;/td&gt;
&lt;td&gt;We used this to crawl the site specifically to verify what search engines and screen readers see without executing client-side logic.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;"Generated At" Timestamp&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;ISR/Cache verification&lt;/td&gt;
&lt;td&gt;A value in the HTML that proved ISR was actually serving a cached version rather than re-rendering on every hit.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Lighthouse&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;General health (Directional)&lt;/td&gt;
&lt;td&gt;We found scores inconsistent between runs for precise benchmarking, treating them as a guide rather than a final grade.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Our AI-Assisted Workflow
&lt;/h2&gt;

&lt;p&gt;We started the migration in January 2026 using Copilot with Claude Sonnet 4.5 and Opus 4.5 to plan the architecture and validate a transition plan. What we didn't account for was a timing gap:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Next.js 16 with Partial Pre-rendering released: &lt;strong&gt;October 2025&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Sonnet 4.5 training cutoff: &lt;strong&gt;July 2025&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Opus 4.5 training cutoff: &lt;strong&gt;May 2025&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Neither model had seen Next.js 16 documentation. Given our limited engineering resources and no prior Next.js experience on the team, choosing Next.js 15 would have been the safer call. A version the AI tools actually knew well, letting us move faster with more reliable assistance. Once I realized the gap, I started feeding relevant documentation links directly into prompts, which helped but didn't fully close the knowledge deficit.&lt;/p&gt;

&lt;p&gt;For the day-to-day workflow, we split tasks by complexity. Large architectural changes: route restructuring, layout migrations, state management refactoring, went to &lt;strong&gt;Opus&lt;/strong&gt;, which handled the full scope of changes and their cascading effects better. Contained tasks: component conversion, boilerplate generation, data fetching logic, went to &lt;strong&gt;Sonnet&lt;/strong&gt;, which was faster and more cost-effective for focused work.&lt;/p&gt;

&lt;p&gt;AI was excellent at accelerating the repetitive parts of migration. Where it required close supervision was refactoring cleanup: removing Redux or restructuring files would often leave dead code, unused imports, or orphaned utilities that needed manual review.&lt;/p&gt;

&lt;h3&gt;
  
  
  The PPR Miss
&lt;/h3&gt;

&lt;p&gt;Because we weren't familiar with Next.js before this migration, we relied on AI research to establish the baseline architecture. The tools guided me toward a solid SSR + ISR setup, but never surfaced Partial Pre-rendering, the one feature that would have cleanly solved several problems we worked around manually. The Header flicker on static pages, the caching constraints on feed detail pages, PPR's ability to serve a static shell while streaming in dynamic components addresses both.&lt;/p&gt;

&lt;p&gt;By the time we discovered PPR through reading the documentation, the architecture was already set. It's on the roadmap to be revisited for a future iteration.&lt;/p&gt;

&lt;p&gt;The takeaway: AI tools are most useful when you already have enough domain knowledge to evaluate what they're not telling you.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why We Chose Vercel Over Cloud Run
&lt;/h2&gt;

&lt;p&gt;We weighed Vercel against Google Cloud Run for our Next.js hosting. While both are robust, they optimize for different priorities: control versus velocity.&lt;/p&gt;

&lt;p&gt;Cloud Run offered lower direct costs and more infrastructure control, but it would have required significant engineering hours to configure the CI/CD pipeline, Dockerize the environment, and manage the CDN layer. Vercel, built by the creators of Next.js, offered a zero-config experience that aligned with our lean team's needs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The deciding factors for Vercel were:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Native Preview Environments:&lt;/strong&gt; Automatic, unique URLs for every PR. A workflow our team already relied on.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Integrated CDN &amp;amp; Caching:&lt;/strong&gt; Next.js features like Incremental Static Regeneration (ISR) work out of the box, allowing us to hit a 91% cache rate without manual configuration.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero Infrastructure Overhead:&lt;/strong&gt; No Dockerfiles or container orchestration; we simply push code and it deploys.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cold Starts:&lt;/strong&gt; Vercel optimizes for JavaScript/Node.js using a warming layer, which keeps cold starts in the 300ms–800ms range. For Cloud Run, spinning up a Next.js container typically takes 2–5 seconds.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Edge Middleware:&lt;/strong&gt; Vercel runs middleware on the edge CDN. In our case, where middleware routes users to cached or dynamic feed detail pages, this was a significant advantage.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The Trade-off:&lt;/strong&gt; We estimated Vercel would cost &lt;strong&gt;$75–$100/month&lt;/strong&gt; (based on dev seats and traffic), compared to roughly &lt;strong&gt;$10–$35/month&lt;/strong&gt; on Cloud Run. However, for a small team where engineering time is the most expensive resource, paying a premium for a platform that "just works" was a clear win for our roadmap.&lt;/p&gt;




&lt;h2&gt;
  
  
  What We'd Do Differently
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Read the docs before asking the AI.&lt;/strong&gt; Spending a day with the Next.js documentation end-to-end before writing any code would have surfaced PPR early enough to build around it. When you're new to a framework, it's tempting to let AI tools guide the learning, but they can only teach you what they know.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Assess AI tools' knowledge boundaries upfront.&lt;/strong&gt; Asking the model about a recent feature and checking the response would have revealed the Next.js 16 gap immediately instead of mid-migration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Break the first PR into smaller pieces.&lt;/strong&gt; The initial migration PR was massive. Smaller, incremental PRs would have been easier to review, easier to roll back, and would have shown clearer progression to the team. &lt;a href="https://github.com/MobilityData/mobilitydatabase-web/pull/3" rel="noopener noreferrer"&gt;Link to the Initial PR&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What This Unlocked
&lt;/h2&gt;

&lt;p&gt;Our pages are indexable, which directly affects whether transit agencies and developers find the MobilityDatabase through organic search, whether AI assistants surface us when someone asks about open transit data, and whether a shared link actually communicates what it points to.&lt;/p&gt;

&lt;p&gt;The mobile experience got meaningfully faster, which matters more than analytics suggest. First impressions at conferences and demos happen on phones, not on the monitors we develop on.&lt;/p&gt;

&lt;p&gt;The codebase is leaner. Removing unnecessary Redux, modernizing the auth flow, and adopting Next.js conventions gave us a foundation that's easier to build on.&lt;/p&gt;

&lt;p&gt;Next up: on-demand cache revalidation to push hit rates higher, SEO keyword tuning to capitalize on our new visibility, adopting Partial Pre-rendering to further close the gap between static and dynamic performance, and continued work on our backlog of transit data tools.&lt;/p&gt;

&lt;h3&gt;
  
  
  About MobilityData
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://mobilitydata.org/" rel="noopener noreferrer"&gt;MobilityData&lt;/a&gt; is a global non-profit maintaining the development of open data standards that power transit and shared mobility apps worldwide. We support specs like &lt;a href="https://gtfs.org/" rel="noopener noreferrer"&gt;GTFS&lt;/a&gt; and &lt;a href="https://gbfs.org/" rel="noopener noreferrer"&gt;GBFS&lt;/a&gt;, working with agencies, companies, and developers to make mobility data more usable and consistent.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>ai</category>
      <category>performance</category>
      <category>react</category>
    </item>
    <item>
      <title>When Murphy Meets Terraform: The Tale of a Simple Guard That Saved My Friday</title>
      <dc:creator>David Gamez</dc:creator>
      <pubDate>Wed, 22 Oct 2025 17:07:26 +0000</pubDate>
      <link>https://dev.to/mobilitydata/when-murphy-meets-terraform-the-tale-of-a-simple-guard-that-saved-my-friday-23gg</link>
      <guid>https://dev.to/mobilitydata/when-murphy-meets-terraform-the-tale-of-a-simple-guard-that-saved-my-friday-23gg</guid>
      <description>&lt;p&gt;This is a story about how simple precautionary measures can save you hours of work and a fair bit of your sanity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Context
&lt;/h2&gt;

&lt;p&gt;At &lt;strong&gt;&lt;a href="https://mobilitydata.org/" rel="noopener noreferrer"&gt;MobilityData&lt;/a&gt;&lt;/strong&gt;, we created the &lt;a href="https://mobilitydatabase.org" rel="noopener noreferrer"&gt;MobilityDatabase&lt;/a&gt; to host public transit and shared mobility feeds.&lt;br&gt;&lt;br&gt;
Our infrastructure lives in &lt;strong&gt;Google Cloud Platform (GCP)&lt;/strong&gt;, and everything is deployed using &lt;strong&gt;Infrastructure as Code (IaC)&lt;/strong&gt; powered by &lt;strong&gt;Hashicorp Terraform&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In theory, Terraform keeps everything tidy and predictable. In practice... well, let's just say Murphy's Law also lives in the cloud.&lt;/p&gt;




&lt;h2&gt;
  
  
  Terraform (in a nutshell)
&lt;/h2&gt;

&lt;p&gt;At a high level, &lt;a href="https://developer.hashicorp.com/terraform" rel="noopener noreferrer"&gt;Terraform&lt;/a&gt; revolves around &lt;strong&gt;three main ingredients&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Configuration files&lt;/strong&gt; (&lt;code&gt;.tf&lt;/code&gt;): These define &lt;em&gt;what&lt;/em&gt; your infrastructure should look like, which services to create, their properties, and dependencies.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;State file&lt;/strong&gt; (&lt;code&gt;terraform.tfstate&lt;/code&gt;): A JSON file that stores the &lt;em&gt;current reality&lt;/em&gt; of your deployed resources, including IDs, metadata, and versions.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The actual cloud resources&lt;/strong&gt;: The real stuff living in GCP, such as Cloud Run services, Pub/Sub topics, buckets, and databases.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here is the magic: Terraform continuously compares your &lt;strong&gt;configuration&lt;/strong&gt; with your &lt;strong&gt;state file&lt;/strong&gt;, then plans changes needed to align reality with your desired setup.  &lt;/p&gt;

&lt;p&gt;When you run &lt;code&gt;terraform plan&lt;/code&gt;, it says:  &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Hey, your config says there should be a new Cloud Run variable, but the state doesn't know about it. Should I fix that for you?"  &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Then, when you approve (&lt;code&gt;terraform apply&lt;/code&gt;), Terraform talks to the cloud provider APIs to make those updates happen.&lt;/p&gt;

&lt;p&gt;It is a beautiful system until versions get out of sync.&lt;/p&gt;

&lt;p&gt;More about how this works is explained &lt;a href="https://developer.hashicorp.com/terraform/tutorials/cli/apply" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Terraform State: The Single Source of Truth
&lt;/h2&gt;

&lt;p&gt;That humble JSON file, the &lt;strong&gt;state&lt;/strong&gt;, is Terraform's brain.&lt;br&gt;&lt;br&gt;
It knows everything: what you created, how it is configured, and who is responsible for it. Without it, Terraform becomes forgetful and might try to rebuild your infrastructure from scratch.&lt;/p&gt;

&lt;p&gt;Because of that, the state file must be handled like a &lt;strong&gt;sacred artifact&lt;/strong&gt;: secure, backed up, and shared safely among your team. (In our case, it lives in a GCP Storage bucket, because no one wants to accidentally delete production with &lt;code&gt;terraform apply&lt;/code&gt; from their laptop.)&lt;/p&gt;




&lt;h2&gt;
  
  
  Different Clouds, Different Flavours
&lt;/h2&gt;

&lt;p&gt;Terraform is cloud-agnostic. It does not care whether you are deploying to AWS, Azure, GCP, or something more exotic like Cloudflare or GitHub Actions.&lt;br&gt;&lt;br&gt;
Each cloud has its &lt;strong&gt;own Terraform provider&lt;/strong&gt;, which translates Terraform's "desired state" into API calls specific to that platform.&lt;/p&gt;

&lt;p&gt;That flexibility is excellent until provider versions change.&lt;br&gt;&lt;br&gt;
That is when the fun begins.&lt;/p&gt;




&lt;h2&gt;
  
  
  Breaking Changes Actually &lt;em&gt;Break&lt;/em&gt; Stuff
&lt;/h2&gt;

&lt;p&gt;Without going too deep into our setup, we needed to add a new property to one of our &lt;strong&gt;Cloud Run&lt;/strong&gt; services.&lt;br&gt;&lt;br&gt;
The configuration looked simple: "Just one new line, what could go wrong?"  &lt;/p&gt;

&lt;p&gt;Turns out, a lot.&lt;/p&gt;

&lt;p&gt;We discovered that our Terraform provider was &lt;strong&gt;one version behind&lt;/strong&gt;, and the new property looked slightly different in the latest one. "No problem," we thought. "Let's just upgrade the provider."&lt;/p&gt;

&lt;p&gt;Famous last words.&lt;/p&gt;

&lt;p&gt;The moment we tried to deploy, Terraform refused to play along:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Error: Resource instance managed by newer provider version&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This was the "mic drop" moment, on a &lt;strong&gt;Friday&lt;/strong&gt;, of course.&lt;/p&gt;

&lt;p&gt;After diving through GitHub issues, Terraform docs, and consulting my &lt;strong&gt;AI army&lt;/strong&gt;, the top suggestion was terrifyingly simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"You can just modify the Terraform state manually."&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Editing the State File (a.k.a. Playing with Fire)
&lt;/h2&gt;

&lt;p&gt;Now, JSON may &lt;em&gt;look&lt;/em&gt; friendly, but manually editing your Terraform state is like performing surgery with a chainsaw.  &lt;/p&gt;

&lt;p&gt;You &lt;em&gt;can&lt;/em&gt; do it, but you really should not.&lt;br&gt;&lt;br&gt;
Even the official docs practically scream in all caps:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"You should not manually change information in your state file to avoid unnecessary drift between your configuration, state, and infrastructure."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And they are right. One wrong edit, and Terraform might decide your entire setup no longer exists and "fix" that by deleting everything.&lt;/p&gt;

&lt;p&gt;So yeah, not exactly my idea of a relaxing Friday morning.&lt;/p&gt;




&lt;h2&gt;
  
  
  Redo My State, Perhaps?
&lt;/h2&gt;

&lt;p&gt;Here is where our &lt;strong&gt;fire extinguisher&lt;/strong&gt; came in.&lt;/p&gt;

&lt;p&gt;When we first set up Terraform, we decided to store our state file in a &lt;strong&gt;GCP bucket&lt;/strong&gt;, and we also turned on &lt;strong&gt;object versioning&lt;/strong&gt; (&lt;a href="https://cloud.google.com/storage/docs/object-versioning" rel="noopener noreferrer"&gt;docs here&lt;/a&gt;).  &lt;/p&gt;

&lt;p&gt;We didn't think much of it at the time; it was more of a "just in case" safety measure. But that small checkbox turned out to be our hero.&lt;/p&gt;

&lt;p&gt;We simply rolled back the state file to the previous version, re-ran &lt;code&gt;terraform apply&lt;/code&gt;, and &lt;em&gt;voila&lt;/em&gt;, everything was back to normal. My Friday was saved. Coffee tasted better again. The weekend was back on track.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Terraform is powerful, but like any tool that touches production, it deserves respect and a few safety nets.  &lt;/p&gt;

&lt;p&gt;So, before you dive into fancy refactors or provider upgrades, make sure you have your &lt;strong&gt;"fire extinguishers"&lt;/strong&gt; in place:  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Versioned state file
&lt;/li&gt;
&lt;li&gt;Remote backend&lt;/li&gt;
&lt;li&gt;Provider version pinning
&lt;/li&gt;
&lt;li&gt;And maybe a reminder not to &lt;code&gt;terraform apply&lt;/code&gt; on Fridays
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because when Murphy shows up, you will want more than luck on your side.  &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Happy coding!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;About MobilityData&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://mobilitydata.org/" rel="noopener noreferrer"&gt;MobilityData&lt;/a&gt; is a global non-profit maintaining the development of open data standards that power transit and shared mobility apps worldwide. We support specs like &lt;a href="https://gtfs.org/" rel="noopener noreferrer"&gt;GTFS&lt;/a&gt; and &lt;a href="https://gbfs.org/" rel="noopener noreferrer"&gt;GBFS&lt;/a&gt;, working with agencies, companies, and developers to make mobility data more usable and consistent.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>terraform</category>
      <category>googlecloud</category>
      <category>infrastructureascode</category>
    </item>
    <item>
      <title>Spotlight on Accessibility: Mapping Paratransit and ADA-Friendly Stops with Mobility Database</title>
      <dc:creator>Jingsi Lu</dc:creator>
      <pubDate>Mon, 08 Sep 2025 14:39:32 +0000</pubDate>
      <link>https://dev.to/mobilitydata/spotlight-on-accessibility-mapping-paratransit-and-ada-friendly-stops-with-mobility-database-5a5j</link>
      <guid>https://dev.to/mobilitydata/spotlight-on-accessibility-mapping-paratransit-and-ada-friendly-stops-with-mobility-database-5a5j</guid>
      <description>&lt;p&gt;Public transit is a lifeline for millions, but for riders with disabilities, navigating the system can be challenging if accessible information isn’t readily available. At MobilityData, we’re committed to making transit data not only open but also inclusive, helping everyone get where they need to go with confidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Accessibility Matters in Transit Data
&lt;/h2&gt;

&lt;p&gt;Accessibility features, such as wheelchair ramps, text-to-speech announcements, and paratransit services, ensure that transit systems serve all riders fairly. However, these features are only useful if they’re clearly marked and easy to find.  &lt;/p&gt;

&lt;p&gt;That’s why the &lt;a href="https://mobilitydatabase.org/" rel="noopener noreferrer"&gt;Mobility Database&lt;/a&gt; includes detailed accessibility attributes in its transit feeds and offers tools to filter data based on these features, making it easier for app developers, planners, and riders themselves to identify accessible stops and trips.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using Mobility Database Accessibility Filters
&lt;/h2&gt;

&lt;p&gt;If you open the &lt;a href="https://mobilitydatabase.org/" rel="noopener noreferrer"&gt;Mobility Database&lt;/a&gt; and browse feeds, you’ll see a panel on the left that lists filters to explore transit feeds. There are &lt;strong&gt;3 key Accessibility Features&lt;/strong&gt; you can filter on:&lt;/p&gt;

&lt;h2&gt;
  
  
  1. &lt;strong&gt;&lt;a href="https://gtfs.org/getting-started/features/accessibility/#stops-wheelchair-accessibility" rel="noopener noreferrer"&gt;Stops Wheelchair Accessibility&lt;/a&gt;&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;This attribute shows whether a bus stop or train station is wheelchair accessible. Knowing which stops or platforms offer accessibility, whether through ramps, elevators or gapless platforms, can be a game-changer for riders who depend on them.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. &lt;strong&gt;&lt;a href="https://gtfs.org/getting-started/features/accessibility/#trips-wheelchair-accessibility" rel="noopener noreferrer"&gt;Trips Wheelchair Accessibility&lt;/a&gt;&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Trips may vary in accessibility — for example, some buses on a route might be equipped for wheelchairs while others aren’t. The Mobility Database allows filtering to see only trips that are wheelchair accessible.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. &lt;strong&gt;&lt;a href="https://gtfs.org/getting-started/features/accessibility/#Text-to-Speech" rel="noopener noreferrer"&gt;Text-to-Speech Support&lt;/a&gt;&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;For riders with visual impairments, text-to-speech announcements at stops or on vehicles provide essential guidance. Mobility Database includes this info so apps can surface it to users who need it.&lt;/p&gt;

&lt;p&gt;All this empowers users with disabilities to travel smarter and safer.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb1izwcbmawt765q78s68.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb1izwcbmawt765q78s68.png" alt=" " width="800" height="469"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Looking Ahead: Toward More Inclusive Transit Systems
&lt;/h2&gt;

&lt;p&gt;Accessibility is an ongoing effort. By making these data features transparent and easily accessible, the Mobility Database supports a future where public transit is truly for everyone, regardless of mobility challenges.  &lt;/p&gt;

&lt;p&gt;If you’re a developer, planner, or rider interested in accessibility data, explore the &lt;a href="https://mobilitydatabase.org/" rel="noopener noreferrer"&gt;Mobility Database&lt;/a&gt; today and see how filtering by features can transform the transit experience.&lt;/p&gt;

&lt;h2&gt;
  
  
  About MobilityData
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://mobilitydata.org/" rel="noopener noreferrer"&gt;MobilityData&lt;/a&gt; is a global non-profit maintaining the development of open data standards that power transit and shared mobility apps worldwide. We support specs like &lt;strong&gt;&lt;a href="https://gtfs.org/" rel="noopener noreferrer"&gt;GTFS&lt;/a&gt;&lt;/strong&gt; and &lt;strong&gt;&lt;a href="https://gbfs.org/" rel="noopener noreferrer"&gt;GBFS&lt;/a&gt;&lt;/strong&gt;, working with agencies, companies, and developers to make mobility data more usable and consistent.&lt;/p&gt;

</description>
      <category>gtfs</category>
      <category>gbfs</category>
      <category>opensource</category>
    </item>
    <item>
      <title>What’s New in the Mobility Database – May 2025 Update</title>
      <dc:creator>Emma Blue</dc:creator>
      <pubDate>Wed, 21 May 2025 20:22:19 +0000</pubDate>
      <link>https://dev.to/mobilitydata/whats-new-in-the-mobility-database-may-2025-update-4hl1</link>
      <guid>https://dev.to/mobilitydata/whats-new-in-the-mobility-database-may-2025-update-4hl1</guid>
      <description>&lt;p&gt;&lt;a href="https://mobilitydatabase.org/" rel="noopener noreferrer"&gt;The Mobility Database&lt;/a&gt; is an international catalog of public transit data for transit agencies, rider-facing apps, technology vendors, researchers, and others to use. It features over 3,000 General Transit Feed Specification (GTFS) and GTFS Realtime feeds.&lt;/p&gt;

&lt;p&gt;Here's what we did in May: &lt;/p&gt;

&lt;h2&gt;
  
  
  ✨ Features and Updates
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Search by GTFS feature&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;🤔 Curious to know which &lt;a href="https://gtfs.org/getting-started/features/overview/" rel="noopener noreferrer"&gt;GTFS features&lt;/a&gt; different agencies support?  This new search filter allows you to easily discover feeds based on the features they’ve implemented, such as Fares, Flex or Pathways. &lt;a href="https://mobilitydatabase.org/feeds?gtfs=true" rel="noopener noreferrer"&gt;Explore here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpwsngiovydprdeqbjg9n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpwsngiovydprdeqbjg9n.png" alt="screenshot of feature search filter on the Mobility Database" width="800" height="440"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Update: TransitFeeds will be deprecated by December 2025&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The Mobility Database is replacing &lt;a href="https://transitfeeds.com/" rel="noopener noreferrer"&gt;TransitFeeds.com&lt;/a&gt; as the central open platform for discovering transit data. We’ll be adding two more features before removing access to TransitFeeds at the end of this year: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Moving historical TransitFeeds data to the Mobility Database&lt;/li&gt;
&lt;li&gt;Adding routes and stop visualizations for each feed &lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  GTFS data added last month
&lt;/h2&gt;

&lt;p&gt;🇨🇦All data from Statistics Canada’s &lt;a href="https://www150.statcan.gc.ca/n1/pub/23-26-0003/232600032025001-eng.htm" rel="noopener noreferrer"&gt;Canadian Public Transit Database&lt;/a&gt; has now been added. &lt;/p&gt;

&lt;p&gt;18 new locations: &lt;/p&gt;

&lt;p&gt;🇧🇷Brazil, Rio de Janeiro: &lt;a href="https://mobilitydatabase.org/feeds/gtfs/mdb-2632" rel="noopener noreferrer"&gt;Angra dos Reis&lt;/a&gt;&lt;br&gt;
🇨🇦Canada&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://mobilitydatabase.org/feeds?q=british+columbia&amp;amp;gtfs=true&amp;amp;gtfs_rt=true" rel="noopener noreferrer"&gt;11 new British Columbia static and real time feeds&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Quebec: &lt;a href="https://mobilitydatabase.org/feeds/mdb-2603" rel="noopener noreferrer"&gt;Rouyn-Noranda&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;🇫🇷France: &lt;a href="https://mobilitydatabase.org/feeds/mdb-2604" rel="noopener noreferrer"&gt;Occitanie&lt;/a&gt; &lt;br&gt;
🇬🇬&lt;a href="https://mobilitydatabase.org/feeds/gtfs/mdb-2615" rel="noopener noreferrer"&gt;Guernsey: Sark Shipping&lt;/a&gt;&lt;br&gt;
🇬🇱Greenland: &lt;a href="https://mobilitydatabase.org/feeds/mdb-2596" rel="noopener noreferrer"&gt;Sermersooq&lt;/a&gt;&lt;br&gt;
🇯🇵Japan: &lt;a href="https://mobilitydatabase.org/feeds/gtfs/mdb-2606" rel="noopener noreferrer"&gt;Tsu Airport Line&lt;/a&gt; and &lt;a href="https://mobilitydatabase.org/feeds/gtfs/mdb-2605" rel="noopener noreferrer"&gt;Kuwana City&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;24 updated locations including data from 🇨🇦Canada, 🇮🇳 India, 🇮🇱 Israel, 🇮🇹 Italy, 🇯🇵 Japan, 🇪🇸 Spain, and  the United States. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/MobilityData/mobility-feed-api/discussions/1207" rel="noopener noreferrer"&gt;Detailed data breakdown here&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  👋 Add yourself to the app list
&lt;/h2&gt;

&lt;p&gt;Are you using the Mobility Database for your app or website? Let us know so your needs can be considered in our roadmap. &lt;a href="https://forms.gle/7wSuPnAPcmroBc4V8" rel="noopener noreferrer"&gt;You can add your app to our list here.&lt;/a&gt; Many thanks to &lt;a href="https://transitous.org/" rel="noopener noreferrer"&gt;Transitous&lt;/a&gt; and &lt;a href="https://www.rome2rio.com/" rel="noopener noreferrer"&gt;Rome2Rio&lt;/a&gt; who added themselves this month!&lt;/p&gt;

&lt;h2&gt;
  
  
  🤩 Thanks to our 11 contributors this month
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Byron Williams&lt;/li&gt;
&lt;li&gt;Christoffer Søndergaard&lt;/li&gt;
&lt;li&gt;Dan Cory &lt;a href="https://urbanfootprint.com/" rel="noopener noreferrer"&gt;(Urban Footprint) &lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Diego Canales &lt;a href="https://actionfigure.ai/" rel="noopener noreferrer"&gt;(ActionFigure)&lt;/a&gt; &lt;/li&gt;
&lt;li&gt;Giovanni R. Ferreora &lt;a href="https://angra.rj.gov.br/" rel="noopener noreferrer"&gt;(City Hall of Angra dos Reis)&lt;/a&gt; &lt;/li&gt;
&lt;li&gt;Hiroyuki Ito &lt;a href="http://Rosenzu.com" rel="noopener noreferrer"&gt;(Rosenzu.com)&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/derhuerst" rel="noopener noreferrer"&gt;Jannis R&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Karl von Weyhe&lt;/li&gt;
&lt;li&gt;Michael Salaverry&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Solain" rel="noopener noreferrer"&gt;@Solain&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Radha Krishna&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>gtfs</category>
      <category>transit</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
