+ {jsonLd &&
}
{/* Connection status indicator */}
{connectionStatus === "live" && (
diff --git a/src/features/online-board/index.ts b/src/features/online-board/index.ts
index 8eb97772..a50d8f64 100644
--- a/src/features/online-board/index.ts
+++ b/src/features/online-board/index.ts
@@ -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";
diff --git a/src/features/online-board/json-ld.test.ts b/src/features/online-board/json-ld.test.ts
index 942142d5..96eb419d 100644
--- a/src/features/online-board/json-ld.test.ts
+++ b/src/features/online-board/json-ld.test.ts
@@ -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", () => {
diff --git a/src/features/online-board/seo.test.ts b/src/features/online-board/seo.test.ts
index 32fe3ad7..d9340f0f 100644
--- a/src/features/online-board/seo.test.ts
+++ b/src/features/online-board/seo.test.ts
@@ -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");
});
});
diff --git a/src/features/online-board/seo.ts b/src/features/online-board/seo.ts
index 19ebce64..3ce4b471 100644
--- a/src/features/online-board/seo.ts
+++ b/src/features/online-board/seo.ts
@@ -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;
+/**
+ * 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
// ---------------------------------------------------------------------------
diff --git a/src/routes/[lang]/onlineboard/[params]/page.tsx b/src/routes/[lang]/onlineboard/[params]/page.tsx
index b876736d..7b727350 100644
--- a/src/routes/[lang]/onlineboard/[params]/page.tsx
+++ b/src/routes/[lang]/onlineboard/[params]/page.tsx
@@ -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 (
}>
-
+
);
}
diff --git a/src/routes/[lang]/onlineboard/arrival/[params]/page.tsx b/src/routes/[lang]/onlineboard/arrival/[params]/page.tsx
index b75d8950..360f3414 100644
--- a/src/routes/[lang]/onlineboard/arrival/[params]/page.tsx
+++ b/src/routes/[lang]/onlineboard/arrival/[params]/page.tsx
@@ -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 (
- }>
-
-
+ <>
+
+ }>
+
+
+ >
);
}
diff --git a/src/routes/[lang]/onlineboard/departure/[params]/page.tsx b/src/routes/[lang]/onlineboard/departure/[params]/page.tsx
index 6e68930f..5c8adbd1 100644
--- a/src/routes/[lang]/onlineboard/departure/[params]/page.tsx
+++ b/src/routes/[lang]/onlineboard/departure/[params]/page.tsx
@@ -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 (
- }>
-
-
+ <>
+
+ }>
+
+
+ >
);
}
diff --git a/src/routes/[lang]/onlineboard/flight/[params]/page.tsx b/src/routes/[lang]/onlineboard/flight/[params]/page.tsx
index a0ad5e91..c3fef4e3 100644
--- a/src/routes/[lang]/onlineboard/flight/[params]/page.tsx
+++ b/src/routes/[lang]/onlineboard/flight/[params]/page.tsx
@@ -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 (
- }>
-
-
+ <>
+
+ }>
+
+
+ >
);
}
diff --git a/src/routes/[lang]/onlineboard/page.tsx b/src/routes/[lang]/onlineboard/page.tsx
index 04289e10..da5ee194 100644
--- a/src/routes/[lang]/onlineboard/page.tsx
+++ b/src/routes/[lang]/onlineboard/page.tsx
@@ -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 (
- Loading...
}>
-
-
+ <>
+
+
Loading... }>
+