Resolve IATA to city names in search-page <title>
Deep-linked search pages rendered `Расписание по маршруту: MOW-MMK` in the document title because the route page called the SEO builder synchronously with the raw URL params, before the dictionary was available. The on-page H1 resolved correctly via `useDictionaries` inside the child component, but the parent never re-rendered so the title stayed frozen on IATA codes. Wire `useCityName` into the 5 deep- link route pages (schedule one-way / round-trip, onlineboard route / departure / arrival) so the SEO title reflects city names once the dictionary loads — per TZ §4.1.14.1.
This commit is contained in:
@@ -51,6 +51,10 @@ vi.mock("@/features/online-board/seo.js", () => ({
|
||||
buildArrivalSearchSeo: () => ({}),
|
||||
}));
|
||||
|
||||
vi.mock("@/shared/hooks/useDictionaries.js", () => ({
|
||||
useCityName: (code: string) => code,
|
||||
}));
|
||||
|
||||
const mockNavigate = vi.fn();
|
||||
vi.mock("@modern-js/runtime/router", () => ({
|
||||
useParams: vi.fn(),
|
||||
|
||||
@@ -14,6 +14,7 @@ import { SeoHead } from "@/ui/seo/SeoHead.js";
|
||||
import { FlightListSkeleton } from "@/ui/flights/FlightListSkeleton.js";
|
||||
import { ErrorPage } from "@/ui/errors/ErrorPage.js";
|
||||
import { getEnv } from "@/env/index.js";
|
||||
import { useCityName } from "@/shared/hooks/useDictionaries.js";
|
||||
import { boardDateRedirect } from "../../_guards.js";
|
||||
|
||||
const OnlineBoardSearchPage = lazy(() =>
|
||||
@@ -29,6 +30,9 @@ export default function ArrivalSearchPage(): JSX.Element {
|
||||
const locale = routeParams.lang ?? "ru-ru";
|
||||
const parsed = parseStationUrlParams(raw);
|
||||
|
||||
// Resolve IATA → city name for `<title>`. Hooks must run unconditionally.
|
||||
const stationName = useCityName(parsed?.station ?? "");
|
||||
|
||||
if (!parsed) return <ErrorPage code="404" />;
|
||||
|
||||
const redirect = boardDateRedirect(locale, parsed.date);
|
||||
@@ -44,7 +48,9 @@ export default function ArrivalSearchPage(): JSX.Element {
|
||||
: {}),
|
||||
};
|
||||
|
||||
const seoProps = buildArrivalSearchSeo(t, searchParams, locale, canonicalOrigin);
|
||||
const seoProps = buildArrivalSearchSeo(t, searchParams, locale, canonicalOrigin, {
|
||||
arrival: stationName,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -51,6 +51,10 @@ vi.mock("@/features/online-board/seo.js", () => ({
|
||||
buildDepartureSearchSeo: () => ({}),
|
||||
}));
|
||||
|
||||
vi.mock("@/shared/hooks/useDictionaries.js", () => ({
|
||||
useCityName: (code: string) => code,
|
||||
}));
|
||||
|
||||
const mockNavigate = vi.fn();
|
||||
vi.mock("@modern-js/runtime/router", () => ({
|
||||
useParams: vi.fn(),
|
||||
|
||||
@@ -14,6 +14,7 @@ import { SeoHead } from "@/ui/seo/SeoHead.js";
|
||||
import { FlightListSkeleton } from "@/ui/flights/FlightListSkeleton.js";
|
||||
import { ErrorPage } from "@/ui/errors/ErrorPage.js";
|
||||
import { getEnv } from "@/env/index.js";
|
||||
import { useCityName } from "@/shared/hooks/useDictionaries.js";
|
||||
import { boardDateRedirect } from "../../_guards.js";
|
||||
|
||||
const OnlineBoardSearchPage = lazy(() =>
|
||||
@@ -29,6 +30,9 @@ export default function DepartureSearchPage(): JSX.Element {
|
||||
const locale = routeParams.lang ?? "ru-ru";
|
||||
const parsed = parseStationUrlParams(raw);
|
||||
|
||||
// Resolve IATA → city name for `<title>`. Hooks must run unconditionally.
|
||||
const stationName = useCityName(parsed?.station ?? "");
|
||||
|
||||
if (!parsed) return <ErrorPage code="404" />;
|
||||
|
||||
const redirect = boardDateRedirect(locale, parsed.date);
|
||||
@@ -44,7 +48,9 @@ export default function DepartureSearchPage(): JSX.Element {
|
||||
: {}),
|
||||
};
|
||||
|
||||
const seoProps = buildDepartureSearchSeo(t, searchParams, locale, canonicalOrigin);
|
||||
const seoProps = buildDepartureSearchSeo(t, searchParams, locale, canonicalOrigin, {
|
||||
departure: stationName,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -51,6 +51,10 @@ vi.mock("@/features/online-board/seo.js", () => ({
|
||||
buildRouteSearchSeo: () => ({}),
|
||||
}));
|
||||
|
||||
vi.mock("@/shared/hooks/useDictionaries.js", () => ({
|
||||
useCityName: (code: string) => code,
|
||||
}));
|
||||
|
||||
const mockNavigate = vi.fn();
|
||||
vi.mock("@modern-js/runtime/router", () => ({
|
||||
useParams: vi.fn(),
|
||||
|
||||
@@ -14,6 +14,7 @@ import { SeoHead } from "@/ui/seo/SeoHead.js";
|
||||
import { FlightListSkeleton } from "@/ui/flights/FlightListSkeleton.js";
|
||||
import { ErrorPage } from "@/ui/errors/ErrorPage.js";
|
||||
import { getEnv } from "@/env/index.js";
|
||||
import { useCityName } from "@/shared/hooks/useDictionaries.js";
|
||||
import { boardDateRedirect } from "../../_guards.js";
|
||||
|
||||
const OnlineBoardSearchPage = lazy(() =>
|
||||
@@ -29,6 +30,10 @@ export default function RouteSearchPage(): JSX.Element {
|
||||
const locale = routeParams.lang ?? "ru-ru";
|
||||
const parsed = parseRouteUrlParams(raw);
|
||||
|
||||
// Resolve IATA → city names for `<title>`. Hooks must run unconditionally.
|
||||
const depName = useCityName(parsed?.departure ?? "");
|
||||
const arrName = useCityName(parsed?.arrival ?? "");
|
||||
|
||||
if (!parsed) return <ErrorPage code="404" />;
|
||||
|
||||
const redirect = boardDateRedirect(locale, parsed.date);
|
||||
@@ -45,7 +50,10 @@ export default function RouteSearchPage(): JSX.Element {
|
||||
: {}),
|
||||
};
|
||||
|
||||
const seoProps = buildRouteSearchSeo(t, searchParams, locale, canonicalOrigin);
|
||||
const seoProps = buildRouteSearchSeo(t, searchParams, locale, canonicalOrigin, {
|
||||
departure: depName,
|
||||
arrival: arrName,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -52,6 +52,10 @@ vi.mock("@/features/schedule/seo.js", () => ({
|
||||
buildScheduleSearchSeo: () => ({}),
|
||||
}));
|
||||
|
||||
vi.mock("@/shared/hooks/useDictionaries.js", () => ({
|
||||
useCityName: (code: string) => code,
|
||||
}));
|
||||
|
||||
const mockNavigate = vi.fn();
|
||||
vi.mock("@modern-js/runtime/router", () => ({
|
||||
useParams: vi.fn(),
|
||||
|
||||
@@ -14,6 +14,7 @@ import { SeoHead } from "@/ui/seo/SeoHead.js";
|
||||
import { FlightListSkeleton } from "@/ui/flights/FlightListSkeleton.js";
|
||||
import { ErrorPage } from "@/ui/errors/ErrorPage.js";
|
||||
import { getEnv } from "@/env/index.js";
|
||||
import { useCityName } from "@/shared/hooks/useDictionaries.js";
|
||||
import { scheduleDateRedirect } from "../../../_guards.js";
|
||||
|
||||
const ScheduleSearchPage = lazy(() =>
|
||||
@@ -32,6 +33,12 @@ export default function ScheduleRoundTripSearchPage(): JSX.Element {
|
||||
const outbound = parseScheduleRouteParams(outboundRaw);
|
||||
const inbound = parseScheduleRouteParams(inboundRaw);
|
||||
|
||||
// Resolve IATA → city names for `<title>`. Hooks must run unconditionally,
|
||||
// so call with the outbound codes (which double as the SEO heading codes)
|
||||
// even when parsing failed.
|
||||
const depName = useCityName(outbound?.departure ?? "");
|
||||
const arrName = useCityName(outbound?.arrival ?? "");
|
||||
|
||||
if (!outbound || !inbound) return <ErrorPage code="404" />;
|
||||
|
||||
const redirect =
|
||||
@@ -41,7 +48,10 @@ export default function ScheduleRoundTripSearchPage(): JSX.Element {
|
||||
|
||||
const canonicalOrigin = getEnv().PROD_ORIGIN;
|
||||
const scheduleParams = { type: "roundtrip" as const, outbound, inbound };
|
||||
const seoProps = buildScheduleSearchSeo(t, scheduleParams, locale, canonicalOrigin);
|
||||
const seoProps = buildScheduleSearchSeo(t, scheduleParams, locale, canonicalOrigin, {
|
||||
departure: depName,
|
||||
arrival: arrName,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -51,6 +51,10 @@ vi.mock("@/features/schedule/seo.js", () => ({
|
||||
buildScheduleSearchSeo: () => ({}),
|
||||
}));
|
||||
|
||||
vi.mock("@/shared/hooks/useDictionaries.js", () => ({
|
||||
useCityName: (code: string) => code,
|
||||
}));
|
||||
|
||||
const mockNavigate = vi.fn();
|
||||
vi.mock("@modern-js/runtime/router", () => ({
|
||||
useParams: vi.fn(),
|
||||
|
||||
@@ -14,6 +14,7 @@ import { SeoHead } from "@/ui/seo/SeoHead.js";
|
||||
import { FlightListSkeleton } from "@/ui/flights/FlightListSkeleton.js";
|
||||
import { ErrorPage } from "@/ui/errors/ErrorPage.js";
|
||||
import { getEnv } from "@/env/index.js";
|
||||
import { useCityName } from "@/shared/hooks/useDictionaries.js";
|
||||
import { scheduleDateRedirect } from "../../_guards.js";
|
||||
|
||||
const ScheduleSearchPage = lazy(() =>
|
||||
@@ -29,6 +30,12 @@ export default function ScheduleRouteSearchPage(): JSX.Element {
|
||||
const locale = routeParams.lang ?? "ru-ru";
|
||||
const parsed = parseScheduleRouteParams(raw);
|
||||
|
||||
// Resolve IATA → city names so `<title>` reads
|
||||
// "Расписание по маршруту: Москва-Мурманск" instead of "MOW-MMK".
|
||||
// Hooks must run unconditionally; pass empty codes if parsing failed.
|
||||
const depName = useCityName(parsed?.departure ?? "");
|
||||
const arrName = useCityName(parsed?.arrival ?? "");
|
||||
|
||||
if (!parsed) return <ErrorPage code="404" />;
|
||||
|
||||
const redirect = scheduleDateRedirect(locale, parsed.dateFrom);
|
||||
@@ -36,7 +43,10 @@ export default function ScheduleRouteSearchPage(): JSX.Element {
|
||||
|
||||
const canonicalOrigin = getEnv().PROD_ORIGIN;
|
||||
const scheduleParams = { type: "route" as const, outbound: parsed };
|
||||
const seoProps = buildScheduleSearchSeo(t, scheduleParams, locale, canonicalOrigin);
|
||||
const seoProps = buildScheduleSearchSeo(t, scheduleParams, locale, canonicalOrigin, {
|
||||
departure: depName,
|
||||
arrival: arrName,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
Reference in New Issue
Block a user