mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
feat(seo): Add metadata and SEO improvements to passenger portal
This commit is contained in:
1
.github/workflows/deploy.yml
vendored
1
.github/workflows/deploy.yml
vendored
@@ -2,7 +2,6 @@ name: Deploy Stacks
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
|
||||||
- dev
|
- dev
|
||||||
- staging
|
- staging
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|||||||
21
apps/edr-passenger-web/portal/src/app/about/layout.tsx
Normal file
21
apps/edr-passenger-web/portal/src/app/about/layout.tsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import type { Metadata } from 'next';
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'About EDR – Connecting East Africa by Rail',
|
||||||
|
description:
|
||||||
|
'Learn about the Ethio-Djibouti Railway (EDR): our mission to connect Ethiopia and Djibouti with comfortable, affordable, and sustainable train travel.',
|
||||||
|
alternates: {
|
||||||
|
canonical: '/about',
|
||||||
|
},
|
||||||
|
openGraph: {
|
||||||
|
title: 'About EDR – Connecting East Africa by Rail',
|
||||||
|
description:
|
||||||
|
'Our mission is to provide reliable, affordable, and comfortable train travel connecting Ethiopia and Djibouti.',
|
||||||
|
url: '/about',
|
||||||
|
images: [{ url: '/edr-banner.jpg', width: 1200, height: 630, alt: 'EDR Railway – About Us' }],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function AboutLayout({ children }: { children: React.ReactNode }) {
|
||||||
|
return children;
|
||||||
|
}
|
||||||
21
apps/edr-passenger-web/portal/src/app/contact/layout.tsx
Normal file
21
apps/edr-passenger-web/portal/src/app/contact/layout.tsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import type { Metadata } from 'next';
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'Contact EDR – Get in Touch',
|
||||||
|
description:
|
||||||
|
'Contact the Ethio-Djibouti Railway (EDR) support team. We are available to assist with your train journey.',
|
||||||
|
alternates: {
|
||||||
|
canonical: '/contact',
|
||||||
|
},
|
||||||
|
openGraph: {
|
||||||
|
title: 'Contact EDR – Get in Touch',
|
||||||
|
description:
|
||||||
|
'Reach the EDR support team by phone, email, or in person. We are available to assist with your train journey.',
|
||||||
|
url: '/contact',
|
||||||
|
images: [{ url: '/edr-banner.jpg', width: 1200, height: 630, alt: 'EDR Railway – Contact Us' }],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function ContactLayout({ children }: { children: React.ReactNode }) {
|
||||||
|
return children;
|
||||||
|
}
|
||||||
28
apps/edr-passenger-web/portal/src/app/guide/layout.tsx
Normal file
28
apps/edr-passenger-web/portal/src/app/guide/layout.tsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import type { Metadata } from 'next';
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'How to Book a Train Ticket – EDR Step-by-Step Guide',
|
||||||
|
description:
|
||||||
|
'Step-by-step guide to booking an EDR train ticket online: search trains, select your seat class, enter passenger details, and complete payment.',
|
||||||
|
alternates: {
|
||||||
|
canonical: '/guide',
|
||||||
|
},
|
||||||
|
openGraph: {
|
||||||
|
title: 'How to Book a Train Ticket – EDR Step-by-Step Guide',
|
||||||
|
description:
|
||||||
|
'Follow our simple guide to book your EDR train journey in minutes.',
|
||||||
|
url: '/guide',
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
url: '/edr-banner.jpg',
|
||||||
|
width: 1200,
|
||||||
|
height: 630,
|
||||||
|
alt: 'EDR Railway – How to Book a Train Ticket',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function GuideLayout({ children }: { children: React.ReactNode }) {
|
||||||
|
return children;
|
||||||
|
}
|
||||||
23
apps/edr-passenger-web/portal/src/app/help/layout.tsx
Normal file
23
apps/edr-passenger-web/portal/src/app/help/layout.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import type { Metadata } from 'next';
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'Help & FAQs – EDR Train Booking',
|
||||||
|
description:
|
||||||
|
'Find answers to common questions about booking EDR train tickets: how to book, passenger pricing, identity verification, seat classes, payment methods, and more.',
|
||||||
|
alternates: {
|
||||||
|
canonical: '/help',
|
||||||
|
},
|
||||||
|
openGraph: {
|
||||||
|
title: 'Help & FAQs – EDR Train Booking',
|
||||||
|
description:
|
||||||
|
'Answers to your questions about booking EDR train tickets, payment methods, identity verification, and seat selection.',
|
||||||
|
url: '/help',
|
||||||
|
images: [
|
||||||
|
{ url: '/edr-banner.jpg', width: 1200, height: 630, alt: 'EDR Railway – Help & FAQs' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function HelpLayout({ children }: { children: React.ReactNode }) {
|
||||||
|
return children;
|
||||||
|
}
|
||||||
@@ -7,10 +7,61 @@ import MobileTopBar from '@/components/MobileTopBar';
|
|||||||
import BottomTabBar from '@/components/BottomTabBar';
|
import BottomTabBar from '@/components/BottomTabBar';
|
||||||
import { LoadingIndicator } from '@/components/LoadingIndicator';
|
import { LoadingIndicator } from '@/components/LoadingIndicator';
|
||||||
import SupportWidget from '@/features/support/SupportWidgetLazy';
|
import SupportWidget from '@/features/support/SupportWidgetLazy';
|
||||||
|
import { JsonLd } from '@/components/JsonLd';
|
||||||
|
|
||||||
|
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL ?? 'https://passenger.edrsc.com';
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'EDR Passenger Portal - Book your train journey',
|
metadataBase: new URL(siteUrl),
|
||||||
description: 'Book train tickets on the Ethio-Djibouti Railway',
|
title: {
|
||||||
|
default: 'EDR Passenger Portal – Book Train Tickets Online',
|
||||||
|
template: '%s | EDR Passenger Portal',
|
||||||
|
},
|
||||||
|
description:
|
||||||
|
'Book train tickets on the Ethio-Djibouti Railway. Travel between Ethiopia and Djibouti with easy online booking, seat selection, and multiple payment options.',
|
||||||
|
authors: [{ name: 'EDR – Ethio-Djibouti Railway' }],
|
||||||
|
creator: 'EDR – Ethio-Djibouti Railway',
|
||||||
|
publisher: 'EDR – Ethio-Djibouti Railway',
|
||||||
|
robots: {
|
||||||
|
index: true,
|
||||||
|
follow: true,
|
||||||
|
googleBot: {
|
||||||
|
index: true,
|
||||||
|
follow: true,
|
||||||
|
'max-snippet': -1,
|
||||||
|
'max-image-preview': 'large',
|
||||||
|
'max-video-preview': -1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
openGraph: {
|
||||||
|
type: 'website',
|
||||||
|
locale: 'en_US',
|
||||||
|
url: siteUrl,
|
||||||
|
siteName: 'EDR Passenger Portal',
|
||||||
|
title: 'EDR Passenger Portal – Book Train Tickets Online',
|
||||||
|
description:
|
||||||
|
'Book train tickets on the Ethio-Djibouti Railway. Comfortable seats, flexible payment options, and easy online booking.',
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
url: '/edr-banner.jpg',
|
||||||
|
width: 1200,
|
||||||
|
height: 630,
|
||||||
|
alt: 'EDR Ethio-Djibouti Railway – Book your train journey',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
card: 'summary_large_image',
|
||||||
|
title: 'EDR Passenger Portal – Book Train Tickets Online',
|
||||||
|
description:
|
||||||
|
'Book train tickets on the Ethio-Djibouti Railway. Comfortable seats, flexible payment options, and easy online booking.',
|
||||||
|
images: ['/edr-banner.jpg'],
|
||||||
|
},
|
||||||
|
icons: {
|
||||||
|
icon: '/edr-logo.png',
|
||||||
|
shortcut: '/edr-logo.png',
|
||||||
|
apple: '/edr-logo.png',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// viewportFit: 'cover' lets fixed bottom bars (e.g. the payment page's Pay
|
// viewportFit: 'cover' lets fixed bottom bars (e.g. the payment page's Pay
|
||||||
@@ -29,8 +80,48 @@ export default function RootLayout({
|
|||||||
}) {
|
}) {
|
||||||
// Nonce set per-request by middleware; required for this inline script under the CSP.
|
// Nonce set per-request by middleware; required for this inline script under the CSP.
|
||||||
const nonce = headers().get('x-nonce') ?? undefined;
|
const nonce = headers().get('x-nonce') ?? undefined;
|
||||||
|
|
||||||
|
const organizationSchema = {
|
||||||
|
'@context': 'https://schema.org',
|
||||||
|
'@type': 'Organization',
|
||||||
|
name: 'Ethio-Djibouti Railway (EDR)',
|
||||||
|
url: siteUrl,
|
||||||
|
logo: `${siteUrl}/edr-logo.png`,
|
||||||
|
contactPoint: {
|
||||||
|
'@type': 'ContactPoint',
|
||||||
|
telephone: '9546',
|
||||||
|
contactType: 'customer service',
|
||||||
|
availableLanguage: ['English', 'Amharic'],
|
||||||
|
},
|
||||||
|
address: {
|
||||||
|
'@type': 'PostalAddress',
|
||||||
|
addressLocality: 'Addis Ababa',
|
||||||
|
addressCountry: 'ET',
|
||||||
|
},
|
||||||
|
sameAs: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const websiteSchema = {
|
||||||
|
'@context': 'https://schema.org',
|
||||||
|
'@type': 'WebSite',
|
||||||
|
name: 'EDR Passenger Portal',
|
||||||
|
url: siteUrl,
|
||||||
|
potentialAction: {
|
||||||
|
'@type': 'SearchAction',
|
||||||
|
target: {
|
||||||
|
'@type': 'EntryPoint',
|
||||||
|
urlTemplate: `${siteUrl}/booking/search?q={search_term_string}`,
|
||||||
|
},
|
||||||
|
'query-input': 'required name=search_term_string',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html lang="en" suppressHydrationWarning>
|
<html lang="en" dir="ltr" suppressHydrationWarning>
|
||||||
|
<head>
|
||||||
|
<JsonLd data={organizationSchema} />
|
||||||
|
<JsonLd data={websiteSchema} />
|
||||||
|
</head>
|
||||||
<body className="font-sans antialiased">
|
<body className="font-sans antialiased">
|
||||||
<script
|
<script
|
||||||
nonce={nonce}
|
nonce={nonce}
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import type { Metadata } from 'next';
|
||||||
|
|
||||||
|
interface PackageSummary {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description?: string | null;
|
||||||
|
outboundSchedule?: {
|
||||||
|
originStation?: { name?: string };
|
||||||
|
destinationStation?: { name?: string };
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch a minimal package summary server-side so we can generate per-package
|
||||||
|
* metadata without requiring the page itself to be a Server Component.
|
||||||
|
* Errors are swallowed so the page still renders even if the API is unreachable
|
||||||
|
* at build/request time.
|
||||||
|
*/
|
||||||
|
async function fetchPackageSummary(id: string): Promise<PackageSummary | null> {
|
||||||
|
try {
|
||||||
|
const apiUrl = process.env.NEXT_PUBLIC_API_URL ?? 'http://localhost:4000';
|
||||||
|
const res = await fetch(`${apiUrl}/packages/${id}`, {
|
||||||
|
next: { revalidate: 3600 },
|
||||||
|
});
|
||||||
|
if (!res.ok) return null;
|
||||||
|
return await res.json();
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateMetadata({
|
||||||
|
params,
|
||||||
|
}: {
|
||||||
|
params: { id: string };
|
||||||
|
}): Promise<Metadata> {
|
||||||
|
const pkg = await fetchPackageSummary(params.id);
|
||||||
|
|
||||||
|
if (!pkg) {
|
||||||
|
return {
|
||||||
|
title: 'Train Package – EDR',
|
||||||
|
description:
|
||||||
|
'View details and book this Ethio-Djibouti Railway travel package with seat selection and flexible payment options.',
|
||||||
|
robots: { index: true, follow: true },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const origin = pkg.outboundSchedule?.originStation?.name?.trim();
|
||||||
|
const destination = pkg.outboundSchedule?.destinationStation?.name?.trim();
|
||||||
|
const routeLabel = origin && destination ? ` – ${origin} to ${destination}` : '';
|
||||||
|
const title = `${pkg.name}${routeLabel} | EDR`;
|
||||||
|
const description =
|
||||||
|
pkg.description?.trim() ||
|
||||||
|
`Book the "${pkg.name}" train package on the Ethio-Djibouti Railway${routeLabel ? ` (${origin} → ${destination})` : ''}. Choose your seat class and complete payment online.`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
alternates: {
|
||||||
|
canonical: `/packages/${params.id}`,
|
||||||
|
},
|
||||||
|
openGraph: {
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
url: `/packages/${params.id}`,
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
url: '/edr-banner.jpg',
|
||||||
|
width: 1200,
|
||||||
|
height: 630,
|
||||||
|
alt: `EDR Railway – ${pkg.name}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PackageDetailLayout({ children }: { children: React.ReactNode }) {
|
||||||
|
return children;
|
||||||
|
}
|
||||||
23
apps/edr-passenger-web/portal/src/app/packages/layout.tsx
Normal file
23
apps/edr-passenger-web/portal/src/app/packages/layout.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import type { Metadata } from 'next';
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'Train Journey Packages – EDR',
|
||||||
|
description:
|
||||||
|
'Browse curated Ethio-Djibouti Railway travel packages. Choose from one-way and round-trip options with a range of seat classes at competitive fares.',
|
||||||
|
alternates: {
|
||||||
|
canonical: '/packages',
|
||||||
|
},
|
||||||
|
openGraph: {
|
||||||
|
title: 'Train Journey Packages – EDR',
|
||||||
|
description:
|
||||||
|
'Curated one-way and round-trip train packages on the Ethio-Djibouti Railway with multiple seat classes and flexible payment options.',
|
||||||
|
url: '/packages',
|
||||||
|
images: [
|
||||||
|
{ url: '/edr-banner.jpg', width: 1200, height: 630, alt: 'EDR Railway – Travel Packages' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function PackagesLayout({ children }: { children: React.ReactNode }) {
|
||||||
|
return children;
|
||||||
|
}
|
||||||
@@ -1,8 +1,25 @@
|
|||||||
import { Suspense } from 'react';
|
import { Suspense } from 'react';
|
||||||
|
import type { Metadata } from 'next';
|
||||||
import dynamic from 'next/dynamic';
|
import dynamic from 'next/dynamic';
|
||||||
import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query';
|
import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query';
|
||||||
import SearchPage from '@/app/booking/search/page';
|
import SearchPage from '@/app/booking/search/page';
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'EDR Passenger Portal – Book Train Tickets Online',
|
||||||
|
description:
|
||||||
|
'Book train tickets on the Ethio-Djibouti Railway. Search trains between Ethiopia and Djibouti with easy online booking, seat selection, and flexible payment options.',
|
||||||
|
alternates: {
|
||||||
|
canonical: '/',
|
||||||
|
},
|
||||||
|
openGraph: {
|
||||||
|
title: 'EDR Passenger Portal – Book Train Tickets Online',
|
||||||
|
description:
|
||||||
|
'Search and book train tickets across the Ethio-Djibouti Railway. Multiple seat classes available.',
|
||||||
|
url: '/',
|
||||||
|
type: 'website',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
// Below-the-fold on the landing page — code-split out of the initial bundle
|
// Below-the-fold on the landing page — code-split out of the initial bundle
|
||||||
// needed to render/hydrate the (above-the-fold) search form.
|
// needed to render/hydrate the (above-the-fold) search form.
|
||||||
const PackagesSection = dynamic(() => import('@/components/PackagesSection'));
|
const PackagesSection = dynamic(() => import('@/components/PackagesSection'));
|
||||||
|
|||||||
37
apps/edr-passenger-web/portal/src/app/robots.ts
Normal file
37
apps/edr-passenger-web/portal/src/app/robots.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import type { MetadataRoute } from 'next';
|
||||||
|
|
||||||
|
export default function robots(): MetadataRoute.Robots {
|
||||||
|
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL ?? 'https://passenger.edrsc.com';
|
||||||
|
|
||||||
|
return {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
userAgent: '*',
|
||||||
|
allow: [
|
||||||
|
'/',
|
||||||
|
'/about',
|
||||||
|
'/contact',
|
||||||
|
'/services',
|
||||||
|
'/packages',
|
||||||
|
'/packages/',
|
||||||
|
'/help',
|
||||||
|
'/guide',
|
||||||
|
],
|
||||||
|
disallow: [
|
||||||
|
'/booking/',
|
||||||
|
'/login',
|
||||||
|
'/register',
|
||||||
|
'/forgot-password',
|
||||||
|
'/reset-password',
|
||||||
|
'/set-password',
|
||||||
|
'/verify-account',
|
||||||
|
'/fayda-setup',
|
||||||
|
'/profile',
|
||||||
|
'/go/',
|
||||||
|
'/api/',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
sitemap: `${baseUrl}/sitemap.xml`,
|
||||||
|
};
|
||||||
|
}
|
||||||
23
apps/edr-passenger-web/portal/src/app/services/layout.tsx
Normal file
23
apps/edr-passenger-web/portal/src/app/services/layout.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import type { Metadata } from 'next';
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'EDR Train Services – Booking, Seats, Payments & More',
|
||||||
|
description:
|
||||||
|
'Explore all services offered by the Ethio-Djibouti Railway: easy online booking, seat selection, flexible payment options, customer support, and live train tracking.',
|
||||||
|
alternates: {
|
||||||
|
canonical: '/services',
|
||||||
|
},
|
||||||
|
openGraph: {
|
||||||
|
title: 'EDR Train Services – Booking, Seats, Payments & More',
|
||||||
|
description:
|
||||||
|
'Book seats, pay with your preferred method, track your train live, and get support on the Ethio-Djibouti Railway.',
|
||||||
|
url: '/services',
|
||||||
|
images: [
|
||||||
|
{ url: '/edr-banner.jpg', width: 1200, height: 630, alt: 'EDR Railway – Our Services' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function ServicesLayout({ children }: { children: React.ReactNode }) {
|
||||||
|
return children;
|
||||||
|
}
|
||||||
59
apps/edr-passenger-web/portal/src/app/sitemap.ts
Normal file
59
apps/edr-passenger-web/portal/src/app/sitemap.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import type { MetadataRoute } from 'next';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Static sitemap covering all publicly indexable pages.
|
||||||
|
* Dynamic package pages are omitted here because they are fetched at runtime;
|
||||||
|
* if you want to include them, replace this with an async function that calls
|
||||||
|
* the packages API and maps over the results.
|
||||||
|
*/
|
||||||
|
export default function sitemap(): MetadataRoute.Sitemap {
|
||||||
|
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL ?? 'https://passenger.edrsc.com';
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
const staticPages: MetadataRoute.Sitemap = [
|
||||||
|
{
|
||||||
|
url: baseUrl,
|
||||||
|
lastModified: now,
|
||||||
|
changeFrequency: 'daily',
|
||||||
|
priority: 1.0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: `${baseUrl}/packages`,
|
||||||
|
lastModified: now,
|
||||||
|
changeFrequency: 'daily',
|
||||||
|
priority: 0.9,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: `${baseUrl}/services`,
|
||||||
|
lastModified: now,
|
||||||
|
changeFrequency: 'monthly',
|
||||||
|
priority: 0.8,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: `${baseUrl}/about`,
|
||||||
|
lastModified: now,
|
||||||
|
changeFrequency: 'monthly',
|
||||||
|
priority: 0.7,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: `${baseUrl}/help`,
|
||||||
|
lastModified: now,
|
||||||
|
changeFrequency: 'weekly',
|
||||||
|
priority: 0.7,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: `${baseUrl}/guide`,
|
||||||
|
lastModified: now,
|
||||||
|
changeFrequency: 'monthly',
|
||||||
|
priority: 0.6,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: `${baseUrl}/contact`,
|
||||||
|
lastModified: now,
|
||||||
|
changeFrequency: 'yearly',
|
||||||
|
priority: 0.5,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return staticPages;
|
||||||
|
}
|
||||||
12
apps/edr-passenger-web/portal/src/components/JsonLd.tsx
Normal file
12
apps/edr-passenger-web/portal/src/components/JsonLd.tsx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
/**
|
||||||
|
* Injects JSON-LD structured data into the page <head>.
|
||||||
|
* Usage: <JsonLd data={{ "@context": "https://schema.org", ... }} />
|
||||||
|
*/
|
||||||
|
export function JsonLd({ data }: { data: Record<string, unknown> }) {
|
||||||
|
return (
|
||||||
|
<script
|
||||||
|
type="application/ld+json"
|
||||||
|
dangerouslySetInnerHTML={{ __html: JSON.stringify(data) }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ const PRESERVED_ROUTES = [
|
|||||||
'/help',
|
'/help',
|
||||||
'/guide',
|
'/guide',
|
||||||
'/services',
|
'/services',
|
||||||
|
'/packages',
|
||||||
'/go/',
|
'/go/',
|
||||||
'/packages/',
|
'/packages/',
|
||||||
];
|
];
|
||||||
@@ -99,10 +100,10 @@ export function middleware(request: NextRequest) {
|
|||||||
pathname.includes('.') ||
|
pathname.includes('.') ||
|
||||||
pathname === '/'
|
pathname === '/'
|
||||||
) {
|
) {
|
||||||
// Next.js internals, static files, API routes, and home: no redirect, just render.
|
// Next.js internals, static files, API routes, and home.
|
||||||
response = render();
|
response = render();
|
||||||
} else if (PRESERVED_ROUTES.some((r) => pathname.startsWith(r))) {
|
} else if (PRESERVED_ROUTES.some((r) => pathname.startsWith(r))) {
|
||||||
// On hard refresh of a preserved route, let it through.
|
// Public and auth routes: let them through.
|
||||||
response = render();
|
response = render();
|
||||||
} else {
|
} else {
|
||||||
response = render();
|
response = render();
|
||||||
@@ -112,6 +113,24 @@ export function middleware(request: NextRequest) {
|
|||||||
// Anti-clickjacking. `frame-ancestors 'none'` (in the CSP above) is the modern control;
|
// Anti-clickjacking. `frame-ancestors 'none'` (in the CSP above) is the modern control;
|
||||||
// X-Frame-Options: DENY is the legacy equivalent for older browsers and scanners.
|
// X-Frame-Options: DENY is the legacy equivalent for older browsers and scanners.
|
||||||
response.headers.set('X-Frame-Options', 'DENY');
|
response.headers.set('X-Frame-Options', 'DENY');
|
||||||
|
|
||||||
|
// Tell crawlers not to index private/transactional routes.
|
||||||
|
const NOINDEX_PREFIXES = [
|
||||||
|
'/booking/',
|
||||||
|
'/login',
|
||||||
|
'/register',
|
||||||
|
'/forgot-password',
|
||||||
|
'/reset-password',
|
||||||
|
'/set-password',
|
||||||
|
'/verify-account',
|
||||||
|
'/fayda-setup',
|
||||||
|
'/profile',
|
||||||
|
'/go/',
|
||||||
|
];
|
||||||
|
if (NOINDEX_PREFIXES.some((p) => pathname.startsWith(p))) {
|
||||||
|
response.headers.set('X-Robots-Tag', 'noindex, nofollow');
|
||||||
|
}
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user