From 0f5d7915be3d421e095f367fedbf9f5a98a98b9a Mon Sep 17 00:00:00 2001 From: gnezim Date: Wed, 15 Apr 2026 09:43:24 +0300 Subject: [PATCH] Add flights-map SEO builder and JSON-LD WebPage schema Phase 4D: buildFlightsMapSeo generates meta tags, canonical, hreflang, OG and Twitter Card props. buildFlightsMapJsonLd produces a schema.org WebPage object for structured data. 10 tests cover both builders. --- src/features/flights-map/json-ld.ts | 42 +++++++++++++ src/features/flights-map/seo.test.ts | 93 ++++++++++++++++++++++++++++ src/features/flights-map/seo.ts | 65 +++++++++++++++++++ 3 files changed, 200 insertions(+) create mode 100644 src/features/flights-map/json-ld.ts create mode 100644 src/features/flights-map/seo.test.ts create mode 100644 src/features/flights-map/seo.ts diff --git a/src/features/flights-map/json-ld.ts b/src/features/flights-map/json-ld.ts new file mode 100644 index 00000000..a313a091 --- /dev/null +++ b/src/features/flights-map/json-ld.ts @@ -0,0 +1,42 @@ +/** + * JSON-LD schema builder for the Flights Map page. + * + * Produces a schema.org WebPage typed object ready for . + * + * @module + */ + +import type { WebPage } from "schema-dts"; + +// --------------------------------------------------------------------------- +// Constants +// --------------------------------------------------------------------------- + +const SITE_NAME = "Aeroflot"; +const PATH_WITHOUT_LOCALE = "/flights-map"; + +// --------------------------------------------------------------------------- +// Public +// --------------------------------------------------------------------------- + +/** + * Build a schema.org WebPage JSON-LD object for the flights map page. + */ +export function buildFlightsMapJsonLd( + locale: string, + canonicalOrigin: string, +): WebPage { + const url = `${canonicalOrigin}/${locale}${PATH_WITHOUT_LOCALE}`; + + return { + "@type": "WebPage", + name: `${SITE_NAME} - Flight Map`, + url, + inLanguage: locale, + isPartOf: { + "@type": "WebSite", + name: SITE_NAME, + url: canonicalOrigin, + }, + }; +} diff --git a/src/features/flights-map/seo.test.ts b/src/features/flights-map/seo.test.ts new file mode 100644 index 00000000..df4a5d68 --- /dev/null +++ b/src/features/flights-map/seo.test.ts @@ -0,0 +1,93 @@ +import { describe, expect, it } from "vitest"; +import { buildFlightsMapSeo } from "./seo.js"; +import { buildFlightsMapJsonLd } from "./json-ld.js"; + +/** Stub t() that returns the key for assertion. */ +function stubT(key: string): string { + return key; +} + +const CANONICAL = "https://www.aeroflot.ru"; + +// --------------------------------------------------------------------------- +// buildFlightsMapSeo +// --------------------------------------------------------------------------- + +describe("buildFlightsMapSeo", () => { + it("uses FLIGHTS_MAP translation keys", () => { + const result = buildFlightsMapSeo(stubT, "ru", CANONICAL); + + expect(result.title).toBe("SEO.FLIGHTS_MAP.TITLE"); + expect(result.description).toBe("SEO.FLIGHTS_MAP.DESCRIPTION"); + }); + + it("sets canonical to /{locale}/flights-map", () => { + const result = buildFlightsMapSeo(stubT, "en", CANONICAL); + + expect(result.canonical).toBe("https://www.aeroflot.ru/en/flights-map"); + }); + + it("includes hreflang with 10 entries", () => { + const result = buildFlightsMapSeo(stubT, "ru", CANONICAL); + + expect(result.hreflang).toHaveLength(10); + }); + + it("sets og tags", () => { + const result = buildFlightsMapSeo(stubT, "ru", CANONICAL); + + expect(result.og.title).toBe(result.title); + expect(result.og.type).toBe("website"); + expect(result.og.locale).toBe("ru"); + expect(result.og.siteName).toBe("Aeroflot"); + expect(result.og.url).toBe("https://www.aeroflot.ru/ru/flights-map"); + }); + + it("sets twitter card to summary", () => { + const result = buildFlightsMapSeo(stubT, "ru", CANONICAL); + + expect(result.twitter).toBeDefined(); + expect(result.twitter!.card).toBe("summary"); + }); +}); + +// --------------------------------------------------------------------------- +// buildFlightsMapJsonLd +// --------------------------------------------------------------------------- + +describe("buildFlightsMapJsonLd", () => { + it("returns a WebPage schema", () => { + const result = buildFlightsMapJsonLd("ru", CANONICAL); + + expect(result["@type"]).toBe("WebPage"); + }); + + it("sets the correct URL with locale", () => { + const result = buildFlightsMapJsonLd("en", CANONICAL); + + expect(result.url).toBe("https://www.aeroflot.ru/en/flights-map"); + }); + + it("sets inLanguage to the locale", () => { + const result = buildFlightsMapJsonLd("ja", CANONICAL); + + expect(result.inLanguage).toBe("ja"); + }); + + it("includes isPartOf WebSite reference", () => { + const result = buildFlightsMapJsonLd("ru", CANONICAL); + + expect(result.isPartOf).toBeDefined(); + const site = result.isPartOf as { "@type": string; name: string; url: string }; + expect(site["@type"]).toBe("WebSite"); + expect(site.name).toBe("Aeroflot"); + expect(site.url).toBe(CANONICAL); + }); + + it("sets name with site name prefix", () => { + const result = buildFlightsMapJsonLd("ru", CANONICAL); + + expect(result.name).toContain("Aeroflot"); + expect(result.name).toContain("Flight Map"); + }); +}); diff --git a/src/features/flights-map/seo.ts b/src/features/flights-map/seo.ts new file mode 100644 index 00000000..4d4909bc --- /dev/null +++ b/src/features/flights-map/seo.ts @@ -0,0 +1,65 @@ +/** + * SEO builder functions for Flights Map route page. + * + * Pure function -- all data arrives via parameters, no hooks or side effects. + * Returns a SeoHeadProps object ready for . + * + * @module + */ + +import type { SeoHeadProps } from "@/ui/seo/SeoHead.js"; +import { buildHreflangSet } from "@/shared/seo/hreflang.js"; + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type TFunction = (key: string, opts?: any) => string; + +// --------------------------------------------------------------------------- +// Constants +// --------------------------------------------------------------------------- + +const OG_IMAGE = "https://www.aeroflot.ru/static/images/aeroflot-og-default.png"; +const SITE_NAME = "Aeroflot"; +const PATH_WITHOUT_LOCALE = "/flights-map"; + +// --------------------------------------------------------------------------- +// Public +// --------------------------------------------------------------------------- + +/** + * SEO props for the Flights Map start page. + */ +export function buildFlightsMapSeo( + t: TFunction, + locale: string, + canonicalOrigin: string, +): SeoHeadProps { + const title = t("SEO.FLIGHTS_MAP.TITLE"); + const description = t("SEO.FLIGHTS_MAP.DESCRIPTION"); + const canonical = `${canonicalOrigin}/${locale}${PATH_WITHOUT_LOCALE}`; + + return { + title, + description, + canonical, + hreflang: buildHreflangSet({ + canonicalOrigin, + pathWithoutLocale: PATH_WITHOUT_LOCALE, + }), + og: { + title, + description, + url: canonical, + image: OG_IMAGE, + type: "website", + locale, + siteName: SITE_NAME, + }, + twitter: { + card: "summary", + }, + }; +}