Render SeoHead at route level; convert Angular-style {{var}} to ICU {var}
On /ru/onlineboard/SU6272-20260418 the document title was blank and the
meta description carried a literal '{{ flightNumber }}' placeholder.
Two root causes:
1. Translation values carried Angular ngx-translate syntax {{ var }} but
the React app uses i18next-icu (single-brace {var}). Interpolation
never fired, so SEO strings served as-is. Rewrite every {{ var }}
(and {{var}}) occurrence to {var} across ru/en locales.
2. <SeoHead> was rendered inside the lazy-loaded OnlineBoardDetailsPage.
The SSR response streams the Suspense fallback before the lazy
bundle resolves, so <title>/<meta> never land in the <head>. Move
SeoHead to the route page (src/routes/.../page.tsx) where it
renders synchronously from URL-derived data, and drop the inner
duplicate. Add buildFlightDetailsSeoFromId for the URL-only path.
formatDateForSeo now handles both 'yyyyMMdd' (URL) and
'yyyy-MM-dd' (API) so both entry points produce '18.04.2026'.
3. React 18 doesn't auto-hoist <title> inside body to document.head —
add a useEffect in SeoHead that also writes document.title on the
client. SSR still emits the <title> element for crawlers.
This commit is contained in:
@@ -12,14 +12,12 @@ import { useNavigate, useSearchParams } from "@modern-js/runtime/router";
|
||||
import { useTranslation } from "@/i18n/provider.js";
|
||||
import "./OnlineBoardDetailsPage.scss";
|
||||
import { FlightListSkeleton } from "@/ui/flights/FlightListSkeleton.js";
|
||||
import { SeoHead } from "@/ui/seo/SeoHead.js";
|
||||
import { JsonLdRenderer } from "@/shared/seo/json-ld.js";
|
||||
import { PageLayout } from "@/ui/layout/PageLayout.js";
|
||||
import { useAppSettings } from "@/shared/hooks/useAppSettings.js";
|
||||
import { useFlightDetails } from "../hooks/useFlightDetails.js";
|
||||
import { useLiveFlightDetails } from "../hooks/useLiveFlightDetails.js";
|
||||
import { useOnlineBoard } from "../hooks/useOnlineBoard.js";
|
||||
import { buildFlightDetailsSeo } from "../seo.js";
|
||||
import { buildFlightJsonLd } from "../json-ld.js";
|
||||
import { buildOnlineBoardUrl } from "../url.js";
|
||||
import { FlightDetailsAccordion } from "./details-panels/FlightDetailsAccordion.js";
|
||||
@@ -507,12 +505,14 @@ export const OnlineBoardDetailsPage: FC<OnlineBoardDetailsPageProps> = ({
|
||||
const routeSuffix = depCity && arrCity ? `, ${depCity} - ${arrCity}` : "";
|
||||
const pageTitle = `${t("BOARD.FLIGHT-INFO")}: ${flightNumber}${routeSuffix}`;
|
||||
|
||||
const seoProps = buildFlightDetailsSeo(t, displayFlight, locale, canonicalOrigin);
|
||||
// SeoHead is rendered at the route level (src/routes/.../page.tsx) from
|
||||
// URL-derived data so SSR can emit <title>/<meta> without waiting for
|
||||
// the lazy flight-details bundle + data fetch. Keep only JSON-LD here
|
||||
// since it needs the full flight payload.
|
||||
const jsonLd = buildFlightJsonLd(displayFlight);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SeoHead {...seoProps} />
|
||||
<JsonLdRenderer data={jsonLd} />
|
||||
<PageLayout
|
||||
headerLeft={<DetailsBackButton locale={locale} />}
|
||||
|
||||
@@ -45,13 +45,15 @@ const SITE_NAME = "Aeroflot";
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Format a yyyyMMdd date string to dd.MM.yyyy for display in SEO strings.
|
||||
* Format a date string to dd.MM.yyyy for display in SEO strings.
|
||||
* Accepts both 'yyyyMMdd' (URL param shape) and 'yyyy-MM-dd' (API response).
|
||||
*/
|
||||
function formatDateForSeo(yyyymmdd: string): string {
|
||||
if (yyyymmdd.length !== 8) return yyyymmdd;
|
||||
const day = yyyymmdd.slice(6, 8);
|
||||
const month = yyyymmdd.slice(4, 6);
|
||||
const year = yyyymmdd.slice(0, 4);
|
||||
function formatDateForSeo(input: string): string {
|
||||
const digits = input.replace(/-/g, "");
|
||||
if (digits.length !== 8) return input;
|
||||
const day = digits.slice(6, 8);
|
||||
const month = digits.slice(4, 6);
|
||||
const year = digits.slice(0, 4);
|
||||
return `${day}.${month}.${year}`;
|
||||
}
|
||||
|
||||
@@ -303,6 +305,22 @@ export function buildRouteSearchSeo(
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* SEO props for flight details page from already-parsed flight id.
|
||||
* Accepts a plain object so the route page can render SeoHead
|
||||
* synchronously without waiting for the flight data to load — SSR
|
||||
* needs the title + meta in the <head> of the first response.
|
||||
*/
|
||||
export function buildFlightDetailsSeoFromId(
|
||||
t: TFunction,
|
||||
flightId: { carrier: string; flightNumber: string; suffix?: string; date: string },
|
||||
locale: string,
|
||||
canonicalOrigin: string,
|
||||
): SeoHeadProps {
|
||||
const flight: ISimpleFlight = { flightId } as unknown as ISimpleFlight;
|
||||
return buildFlightDetailsSeo(t, flight, locale, canonicalOrigin);
|
||||
}
|
||||
|
||||
/**
|
||||
* SEO props for flight details page.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user