Wire SEO and JSON-LD into all 6 Online Board route pages
Each route page now renders <SeoHead> with title, description, canonical, hreflang, OG, and Twitter card from the SEO builders. Search pages render <JsonLdRenderer> with ItemList of Flight schemas. Details page renders Flight JSON-LD. Barrel exports updated with 2F SEO and JSON-LD functions.
This commit is contained in:
@@ -91,6 +91,13 @@ vi.mock("../hooks/useLiveFlightDetails.js", () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("@/i18n/provider.js", () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
i18n: { language: "ru" },
|
||||
}),
|
||||
}));
|
||||
|
||||
describe("OnlineBoardDetailsPage", () => {
|
||||
beforeEach(() => {
|
||||
mockState = {
|
||||
@@ -101,7 +108,7 @@ describe("OnlineBoardDetailsPage", () => {
|
||||
});
|
||||
|
||||
it("renders flight details", () => {
|
||||
render(<OnlineBoardDetailsPage flightId={mockFlightId} />);
|
||||
render(<OnlineBoardDetailsPage flightId={mockFlightId} locale="ru" canonicalOrigin="https://www.aeroflot.ru" />);
|
||||
expect(screen.getByTestId("flight-details")).toBeTruthy();
|
||||
// "SU 100" appears in both the header and FlightCard
|
||||
expect(screen.getAllByText("SU 100").length).toBeGreaterThanOrEqual(1);
|
||||
@@ -109,42 +116,42 @@ describe("OnlineBoardDetailsPage", () => {
|
||||
|
||||
it("renders loading skeleton", () => {
|
||||
mockState = { flight: null, loading: true, error: null };
|
||||
render(<OnlineBoardDetailsPage flightId={mockFlightId} />);
|
||||
render(<OnlineBoardDetailsPage flightId={mockFlightId} locale="ru" canonicalOrigin="https://www.aeroflot.ru" />);
|
||||
expect(screen.queryByTestId("flight-details")).toBeNull();
|
||||
});
|
||||
|
||||
it("renders error state", () => {
|
||||
mockState = { flight: null, loading: false, error: new Error("fail") };
|
||||
render(<OnlineBoardDetailsPage flightId={mockFlightId} />);
|
||||
render(<OnlineBoardDetailsPage flightId={mockFlightId} locale="ru" canonicalOrigin="https://www.aeroflot.ru" />);
|
||||
expect(screen.getByTestId("flight-details-error")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders not-found state", () => {
|
||||
mockState = { flight: null, loading: false, error: null };
|
||||
render(<OnlineBoardDetailsPage flightId={mockFlightId} />);
|
||||
render(<OnlineBoardDetailsPage flightId={mockFlightId} locale="ru" canonicalOrigin="https://www.aeroflot.ru" />);
|
||||
expect(screen.getByTestId("flight-details-not-found")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders flight legs", () => {
|
||||
render(<OnlineBoardDetailsPage flightId={mockFlightId} />);
|
||||
render(<OnlineBoardDetailsPage flightId={mockFlightId} locale="ru" canonicalOrigin="https://www.aeroflot.ru" />);
|
||||
expect(screen.getByTestId("flight-legs")).toBeTruthy();
|
||||
expect(screen.getByTestId("flight-leg-0")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("displays departure and arrival stations", () => {
|
||||
render(<OnlineBoardDetailsPage flightId={mockFlightId} />);
|
||||
render(<OnlineBoardDetailsPage flightId={mockFlightId} locale="ru" canonicalOrigin="https://www.aeroflot.ru" />);
|
||||
// SVO and JFK appear in both FlightCard and FlightLegs sections
|
||||
expect(screen.getAllByText("SVO").length).toBeGreaterThanOrEqual(1);
|
||||
expect(screen.getAllByText("JFK").length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it("displays aircraft info", () => {
|
||||
render(<OnlineBoardDetailsPage flightId={mockFlightId} />);
|
||||
render(<OnlineBoardDetailsPage flightId={mockFlightId} locale="ru" canonicalOrigin="https://www.aeroflot.ru" />);
|
||||
expect(screen.getByText("Aircraft: Boeing 777-300ER (77W)")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("displays flying time", () => {
|
||||
render(<OnlineBoardDetailsPage flightId={mockFlightId} />);
|
||||
render(<OnlineBoardDetailsPage flightId={mockFlightId} locale="ru" canonicalOrigin="https://www.aeroflot.ru" />);
|
||||
expect(screen.getByTestId("flying-time")).toBeTruthy();
|
||||
expect(screen.getByText("Total flying time: 10:30")).toBeTruthy();
|
||||
});
|
||||
|
||||
@@ -8,15 +8,24 @@
|
||||
*/
|
||||
|
||||
import type { FC } from "react";
|
||||
import { useTranslation } from "@/i18n/provider.js";
|
||||
import { FlightCard } from "@/ui/flights/FlightCard.js";
|
||||
import { FlightListSkeleton } from "@/ui/flights/FlightListSkeleton.js";
|
||||
import { SeoHead } from "@/ui/seo/SeoHead.js";
|
||||
import { JsonLdRenderer } from "@/shared/seo/json-ld.js";
|
||||
import { useFlightDetails } from "../hooks/useFlightDetails.js";
|
||||
import { useLiveFlightDetails } from "../hooks/useLiveFlightDetails.js";
|
||||
import { buildFlightDetailsSeo } from "../seo.js";
|
||||
import { buildFlightJsonLd } from "../json-ld.js";
|
||||
import type { IParsedFlightId, IFlightLeg } from "../types.js";
|
||||
|
||||
export interface OnlineBoardDetailsPageProps {
|
||||
/** Parsed flight identifier from the URL */
|
||||
flightId: IParsedFlightId;
|
||||
/** Current locale for SEO */
|
||||
locale: string;
|
||||
/** Canonical origin for SEO URLs */
|
||||
canonicalOrigin: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -127,7 +136,11 @@ function getLegs(flight: { routeType: string; leg?: IFlightLeg; legs?: IFlightLe
|
||||
*/
|
||||
export const OnlineBoardDetailsPage: FC<OnlineBoardDetailsPageProps> = ({
|
||||
flightId,
|
||||
locale,
|
||||
canonicalOrigin,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Fetch flight details
|
||||
const detailsParams = {
|
||||
flights: `${flightId.carrier}${flightId.flightNumber}${flightId.suffix ?? ""}`,
|
||||
@@ -166,8 +179,13 @@ export const OnlineBoardDetailsPage: FC<OnlineBoardDetailsPageProps> = ({
|
||||
const legs = getLegs(displayFlight);
|
||||
const flightNumber = `${displayFlight.flightId.carrier} ${displayFlight.flightId.flightNumber}`;
|
||||
|
||||
const seoProps = buildFlightDetailsSeo(t, displayFlight, locale, canonicalOrigin);
|
||||
const jsonLd = buildFlightJsonLd(displayFlight);
|
||||
|
||||
return (
|
||||
<div className="flight-details" data-testid="flight-details">
|
||||
<SeoHead {...seoProps} />
|
||||
<JsonLdRenderer data={jsonLd} />
|
||||
{/* Connection status */}
|
||||
<div className="flight-details__status" data-testid="connection-status">
|
||||
{connectionStatus === "live" && (
|
||||
|
||||
@@ -16,10 +16,12 @@ import type { FC } from "react";
|
||||
import { useCallback } from "react";
|
||||
import { useNavigate, useParams } from "@modern-js/runtime/router";
|
||||
import { FlightList } from "@/ui/flights/FlightList.js";
|
||||
import { JsonLdRenderer } from "@/shared/seo/json-ld.js";
|
||||
import { useOnlineBoard } from "../hooks/useOnlineBoard.js";
|
||||
import { useLiveBoardSearch } from "../hooks/useLiveBoardSearch.js";
|
||||
import { useCalendarDays } from "../hooks/useCalendarDays.js";
|
||||
import { buildOnlineBoardUrl } from "../url.js";
|
||||
import { buildFlightListJsonLd } from "../json-ld.js";
|
||||
import type { OnlineBoardParams } from "../url.js";
|
||||
import type { SearchFlightsParams, CalendarParams } from "../api.js";
|
||||
import type { FlightRequestType, ISimpleFlight } from "../types.js";
|
||||
@@ -184,8 +186,15 @@ export const OnlineBoardSearchPage: FC<OnlineBoardSearchPageProps> = ({
|
||||
// Use live flights when connected, otherwise fetched flights
|
||||
const displayFlights = connectionStatus === "live" ? liveFlights : flights;
|
||||
|
||||
// JSON-LD for search results (rendered once we have flights)
|
||||
const searchDescription = `Online board ${params.type} search results`;
|
||||
const jsonLd = displayFlights.length > 0
|
||||
? buildFlightListJsonLd(displayFlights, searchDescription)
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<div className="online-board-search" data-testid="online-board-search">
|
||||
{jsonLd && <JsonLdRenderer data={jsonLd} />}
|
||||
{/* Connection status indicator */}
|
||||
<div className="online-board-search__status" data-testid="connection-status">
|
||||
{connectionStatus === "live" && (
|
||||
|
||||
@@ -36,6 +36,20 @@ export type { UseLiveBoardSearchResult } from "./hooks/useLiveBoardSearch.js";
|
||||
export { useLiveFlightDetails } from "./hooks/useLiveFlightDetails.js";
|
||||
export type { UseLiveFlightDetailsResult } from "./hooks/useLiveFlightDetails.js";
|
||||
|
||||
// 2F — SEO builder functions
|
||||
export {
|
||||
buildOnlineBoardStartSeo,
|
||||
buildFlightSearchSeo,
|
||||
buildDepartureSearchSeo,
|
||||
buildArrivalSearchSeo,
|
||||
buildRouteSearchSeo,
|
||||
buildFlightDetailsSeo,
|
||||
} from "./seo.js";
|
||||
export type { TFunction, CityNames } from "./seo.js";
|
||||
|
||||
// 2F — JSON-LD builder functions
|
||||
export { buildFlightJsonLd, buildFlightListJsonLd } from "./json-ld.js";
|
||||
|
||||
// 2E — Feature-specific page components
|
||||
export { OnlineBoardStartPage } from "./components/OnlineBoardStartPage.js";
|
||||
export { OnlineBoardSearchPage } from "./components/OnlineBoardSearchPage.js";
|
||||
|
||||
@@ -178,11 +178,11 @@ describe("buildFlightListJsonLd", () => {
|
||||
const items = result.itemListElement;
|
||||
|
||||
expect(Array.isArray(items)).toBe(true);
|
||||
const itemArray = items as Array<{ "@type": string; position: number; item: Flight }>;
|
||||
const itemArray = items as unknown as Array<{ "@type": string; position: number; item: Flight }>;
|
||||
expect(itemArray).toHaveLength(2);
|
||||
expect(itemArray[0]!["@type"]).toBe("ListItem");
|
||||
expect(itemArray[0]!.position).toBe(1);
|
||||
expect(itemArray[1]!.position).toBe(2);
|
||||
expect(itemArray[0]).toHaveProperty("@type", "ListItem");
|
||||
expect(itemArray[0]).toHaveProperty("position", 1);
|
||||
expect(itemArray[1]).toHaveProperty("position", 2);
|
||||
});
|
||||
|
||||
it("embeds Flight objects inside ListItem.item", () => {
|
||||
@@ -190,8 +190,8 @@ describe("buildFlightListJsonLd", () => {
|
||||
const result = buildFlightListJsonLd(flights, "Flights");
|
||||
const items = result.itemListElement as unknown as Array<{ item: Flight }>;
|
||||
|
||||
expect(items[0]!.item["@type"]).toBe("Flight");
|
||||
expect(items[0]!.item.flightNumber).toBe("SU0100");
|
||||
expect(items[0]).toHaveProperty("item.@type", "Flight");
|
||||
expect(items[0]).toHaveProperty("item.flightNumber", "SU0100");
|
||||
});
|
||||
|
||||
it("handles empty flight list", () => {
|
||||
|
||||
@@ -58,6 +58,7 @@ describe("buildOnlineBoardStartSeo", () => {
|
||||
const result = buildOnlineBoardStartSeo(stubT, "ru", CANONICAL);
|
||||
|
||||
expect(result.twitter).toBeDefined();
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- test assertion guards above
|
||||
expect(result.twitter!.card).toBe("summary");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,14 +14,18 @@ import type { SeoHeadProps } from "@/ui/seo/SeoHead.js";
|
||||
import { buildHreflangSet } from "@/shared/seo/hreflang.js";
|
||||
import { buildOnlineBoardUrl } from "./url.js";
|
||||
import type { OnlineBoardParams } from "./url.js";
|
||||
import type { ISimpleFlight, IFlightLeg } from "./types.js";
|
||||
import type { ISimpleFlight } from "./types.js";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Translation function signature (compatible with i18next t()) */
|
||||
export type TFunction = (key: string, opts?: Record<string, unknown>) => string;
|
||||
/**
|
||||
* Translation function signature — intentionally loose to accept
|
||||
* both i18next's TFunction and simple test stubs.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type TFunction = (key: string, opts?: any) => string;
|
||||
|
||||
/** Optional city names for station/route searches */
|
||||
export interface CityNames {
|
||||
@@ -103,16 +107,6 @@ function buildCommonSeoProps(args: {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the first leg from a flight (handles both Direct and MultiLeg).
|
||||
*/
|
||||
function getFirstLeg(flight: ISimpleFlight): IFlightLeg | undefined {
|
||||
if (flight.routeType === "Direct") {
|
||||
return flight.leg;
|
||||
}
|
||||
return flight.legs[0];
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public SEO builder functions
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
/**
|
||||
* Online Board flight details route.
|
||||
*
|
||||
* Parses flight ID from URL, renders detailed flight info.
|
||||
* Parses flight ID from URL, renders detailed flight info with SEO.
|
||||
* SEO head is rendered at route level with static info from URL params.
|
||||
* JSON-LD is rendered inside the details component once flight data loads.
|
||||
* URL: /{lang}/onlineboard/{carrier}{flightNumber}-{yyyyMMdd}
|
||||
*/
|
||||
|
||||
@@ -9,6 +11,7 @@ import { lazy, Suspense } from "react";
|
||||
import { useParams } from "@modern-js/runtime/router";
|
||||
import { parseFlightUrlParams } from "@/features/online-board/url.js";
|
||||
import { FlightListSkeleton } from "@/ui/flights/FlightListSkeleton.js";
|
||||
import { getEnv } from "@/env/index.js";
|
||||
|
||||
const OnlineBoardDetailsPage = lazy(() =>
|
||||
import("@/features/online-board/components/OnlineBoardDetailsPage.js").then(
|
||||
@@ -17,8 +20,9 @@ const OnlineBoardDetailsPage = lazy(() =>
|
||||
);
|
||||
|
||||
export default function FlightDetailsPage(): JSX.Element {
|
||||
const routeParams = useParams<{ params: string }>();
|
||||
const routeParams = useParams<{ params: string; lang: string }>();
|
||||
const raw = routeParams.params ?? "";
|
||||
const locale = routeParams.lang ?? "ru";
|
||||
const parsed = parseFlightUrlParams(raw);
|
||||
|
||||
if (!parsed) {
|
||||
@@ -29,9 +33,15 @@ export default function FlightDetailsPage(): JSX.Element {
|
||||
);
|
||||
}
|
||||
|
||||
const canonicalOrigin = getEnv().PROD_ORIGIN;
|
||||
|
||||
return (
|
||||
<Suspense fallback={<FlightListSkeleton count={1} />}>
|
||||
<OnlineBoardDetailsPage flightId={parsed} />
|
||||
<OnlineBoardDetailsPage
|
||||
flightId={parsed}
|
||||
locale={locale}
|
||||
canonicalOrigin={canonicalOrigin}
|
||||
/>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
/**
|
||||
* Online Board arrival search route.
|
||||
*
|
||||
* Parses station params from URL, renders shared search page.
|
||||
* Parses station params from URL, renders shared search page with SEO.
|
||||
* URL: /{lang}/onlineboard/arrival/{station}-{yyyyMMdd}[-{timeRange}]
|
||||
*/
|
||||
|
||||
import { lazy, Suspense } from "react";
|
||||
import { useParams } from "@modern-js/runtime/router";
|
||||
import { useTranslation } from "@/i18n/provider.js";
|
||||
import { parseStationUrlParams } from "@/features/online-board/url.js";
|
||||
import { buildArrivalSearchSeo } from "@/features/online-board/seo.js";
|
||||
import { SeoHead } from "@/ui/seo/SeoHead.js";
|
||||
import { FlightListSkeleton } from "@/ui/flights/FlightListSkeleton.js";
|
||||
import { getEnv } from "@/env/index.js";
|
||||
|
||||
const OnlineBoardSearchPage = lazy(() =>
|
||||
import("@/features/online-board/components/OnlineBoardSearchPage.js").then(
|
||||
@@ -17,8 +21,10 @@ const OnlineBoardSearchPage = lazy(() =>
|
||||
);
|
||||
|
||||
export default function ArrivalSearchPage(): JSX.Element {
|
||||
const routeParams = useParams<{ params: string }>();
|
||||
const { t } = useTranslation();
|
||||
const routeParams = useParams<{ params: string; lang: string }>();
|
||||
const raw = routeParams.params ?? "";
|
||||
const locale = routeParams.lang ?? "ru";
|
||||
const parsed = parseStationUrlParams(raw);
|
||||
|
||||
if (!parsed) {
|
||||
@@ -29,6 +35,7 @@ export default function ArrivalSearchPage(): JSX.Element {
|
||||
);
|
||||
}
|
||||
|
||||
const canonicalOrigin = getEnv().PROD_ORIGIN;
|
||||
const searchParams = {
|
||||
type: "arrival" as const,
|
||||
station: parsed.station,
|
||||
@@ -38,9 +45,14 @@ export default function ArrivalSearchPage(): JSX.Element {
|
||||
: {}),
|
||||
};
|
||||
|
||||
const seoProps = buildArrivalSearchSeo(t, searchParams, locale, canonicalOrigin);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SeoHead {...seoProps} />
|
||||
<Suspense fallback={<FlightListSkeleton />}>
|
||||
<OnlineBoardSearchPage params={searchParams} />
|
||||
</Suspense>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
/**
|
||||
* Online Board departure search route.
|
||||
*
|
||||
* Parses station params from URL, renders shared search page.
|
||||
* Parses station params from URL, renders shared search page with SEO.
|
||||
* URL: /{lang}/onlineboard/departure/{station}-{yyyyMMdd}[-{timeRange}]
|
||||
*/
|
||||
|
||||
import { lazy, Suspense } from "react";
|
||||
import { useParams } from "@modern-js/runtime/router";
|
||||
import { useTranslation } from "@/i18n/provider.js";
|
||||
import { parseStationUrlParams } from "@/features/online-board/url.js";
|
||||
import { buildDepartureSearchSeo } from "@/features/online-board/seo.js";
|
||||
import { SeoHead } from "@/ui/seo/SeoHead.js";
|
||||
import { FlightListSkeleton } from "@/ui/flights/FlightListSkeleton.js";
|
||||
import { getEnv } from "@/env/index.js";
|
||||
|
||||
const OnlineBoardSearchPage = lazy(() =>
|
||||
import("@/features/online-board/components/OnlineBoardSearchPage.js").then(
|
||||
@@ -17,8 +21,10 @@ const OnlineBoardSearchPage = lazy(() =>
|
||||
);
|
||||
|
||||
export default function DepartureSearchPage(): JSX.Element {
|
||||
const routeParams = useParams<{ params: string }>();
|
||||
const { t } = useTranslation();
|
||||
const routeParams = useParams<{ params: string; lang: string }>();
|
||||
const raw = routeParams.params ?? "";
|
||||
const locale = routeParams.lang ?? "ru";
|
||||
const parsed = parseStationUrlParams(raw);
|
||||
|
||||
if (!parsed) {
|
||||
@@ -29,6 +35,7 @@ export default function DepartureSearchPage(): JSX.Element {
|
||||
);
|
||||
}
|
||||
|
||||
const canonicalOrigin = getEnv().PROD_ORIGIN;
|
||||
const searchParams = {
|
||||
type: "departure" as const,
|
||||
station: parsed.station,
|
||||
@@ -38,9 +45,14 @@ export default function DepartureSearchPage(): JSX.Element {
|
||||
: {}),
|
||||
};
|
||||
|
||||
const seoProps = buildDepartureSearchSeo(t, searchParams, locale, canonicalOrigin);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SeoHead {...seoProps} />
|
||||
<Suspense fallback={<FlightListSkeleton />}>
|
||||
<OnlineBoardSearchPage params={searchParams} />
|
||||
</Suspense>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
/**
|
||||
* Online Board flight number search route.
|
||||
*
|
||||
* Parses flight params from URL, renders shared search page.
|
||||
* Parses flight params from URL, renders shared search page with SEO.
|
||||
* URL: /{lang}/onlineboard/flight/{carrier}{flightNumber}-{yyyyMMdd}
|
||||
*/
|
||||
|
||||
import { lazy, Suspense } from "react";
|
||||
import { useParams } from "@modern-js/runtime/router";
|
||||
import { useTranslation } from "@/i18n/provider.js";
|
||||
import { parseFlightUrlParams } from "@/features/online-board/url.js";
|
||||
import { buildFlightSearchSeo } from "@/features/online-board/seo.js";
|
||||
import { SeoHead } from "@/ui/seo/SeoHead.js";
|
||||
import { FlightListSkeleton } from "@/ui/flights/FlightListSkeleton.js";
|
||||
import { getEnv } from "@/env/index.js";
|
||||
|
||||
const OnlineBoardSearchPage = lazy(() =>
|
||||
import("@/features/online-board/components/OnlineBoardSearchPage.js").then(
|
||||
@@ -17,8 +21,10 @@ const OnlineBoardSearchPage = lazy(() =>
|
||||
);
|
||||
|
||||
export default function FlightSearchPage(): JSX.Element {
|
||||
const routeParams = useParams<{ params: string }>();
|
||||
const { t } = useTranslation();
|
||||
const routeParams = useParams<{ params: string; lang: string }>();
|
||||
const raw = routeParams.params ?? "";
|
||||
const locale = routeParams.lang ?? "ru";
|
||||
const parsed = parseFlightUrlParams(raw);
|
||||
|
||||
if (!parsed) {
|
||||
@@ -29,6 +35,7 @@ export default function FlightSearchPage(): JSX.Element {
|
||||
);
|
||||
}
|
||||
|
||||
const canonicalOrigin = getEnv().PROD_ORIGIN;
|
||||
const searchParams = parsed.suffix
|
||||
? {
|
||||
type: "flight" as const,
|
||||
@@ -44,9 +51,14 @@ export default function FlightSearchPage(): JSX.Element {
|
||||
date: parsed.date,
|
||||
};
|
||||
|
||||
const seoProps = buildFlightSearchSeo(t, searchParams, locale, canonicalOrigin);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SeoHead {...seoProps} />
|
||||
<Suspense fallback={<FlightListSkeleton />}>
|
||||
<OnlineBoardSearchPage params={searchParams} />
|
||||
</Suspense>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,11 @@
|
||||
*/
|
||||
|
||||
import { lazy, Suspense } from "react";
|
||||
import { useParams } from "@modern-js/runtime/router";
|
||||
import { useTranslation } from "@/i18n/provider.js";
|
||||
import { SeoHead } from "@/ui/seo/SeoHead.js";
|
||||
import { buildOnlineBoardStartSeo } from "@/features/online-board/seo.js";
|
||||
import { getEnv } from "@/env/index.js";
|
||||
|
||||
const OnlineBoardStartPage = lazy(() =>
|
||||
import("@/features/online-board/components/OnlineBoardStartPage.js").then(
|
||||
@@ -14,9 +19,19 @@ const OnlineBoardStartPage = lazy(() =>
|
||||
);
|
||||
|
||||
export default function OnlineBoardPage(): JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
const routeParams = useParams<{ lang: string }>();
|
||||
const locale = routeParams.lang ?? "ru";
|
||||
const canonicalOrigin = getEnv().PROD_ORIGIN;
|
||||
|
||||
const seoProps = buildOnlineBoardStartSeo(t, locale, canonicalOrigin);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SeoHead {...seoProps} />
|
||||
<Suspense fallback={<div aria-busy="true">Loading...</div>}>
|
||||
<OnlineBoardStartPage />
|
||||
</Suspense>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
/**
|
||||
* Online Board route search page.
|
||||
*
|
||||
* Parses departure + arrival + date from URL, renders shared search page.
|
||||
* Parses departure + arrival + date from URL, renders shared search page with SEO.
|
||||
* URL: /{lang}/onlineboard/route/{dep}-{arr}-{yyyyMMdd}[-{timeRange}]
|
||||
*/
|
||||
|
||||
import { lazy, Suspense } from "react";
|
||||
import { useParams } from "@modern-js/runtime/router";
|
||||
import { useTranslation } from "@/i18n/provider.js";
|
||||
import { parseRouteUrlParams } from "@/features/online-board/url.js";
|
||||
import { buildRouteSearchSeo } from "@/features/online-board/seo.js";
|
||||
import { SeoHead } from "@/ui/seo/SeoHead.js";
|
||||
import { FlightListSkeleton } from "@/ui/flights/FlightListSkeleton.js";
|
||||
import { getEnv } from "@/env/index.js";
|
||||
|
||||
const OnlineBoardSearchPage = lazy(() =>
|
||||
import("@/features/online-board/components/OnlineBoardSearchPage.js").then(
|
||||
@@ -17,8 +21,10 @@ const OnlineBoardSearchPage = lazy(() =>
|
||||
);
|
||||
|
||||
export default function RouteSearchPage(): JSX.Element {
|
||||
const routeParams = useParams<{ params: string }>();
|
||||
const { t } = useTranslation();
|
||||
const routeParams = useParams<{ params: string; lang: string }>();
|
||||
const raw = routeParams.params ?? "";
|
||||
const locale = routeParams.lang ?? "ru";
|
||||
const parsed = parseRouteUrlParams(raw);
|
||||
|
||||
if (!parsed) {
|
||||
@@ -29,6 +35,7 @@ export default function RouteSearchPage(): JSX.Element {
|
||||
);
|
||||
}
|
||||
|
||||
const canonicalOrigin = getEnv().PROD_ORIGIN;
|
||||
const searchParams = {
|
||||
type: "route" as const,
|
||||
departure: parsed.departure,
|
||||
@@ -39,9 +46,14 @@ export default function RouteSearchPage(): JSX.Element {
|
||||
: {}),
|
||||
};
|
||||
|
||||
const seoProps = buildRouteSearchSeo(t, searchParams, locale, canonicalOrigin);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SeoHead {...seoProps} />
|
||||
<Suspense fallback={<FlightListSkeleton />}>
|
||||
<OnlineBoardSearchPage params={searchParams} />
|
||||
</Suspense>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user