Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,12 @@
"viewReport": "View Validation Report",
"viewJsonReport": "View Validation Report in JSON format"
},
"datasetHistoryColumns": {
"downloadedAt": "Downloaded At",
"serviceDateRange": "Service Date Range",
"validationReport": "Validation Report",
"actions": "Actions"
},
"viewRealtimeVisualization": "View real-time visualization",
"versions": "Versions",
"dataAttribution": "Transit data provided by",
Expand Down Expand Up @@ -368,7 +374,9 @@
"producerUrl": "Producer URL",
"providerUrl": "Provider URL",
"systemId": "System ID",
"feedAuthentication": "Feed Authentication"
"feedAuthentication": "Feed Authentication",
"openAutoDiscoveryUrl": "Open auto-discovery URL in new tab",
"downloadProducerUrl": "Download producer URL"
}
},
"account": {
Expand Down
10 changes: 9 additions & 1 deletion messages/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,12 @@
"viewReport": "View Validation Report",
"viewJsonReport": "View Validation Report in JSON format"
},
"datasetHistoryColumns": {
"downloadedAt": "Téléchargé le",
"serviceDateRange": "Plage de dates de service",
"validationReport": "Rapport de validation",
"actions": "Actions"
},
"viewRealtimeVisualization": "View real-time visualization",
"versions": "Versions",
"dataAttribution": "Transit data provided by",
Expand Down Expand Up @@ -368,7 +374,9 @@
"producerUrl": "Producer URL",
"providerUrl": "Provider URL",
"systemId": "System ID",
"feedAuthentication": "Feed Authentication"
"feedAuthentication": "Feed Authentication",
"openAutoDiscoveryUrl": "Ouvrir l'URL de découverte automatique dans un nouvel onglet",
"downloadProducerUrl": "Télécharger l'URL du producteur"
}
},
"account": {
Expand Down
Binary file added public/android-chrome-192x192.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/android-chrome-512x512.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/apple-touch-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/assets/MOBILTYDATA_logo_light_blue_M.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/assets/MOBILTYDATA_logo_purple_M.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed public/assets/MOBILTYDATA_logo_purple_M.webp
Binary file not shown.
Binary file added public/favicon-16x16.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/favicon-32x32.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/favicon.ico
Binary file not shown.
15 changes: 0 additions & 15 deletions public/manifest.json

This file was deleted.

21 changes: 21 additions & 0 deletions public/site.webmanifest
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "Mobility Database",
"short_name": "MobilityDB",
"description": "Access GTFS, GTFS Realtime, GBFS transit data with over 4,000 feeds from 70+ countries.",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We might want to update these numbers to 6000 feeds and 99 countries ;-)

