Branch schedule details title by direct vs connecting per TZ Table 6 rows 11-13

This commit is contained in:
2026-04-21 17:24:16 +03:00
parent f03562e4cd
commit df4ac19f7d
11 changed files with 215 additions and 27 deletions
+137 -14
View File
@@ -195,12 +195,146 @@ describe("buildScheduleDetailsSeo", () => {
},
};
it("uses FLIGHT-DETAILS translation keys", () => {
it("uses TITLE-DIRECT key for single flight number (direct)", () => {
const result = buildScheduleDetailsSeo(stubT, [flight], "ru", CANONICAL, flightIds);
expect(result.title).toContain("SEO.SCHEDULE.FLIGHT-DETAILS.TITLE");
expect(result.title).toContain("SEO.SCHEDULE.FLIGHT-DETAILS.TITLE-DIRECT");
expect(result.title).toContain("flightNumber=SU 0012");
expect(result.title).toContain("date=27.05.2022");
expect(result.title).toContain("routeCities=Moscow-Saint Petersburg");
});
it("uses TITLE-CONNECTING key for multiple distinct flight numbers (connecting)", () => {
const connectingFlightIds: IScheduleFlightId[] = [
{ carrier: "SU", flightNumber: "1234", date: "20220527" },
{ carrier: "SU", flightNumber: "5678", date: "20220527" },
];
const result = buildScheduleDetailsSeo(stubT, [flight], "ru", CANONICAL, connectingFlightIds);
expect(result.title).toContain("SEO.SCHEDULE.FLIGHT-DETAILS.TITLE-CONNECTING");
expect(result.title).toContain("flightNumbers=SU 1234, SU 5678");
});
it("uses TITLE-DIRECT for multi-segment flight (same flight number across legs)", () => {
const multiSegFlight: ISimpleFlight = {
id: "SU0012-20220527",
routeType: "MultiLeg",
flightId: { carrier: "SU", flightNumber: "0012", suffix: "", date: "20220527" },
flyingTime: "3h 00m",
operatingBy: {},
status: "Scheduled",
legs: [
{
index: 0,
status: "Scheduled",
flyingTime: "1h 30m",
updated: "2022-05-27T10:00:00Z",
dayChange: 0,
equipment: { name: "Airbus A320", code: "320" },
flags: { checkinAvailable: false, returnToAirport: false, routeChanged: false },
operatingBy: {},
departure: {
scheduled: {
airport: "Sheremetyevo",
airportCode: "SVO",
city: "Moscow",
cityCode: "MOW",
countryCode: "RU",
},
checkingStatus: "closed",
times: {
scheduledDeparture: {
dayChange: { value: 0, title: "" },
local: "2022-05-27T10:00:00",
localTime: "10:00",
tzOffset: 3,
utc: "2022-05-27T07:00:00Z",
},
},
},
arrival: {
scheduled: {
airport: "Tolmachevo",
airportCode: "OVB",
city: "Novosibirsk",
cityCode: "OVB",
countryCode: "RU",
},
times: {
scheduledArrival: {
dayChange: { value: 0, title: "" },
local: "2022-05-27T14:00:00",
localTime: "14:00",
tzOffset: 7,
utc: "2022-05-27T07:00:00Z",
},
},
},
},
{
index: 1,
status: "Scheduled",
flyingTime: "1h 30m",
updated: "2022-05-27T14:30:00Z",
dayChange: 0,
equipment: { name: "Airbus A320", code: "320" },
flags: { checkinAvailable: false, returnToAirport: false, routeChanged: false },
operatingBy: {},
departure: {
scheduled: {
airport: "Tolmachevo",
airportCode: "OVB",
city: "Novosibirsk",
cityCode: "OVB",
countryCode: "RU",
},
checkingStatus: "closed",
times: {
scheduledDeparture: {
dayChange: { value: 0, title: "" },
local: "2022-05-27T14:30:00",
localTime: "14:30",
tzOffset: 7,
utc: "2022-05-27T07:30:00Z",
},
},
},
arrival: {
scheduled: {
airport: "Vladivostok",
airportCode: "VVO",
city: "Vladivostok",
cityCode: "VVO",
countryCode: "RU",
},
times: {
scheduledArrival: {
dayChange: { value: 0, title: "" },
local: "2022-05-27T17:00:00",
localTime: "17:00",
tzOffset: 10,
utc: "2022-05-27T07:00:00Z",
},
},
},
},
],
};
// Multi-segment = same flight number on all legs → treated as direct (singular)
const result = buildScheduleDetailsSeo(stubT, [multiSegFlight], "ru", CANONICAL, flightIds);
expect(result.title).toContain("SEO.SCHEDULE.FLIGHT-DETAILS.TITLE-DIRECT");
expect(result.title).toContain("flightNumber=SU 0012");
expect(result.title).toContain("routeCities=Moscow-Vladivostok");
});
it("uses TITLE-NO-ROUTE-DIRECT when flight data is empty (SSR without loaded data)", () => {
const result = buildScheduleDetailsSeo(stubT, [], "ru", CANONICAL, flightIds);
expect(result.title).toContain("SEO.SCHEDULE.FLIGHT-DETAILS.TITLE-NO-ROUTE-DIRECT");
expect(result.title).toContain("flightNumber=SU 0012");
expect(result.title).not.toContain("routeCities");
});
it("sets canonical to the details URL", () => {
@@ -222,15 +356,4 @@ describe("buildScheduleDetailsSeo", () => {
expect(result.og.type).toBe("article");
});
it("handles multiple flights in display", () => {
const multiFlightIds: IScheduleFlightId[] = [
{ carrier: "SU", flightNumber: "0012", date: "20220527" },
{ carrier: "SU", flightNumber: "0013", date: "20220527" },
];
const result = buildScheduleDetailsSeo(stubT, [flight], "ru", CANONICAL, multiFlightIds);
expect(result.title).toContain("SU 0012, SU 0013");
});
});
+51 -4
View File
@@ -171,6 +171,33 @@ export function buildScheduleSearchSeo(
};
}
// ---------------------------------------------------------------------------
// Internal helpers (details)
// ---------------------------------------------------------------------------
/**
* Derive "{depCity}-{arrCity}" from the loaded flight data.
* Returns empty string when flights aren't available (SSR without data).
*/
function deriveRouteCities(flights: ISimpleFlight[]): string {
const first = flights[0];
const last = flights[flights.length - 1] ?? first;
if (!first || !last) return "";
const depCity =
first.routeType === "Direct"
? first.leg.departure.scheduled.city
: first.legs[0]?.departure.scheduled.city ?? "";
const arrCity =
last.routeType === "Direct"
? last.leg.arrival.scheduled.city
: last.legs[last.legs.length - 1]?.arrival.scheduled.city ?? "";
if (!depCity || !arrCity) return "";
return `${depCity}-${arrCity}`;
}
/**
* SEO props for schedule details page.
*/
@@ -181,6 +208,9 @@ export function buildScheduleDetailsSeo(
canonicalOrigin: string,
flightIds: IScheduleFlightId[],
): SeoHeadProps {
const uniqueNumbers = new Set(flightIds.map((f) => f.flightNumber));
const isConnecting = uniqueNumbers.size > 1;
const flightDisplay = flightIds
.map((f) => `${f.carrier} ${f.flightNumber}${f.suffix ?? ""}`)
.join(", ");
@@ -188,10 +218,27 @@ export function buildScheduleDetailsSeo(
? formatDateForSeo(flightIds[0].date)
: "";
const title = t("SEO.SCHEDULE.FLIGHT-DETAILS.TITLE", {
flightNumber: flightDisplay,
date: dateDisplay,
});
let title: string;
if (isConnecting) {
title = t("SEO.SCHEDULE.FLIGHT-DETAILS.TITLE-CONNECTING", {
flightNumbers: flightDisplay,
});
} else {
const first = flightIds[0]!;
const flightNumber = `${first.carrier} ${first.flightNumber}${first.suffix ?? ""}`;
const routeCities = deriveRouteCities(flights);
if (routeCities) {
title = t("SEO.SCHEDULE.FLIGHT-DETAILS.TITLE-DIRECT", {
flightNumber,
routeCities,
});
} else {
title = t("SEO.SCHEDULE.FLIGHT-DETAILS.TITLE-NO-ROUTE-DIRECT", {
flightNumber,
});
}
}
const description = t("SEO.SCHEDULE.FLIGHT-DETAILS.DESCRIPTION", {
flightNumber: flightDisplay,
date: dateDisplay,
+3 -1
View File
@@ -220,7 +220,9 @@
"SCHEDULE": {
"FLIGHT-DETAILS": {
"DESCRIPTION": "",
"TITLE": ""
"TITLE-DIRECT": "",
"TITLE-NO-ROUTE-DIRECT": "",
"TITLE-CONNECTING": ""
},
"MAIN": {
"DESCRIPTION": "",
+3 -1
View File
@@ -253,7 +253,9 @@
"SCHEDULE": {
"FLIGHT-DETAILS": {
"DESCRIPTION": "Live departure and arrival information for flight {flightNumber}. Departure time, arrival time and current status on the official Aeroflot website.",
"TITLE": "Flight {flightNumber} Flight schedule for {date} | Aeroflot"
"TITLE-DIRECT": "Schedule: flight {flightNumber}, {routeCities}",
"TITLE-NO-ROUTE-DIRECT": "Schedule: flight {flightNumber}",
"TITLE-CONNECTING": "Schedule: flights {flightNumbers}"
},
"MAIN": {
"DESCRIPTION": "Aeroflot flight schedule for Russian and international destinations. List of available flights and current departure / arrival times.",
+3 -1
View File
@@ -220,7 +220,9 @@
"SCHEDULE": {
"FLIGHT-DETAILS": {
"DESCRIPTION": "",
"TITLE": ""
"TITLE-DIRECT": "",
"TITLE-NO-ROUTE-DIRECT": "",
"TITLE-CONNECTING": ""
},
"MAIN": {
"DESCRIPTION": "",
+3 -1
View File
@@ -220,7 +220,9 @@
"SCHEDULE": {
"FLIGHT-DETAILS": {
"DESCRIPTION": "",
"TITLE": ""
"TITLE-DIRECT": "",
"TITLE-NO-ROUTE-DIRECT": "",
"TITLE-CONNECTING": ""
},
"MAIN": {
"DESCRIPTION": "",
+3 -1
View File
@@ -220,7 +220,9 @@
"SCHEDULE": {
"FLIGHT-DETAILS": {
"DESCRIPTION": "",
"TITLE": ""
"TITLE-DIRECT": "",
"TITLE-NO-ROUTE-DIRECT": "",
"TITLE-CONNECTING": ""
},
"MAIN": {
"DESCRIPTION": "",
+3 -1
View File
@@ -220,7 +220,9 @@
"SCHEDULE": {
"FLIGHT-DETAILS": {
"DESCRIPTION": "",
"TITLE": ""
"TITLE-DIRECT": "",
"TITLE-NO-ROUTE-DIRECT": "",
"TITLE-CONNECTING": ""
},
"MAIN": {
"DESCRIPTION": "",
+3 -1
View File
@@ -220,7 +220,9 @@
"SCHEDULE": {
"FLIGHT-DETAILS": {
"DESCRIPTION": "",
"TITLE": ""
"TITLE-DIRECT": "",
"TITLE-NO-ROUTE-DIRECT": "",
"TITLE-CONNECTING": ""
},
"MAIN": {
"DESCRIPTION": "",
+3 -1
View File
@@ -253,7 +253,9 @@
"SCHEDULE": {
"FLIGHT-DETAILS": {
"DESCRIPTION": "Информация об отправлении и прибытии рейса {flightNumber} в режиме онлайн! Время вылета, время прилета, актуальный статус рейса на официальном сайте авиакомпании Аэрофлот.",
"TITLE": "Рейс {flightNumber} – Расписание рейсов на {date} | Аэрофлот"
"TITLE-DIRECT": "Расписание рейса: {flightNumber}, {routeCities}",
"TITLE-NO-ROUTE-DIRECT": "Расписание рейса: {flightNumber}",
"TITLE-CONNECTING": "Расписание рейсов: {flightNumbers}"
},
"MAIN": {
"DESCRIPTION": "Расписание полетов 'Аэрофлот' по России и международным направлениям. Список доступных рейсов и актуальная информация о времени вылета и прилета.",
+3 -1
View File
@@ -220,7 +220,9 @@
"SCHEDULE": {
"FLIGHT-DETAILS": {
"DESCRIPTION": "",
"TITLE": ""
"TITLE-DIRECT": "",
"TITLE-NO-ROUTE-DIRECT": "",
"TITLE-CONNECTING": ""
},
"MAIN": {
"DESCRIPTION": "",