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);
|
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("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", () => {
|
it("sets canonical to the details URL", () => {
|
||||||
@@ -222,15 +356,4 @@ describe("buildScheduleDetailsSeo", () => {
|
|||||||
|
|
||||||
expect(result.og.type).toBe("article");
|
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.
|
* SEO props for schedule details page.
|
||||||
*/
|
*/
|
||||||
@@ -181,6 +208,9 @@ export function buildScheduleDetailsSeo(
|
|||||||
canonicalOrigin: string,
|
canonicalOrigin: string,
|
||||||
flightIds: IScheduleFlightId[],
|
flightIds: IScheduleFlightId[],
|
||||||
): SeoHeadProps {
|
): SeoHeadProps {
|
||||||
|
const uniqueNumbers = new Set(flightIds.map((f) => f.flightNumber));
|
||||||
|
const isConnecting = uniqueNumbers.size > 1;
|
||||||
|
|
||||||
const flightDisplay = flightIds
|
const flightDisplay = flightIds
|
||||||
.map((f) => `${f.carrier} ${f.flightNumber}${f.suffix ?? ""}`)
|
.map((f) => `${f.carrier} ${f.flightNumber}${f.suffix ?? ""}`)
|
||||||
.join(", ");
|
.join(", ");
|
||||||
@@ -188,10 +218,27 @@ export function buildScheduleDetailsSeo(
|
|||||||
? formatDateForSeo(flightIds[0].date)
|
? formatDateForSeo(flightIds[0].date)
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
const title = t("SEO.SCHEDULE.FLIGHT-DETAILS.TITLE", {
|
let title: string;
|
||||||
flightNumber: flightDisplay,
|
if (isConnecting) {
|
||||||
date: dateDisplay,
|
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", {
|
const description = t("SEO.SCHEDULE.FLIGHT-DETAILS.DESCRIPTION", {
|
||||||
flightNumber: flightDisplay,
|
flightNumber: flightDisplay,
|
||||||
date: dateDisplay,
|
date: dateDisplay,
|
||||||
|
|||||||
@@ -220,7 +220,9 @@
|
|||||||
"SCHEDULE": {
|
"SCHEDULE": {
|
||||||
"FLIGHT-DETAILS": {
|
"FLIGHT-DETAILS": {
|
||||||
"DESCRIPTION": "",
|
"DESCRIPTION": "",
|
||||||
"TITLE": ""
|
"TITLE-DIRECT": "",
|
||||||
|
"TITLE-NO-ROUTE-DIRECT": "",
|
||||||
|
"TITLE-CONNECTING": ""
|
||||||
},
|
},
|
||||||
"MAIN": {
|
"MAIN": {
|
||||||
"DESCRIPTION": "",
|
"DESCRIPTION": "",
|
||||||
|
|||||||
@@ -253,7 +253,9 @@
|
|||||||
"SCHEDULE": {
|
"SCHEDULE": {
|
||||||
"FLIGHT-DETAILS": {
|
"FLIGHT-DETAILS": {
|
||||||
"DESCRIPTION": "Live departure and arrival information for flight {flightNumber}. Departure time, arrival time and current status on the official Aeroflot website.",
|
"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": {
|
"MAIN": {
|
||||||
"DESCRIPTION": "Aeroflot flight schedule for Russian and international destinations. List of available flights and current departure / arrival times.",
|
"DESCRIPTION": "Aeroflot flight schedule for Russian and international destinations. List of available flights and current departure / arrival times.",
|
||||||
|
|||||||
@@ -220,7 +220,9 @@
|
|||||||
"SCHEDULE": {
|
"SCHEDULE": {
|
||||||
"FLIGHT-DETAILS": {
|
"FLIGHT-DETAILS": {
|
||||||
"DESCRIPTION": "",
|
"DESCRIPTION": "",
|
||||||
"TITLE": ""
|
"TITLE-DIRECT": "",
|
||||||
|
"TITLE-NO-ROUTE-DIRECT": "",
|
||||||
|
"TITLE-CONNECTING": ""
|
||||||
},
|
},
|
||||||
"MAIN": {
|
"MAIN": {
|
||||||
"DESCRIPTION": "",
|
"DESCRIPTION": "",
|
||||||
|
|||||||
@@ -220,7 +220,9 @@
|
|||||||
"SCHEDULE": {
|
"SCHEDULE": {
|
||||||
"FLIGHT-DETAILS": {
|
"FLIGHT-DETAILS": {
|
||||||
"DESCRIPTION": "",
|
"DESCRIPTION": "",
|
||||||
"TITLE": ""
|
"TITLE-DIRECT": "",
|
||||||
|
"TITLE-NO-ROUTE-DIRECT": "",
|
||||||
|
"TITLE-CONNECTING": ""
|
||||||
},
|
},
|
||||||
"MAIN": {
|
"MAIN": {
|
||||||
"DESCRIPTION": "",
|
"DESCRIPTION": "",
|
||||||
|
|||||||
@@ -220,7 +220,9 @@
|
|||||||
"SCHEDULE": {
|
"SCHEDULE": {
|
||||||
"FLIGHT-DETAILS": {
|
"FLIGHT-DETAILS": {
|
||||||
"DESCRIPTION": "",
|
"DESCRIPTION": "",
|
||||||
"TITLE": ""
|
"TITLE-DIRECT": "",
|
||||||
|
"TITLE-NO-ROUTE-DIRECT": "",
|
||||||
|
"TITLE-CONNECTING": ""
|
||||||
},
|
},
|
||||||
"MAIN": {
|
"MAIN": {
|
||||||
"DESCRIPTION": "",
|
"DESCRIPTION": "",
|
||||||
|
|||||||
@@ -220,7 +220,9 @@
|
|||||||
"SCHEDULE": {
|
"SCHEDULE": {
|
||||||
"FLIGHT-DETAILS": {
|
"FLIGHT-DETAILS": {
|
||||||
"DESCRIPTION": "",
|
"DESCRIPTION": "",
|
||||||
"TITLE": ""
|
"TITLE-DIRECT": "",
|
||||||
|
"TITLE-NO-ROUTE-DIRECT": "",
|
||||||
|
"TITLE-CONNECTING": ""
|
||||||
},
|
},
|
||||||
"MAIN": {
|
"MAIN": {
|
||||||
"DESCRIPTION": "",
|
"DESCRIPTION": "",
|
||||||
|
|||||||
@@ -220,7 +220,9 @@
|
|||||||
"SCHEDULE": {
|
"SCHEDULE": {
|
||||||
"FLIGHT-DETAILS": {
|
"FLIGHT-DETAILS": {
|
||||||
"DESCRIPTION": "",
|
"DESCRIPTION": "",
|
||||||
"TITLE": ""
|
"TITLE-DIRECT": "",
|
||||||
|
"TITLE-NO-ROUTE-DIRECT": "",
|
||||||
|
"TITLE-CONNECTING": ""
|
||||||
},
|
},
|
||||||
"MAIN": {
|
"MAIN": {
|
||||||
"DESCRIPTION": "",
|
"DESCRIPTION": "",
|
||||||
|
|||||||
@@ -253,7 +253,9 @@
|
|||||||
"SCHEDULE": {
|
"SCHEDULE": {
|
||||||
"FLIGHT-DETAILS": {
|
"FLIGHT-DETAILS": {
|
||||||
"DESCRIPTION": "Информация об отправлении и прибытии рейса {flightNumber} в режиме онлайн! Время вылета, время прилета, актуальный статус рейса на официальном сайте авиакомпании Аэрофлот.",
|
"DESCRIPTION": "Информация об отправлении и прибытии рейса {flightNumber} в режиме онлайн! Время вылета, время прилета, актуальный статус рейса на официальном сайте авиакомпании Аэрофлот.",
|
||||||
"TITLE": "Рейс {flightNumber} – Расписание рейсов на {date} | Аэрофлот"
|
"TITLE-DIRECT": "Расписание рейса: {flightNumber}, {routeCities}",
|
||||||
|
"TITLE-NO-ROUTE-DIRECT": "Расписание рейса: {flightNumber}",
|
||||||
|
"TITLE-CONNECTING": "Расписание рейсов: {flightNumbers}"
|
||||||
},
|
},
|
||||||
"MAIN": {
|
"MAIN": {
|
||||||
"DESCRIPTION": "Расписание полетов 'Аэрофлот' по России и международным направлениям. Список доступных рейсов и актуальная информация о времени вылета и прилета.",
|
"DESCRIPTION": "Расписание полетов 'Аэрофлот' по России и международным направлениям. Список доступных рейсов и актуальная информация о времени вылета и прилета.",
|
||||||
|
|||||||
@@ -220,7 +220,9 @@
|
|||||||
"SCHEDULE": {
|
"SCHEDULE": {
|
||||||
"FLIGHT-DETAILS": {
|
"FLIGHT-DETAILS": {
|
||||||
"DESCRIPTION": "",
|
"DESCRIPTION": "",
|
||||||
"TITLE": ""
|
"TITLE-DIRECT": "",
|
||||||
|
"TITLE-NO-ROUTE-DIRECT": "",
|
||||||
|
"TITLE-CONNECTING": ""
|
||||||
},
|
},
|
||||||
"MAIN": {
|
"MAIN": {
|
||||||
"DESCRIPTION": "",
|
"DESCRIPTION": "",
|
||||||
|
|||||||
Reference in New Issue
Block a user