"icons": [
{
"src": "/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#3959fa",
"background_color": "#ffffff",
"display": "standalone",
"start_url": "/"
}
27 changes: 16 additions & 11 deletions src/app/[locale]/feeds/[feedDataType]/[feedId]/authed/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { type ReactElement } from 'react';
import FeedView from '../../../../../screens/Feed/FeedView';
import FeedJsonLd from '../lib/FeedJsonLd';
import type { Metadata, ResolvingMetadata } from 'next';
import { getTranslations } from 'next-intl/server';
import { fetchCompleteFeedData } from '../lib/feed-data';
Expand All @@ -22,8 +23,6 @@ export async function generateMetadata(
return generateFeedMetadata({
feed: feedData?.feed,
t,
gtfsFeeds: feedData?.relatedFeeds ?? [],
gtfsRtFeeds: feedData?.relatedGtfsRtFeeds ?? [],
});
}

Expand Down Expand Up @@ -57,14 +56,20 @@ export default async function AuthedFeedPage({
} = feedData;

return (
<FeedView
feed={feed}
feedDataType={feedDataType}
initialDatasets={initialDatasets}
relatedFeeds={relatedFeeds}
relatedGtfsRtFeeds={relatedGtfsRtFeeds}
totalRoutes={totalRoutes}
routeTypes={routeTypes}
/>
<>
<FeedJsonLd
feed={feed}
relatedFeeds={relatedFeeds}
relatedGtfsRtFeeds={relatedGtfsRtFeeds}
/>
<FeedView
feed={feed}
initialDatasets={initialDatasets}
relatedFeeds={relatedFeeds}
relatedGtfsRtFeeds={relatedGtfsRtFeeds}
totalRoutes={totalRoutes}
routeTypes={routeTypes}
/>
</>
);
}
61 changes: 61 additions & 0 deletions src/app/[locale]/feeds/[feedDataType]/[feedId]/lib/FeedJsonLd.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { type ReactElement } from 'react';
import { getTranslations } from 'next-intl/server';
import type {
AllFeedType,
GTFSFeedType,
GTFSRTFeedType,
} from '../../../../../services/feeds/utils';
import {
formatProvidersSorted,
generateDescriptionMetaTag,
} from '../../../../../screens/Feed/Feed.functions';
import generateFeedStructuredData from './generate-feed-metadata';

interface FeedJsonLdProps {
feed: AllFeedType;
relatedFeeds?: AllFeedType[];
relatedGtfsRtFeeds?: GTFSRTFeedType[];
}

/**
* Server component that renders JSON-LD structured data for SEO.
* Injects a <script type="application/ld+json"> tag directly into the page,
* following the Next.js App Router recommendation:
* https://nextjs.org/docs/app/guides/json-ld
*/
export default async function FeedJsonLd({
feed,
relatedFeeds,
relatedGtfsRtFeeds,
}: FeedJsonLdProps): Promise<ReactElement | null> {
if (feed == null) return null;

const t = await getTranslations();

const sortedProviders = formatProvidersSorted(feed?.provider ?? '');
const feedDataType = feed.data_type;
const description = generateDescriptionMetaTag(
t,
sortedProviders,
feedDataType as 'gtfs' | 'gtfs_rt' | 'gbfs',
(feed as { feed_name?: string })?.feed_name,
);

const structuredData = generateFeedStructuredData(
feed,
description,
relatedFeeds,
relatedGtfsRtFeeds as GTFSFeedType[],
Comment thread
Alessandro100 marked this conversation as resolved.
Outdated
);

if (structuredData == null) return null;

return (
<script
type='application/ld+json'
dangerouslySetInnerHTML={{
__html: JSON.stringify(structuredData).replace(/</g, '\\u003c'),
}}
/>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ function getBasicStructuredData(
const structuredData: StructureDataInterface = {
'@context': 'https://schema.org',
'@type': 'Dataset',
isAccessibleForFree: true,
name: `${dataTypeNaming ?? ''} Feed for ${feed?.provider}`,
description,
url: `https://mobilitydatabase.org/feeds/${feed?.data_type}/${feed?.id}`,
Expand Down Expand Up @@ -213,6 +214,8 @@ function getGtfsRtStructuredData(
(structuredGtfsRtData.hasPart as unknown[]).push({
'@type': 'Dataset',
name: `GTFS Static Feed for ${feed?.provider}`,
description: `The GTFS static feed associated with this GTFS Realtime feed, if available.`,
isAccessibleForFree: true,
url: `https://mobilitydatabase.org/feeds/gtfs/${associatedGtfsFeed.id}`,
distribution: {
'@type': 'DataDownload',
Expand All @@ -237,6 +240,8 @@ function getGtfsRtStructuredData(

(structuredGtfsRtData.hasPart as unknown[]).push({
'@type': 'Dataset',
isAccessibleForFree: true,
description: `A related GTFS Realtime feed for the same provider, with entity types ${relatedFeed?.entity_types?.join(', ')}`,
name,
url: `https://mobilitydatabase.org/feeds/gtfs_rt/${relatedFeed?.id}`,
distribution: {
Expand Down Expand Up @@ -278,23 +283,17 @@ export default function generateFeedStructuredData(
interface GenerateFeedMetadataParams {
feed: AllFeedType | undefined;
t: (key: string) => string;
gtfsFeeds?: GTFSFeedType[];
gtfsRtFeeds?: GTFSRTFeedType[];
}

/**
* Shared metadata generation logic for feed pages (authed and static).
*
* @param feed - The feed data\
* @param feed - The feed data
* @param t - Translation function
* @param gtfsFeeds - Related GTFS feeds (for GTFS-RT)
* @param gtfsRtFeeds - Related GTFS-RT feeds
*/
export function generateFeedMetadata({
feed,
t,
gtfsFeeds = [],
gtfsRtFeeds = [],
}: GenerateFeedMetadataParams): Metadata {
if (feed == null) {
return {
Expand All @@ -316,14 +315,6 @@ export function generateFeedMetadata({
(feed as { feed_name?: string })?.feed_name,
);

// Generate structured data for SEO
const structuredData = generateFeedStructuredData(
feed,
description,
gtfsFeeds,
gtfsRtFeeds,
);

return {
title,
description,
Expand All @@ -340,13 +331,7 @@ export function generateFeedMetadata({
description,
},
alternates: {
canonical: `/feeds/${feedDataType}/${feedId}`,
},
other: {
// Structured data for JSON-LD
...(structuredData != null && {
'script:ld+json': JSON.stringify(structuredData),
}),
canonical: `https://mobilitydatabase.org/feeds/${feedDataType}/${feedId}`,
},
};
}
7 changes: 7 additions & 0 deletions src/app/[locale]/feeds/[feedDataType]/[feedId]/loading.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import { Box, Container, Skeleton } from '@mui/material';

/**
* Loading page streams content as it becomes available, providing a faster time-to-interactive and better user experience.
* NextJs automatically automatically detects user agents to choose between blocking and streaming behavior.
* Since streaming is server-rendered, it does not impact SEO
* ref: https://nextjs.org/docs/app/api-reference/file-conventions/loading
*/

export default function Loading(): React.ReactElement {
return (
<Container
Expand Down
37 changes: 22 additions & 15 deletions src/app/[locale]/feeds/[feedDataType]/[feedId]/static/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { type ReactElement } from 'react';
import FeedView from '../../../../../screens/Feed/FeedView';
import FeedJsonLd from '../lib/FeedJsonLd';
import type { Metadata, ResolvingMetadata } from 'next';
import { notFound } from 'next/navigation';
import { getTranslations } from 'next-intl/server';
import { fetchGuestFeedData } from '../lib/guest-feed-data';
import { generateFeedMetadata } from '../lib/generate-feed-metadata';
Expand All @@ -15,16 +17,15 @@ export async function generateMetadata(
parent: ResolvingMetadata,
): Promise<Metadata> {
const { feedId, feedDataType } = await params;
const t = await getTranslations();

// Use guest (ISR-safe) fetcher - same cache as page component
const feedData = await fetchGuestFeedData(feedDataType, feedId);
const [t, feedData] = await Promise.all([
getTranslations(),
fetchGuestFeedData(feedDataType, feedId),
]);

return generateFeedMetadata({
feed: feedData.feed,
t,
gtfsFeeds: feedData.relatedFeeds,
gtfsRtFeeds: feedData.relatedGtfsRtFeeds,
});
}

Expand All @@ -40,7 +41,7 @@ export default async function StaticFeedPage({
} catch (e) {
// Layout should have caught non-existent feeds, but handle edge case
console.error(`[StaticFeedPage] Failed to fetch feed ${feedId}:`, e);
return <div>Feed not found</div>;
notFound();
}

const {
Expand All @@ -53,14 +54,20 @@ export default async function StaticFeedPage({
} = feedData;

return (
<FeedView
feed={feed}
feedDataType={feedDataType}
initialDatasets={initialDatasets}
relatedFeeds={relatedFeeds}
relatedGtfsRtFeeds={relatedGtfsRtFeeds}
totalRoutes={totalRoutes}
routeTypes={routeTypes}
/>
<>
<FeedJsonLd
feed={feed}
relatedFeeds={relatedFeeds}
relatedGtfsRtFeeds={relatedGtfsRtFeeds}
/>
<FeedView
feed={feed}
initialDatasets={initialDatasets}
relatedFeeds={relatedFeeds}
relatedGtfsRtFeeds={relatedGtfsRtFeeds}
totalRoutes={totalRoutes}
routeTypes={routeTypes}
/>
</>
);
}
12 changes: 11 additions & 1 deletion src/app/[locale]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,23 @@ import { Container } from '@mui/material';
import { type Locale, routing } from '../../i18n/routing';

export const metadata = {
title: 'Mobility Database',
title:
'MobilityDatabase | The Global Catalog of GTFS, GTFS-Realtime & GBFS Feeds',
description:
"Access GTFS, GTFS Realtime, GBFS transit data with over 4,000 feeds from 70+ countries on the web's leading transit data platform.",
robots:
process.env.VERCEL_ENV === 'production'
? 'index, follow'
: 'noindex, nofollow',
icons: {
icon: [
{ url: '/favicon-16x16.png', sizes: '16x16', type: 'image/png' },
{ url: '/favicon-32x32.png', sizes: '32x32', type: 'image/png' },
],
shortcut: '/favicon.ico',
apple: [{ url: '/apple-touch-icon.png' }],
},
manifest: '/site.webmanifest',
};

export const viewport = {
Expand Down
3 changes: 2 additions & 1 deletion src/app/[locale]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ interface PageProps {
}

export const metadata: Metadata = {
title: 'Mobility Database',
title:
'MobilityDatabase | The Global Catalog of GTFS, GTFS-Realtime & GBFS Feeds',
description:
'Discover open public transit data worldwide. Mobility Database provides GTFS, GTFS-RT, and GBFS feeds to help developers, cities, and agencies build better mobility tools.',
applicationName: 'Mobility Database',
Expand Down
Loading
Loading