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.
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* JSON-LD schema builder for the Flights Map page.
|
||||
*
|
||||
* Produces a schema.org WebPage typed object ready for <JsonLdRenderer>.
|
||||
*
|
||||
* @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,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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 <SeoHead>.
|
||||
*
|
||||
* @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",
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user