Branch schedule details title by direct vs connecting per TZ Table 6 rows 11-13
This commit is contained in:
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -220,7 +220,9 @@
|
||||
"SCHEDULE": {
|
||||
"FLIGHT-DETAILS": {
|
||||
"DESCRIPTION": "",
|
||||
"TITLE": ""
|
||||
"TITLE-DIRECT": "",
|
||||
"TITLE-NO-ROUTE-DIRECT": "",
|
||||
"TITLE-CONNECTING": ""
|
||||
},
|
||||
"MAIN": {
|
||||
"DESCRIPTION": "",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -220,7 +220,9 @@
|
||||
"SCHEDULE": {
|
||||
"FLIGHT-DETAILS": {
|
||||
"DESCRIPTION": "",
|
||||
"TITLE": ""
|
||||
"TITLE-DIRECT": "",
|
||||
"TITLE-NO-ROUTE-DIRECT": "",
|
||||
"TITLE-CONNECTING": ""
|
||||
},
|
||||
"MAIN": {
|
||||
"DESCRIPTION": "",
|
||||
|
||||
@@ -220,7 +220,9 @@
|
||||
"SCHEDULE": {
|
||||
"FLIGHT-DETAILS": {
|
||||
"DESCRIPTION": "",
|
||||
"TITLE": ""
|
||||
"TITLE-DIRECT": "",
|
||||
"TITLE-NO-ROUTE-DIRECT": "",
|
||||
"TITLE-CONNECTING": ""
|
||||
},
|
||||
"MAIN": {
|
||||
"DESCRIPTION": "",
|
||||
|
||||
@@ -220,7 +220,9 @@
|
||||
"SCHEDULE": {
|
||||
"FLIGHT-DETAILS": {
|
||||
"DESCRIPTION": "",
|
||||
"TITLE": ""
|
||||
"TITLE-DIRECT": "",
|
||||
"TITLE-NO-ROUTE-DIRECT": "",
|
||||
"TITLE-CONNECTING": ""
|
||||
},
|
||||
"MAIN": {
|
||||
"DESCRIPTION": "",
|
||||
|
||||
@@ -220,7 +220,9 @@
|
||||
"SCHEDULE": {
|
||||
"FLIGHT-DETAILS": {
|
||||
"DESCRIPTION": "",
|
||||
"TITLE": ""
|
||||
"TITLE-DIRECT": "",
|
||||
"TITLE-NO-ROUTE-DIRECT": "",
|
||||
"TITLE-CONNECTING": ""
|
||||
},
|
||||
"MAIN": {
|
||||
"DESCRIPTION": "",
|
||||
|
||||
@@ -220,7 +220,9 @@
|
||||
"SCHEDULE": {
|
||||
"FLIGHT-DETAILS": {
|
||||
"DESCRIPTION": "",
|
||||
"TITLE": ""
|
||||
"TITLE-DIRECT": "",
|
||||
"TITLE-NO-ROUTE-DIRECT": "",
|
||||
"TITLE-CONNECTING": ""
|
||||
},
|
||||
"MAIN": {
|
||||
"DESCRIPTION": "",
|
||||
|
||||
@@ -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": "Расписание полетов 'Аэрофлот' по России и международным направлениям. Список доступных рейсов и актуальная информация о времени вылета и прилета.",
|
||||
|
||||
@@ -220,7 +220,9 @@
|
||||
"SCHEDULE": {
|
||||
"FLIGHT-DETAILS": {
|
||||
"DESCRIPTION": "",
|
||||
"TITLE": ""
|
||||
"TITLE-DIRECT": "",
|
||||
"TITLE-NO-ROUTE-DIRECT": "",
|
||||
"TITLE-CONNECTING": ""
|
||||
},
|
||||
"MAIN": {
|
||||
"DESCRIPTION": "",
|
||||
|
||||
Reference in New Issue
Block a user