import { describe, expect, it } from "vitest"; import { buildDefaultSeoSettings } from "@/lib/seo-settings"; import { buildDefaultSiteSettings } from "@/lib/site-settings"; import { applyTitleTemplateFn, buildAppMetadataFromConfig, buildLocaleAlternates, buildLocalizedMetadataFromConfig, buildProjectJsonLd, buildSiteJsonLd, serializeJsonLd, } from "@/lib/metadata"; const noBindings = { siteLogoLight: null, siteLogoDark: null, favicon: null, defaultOgImage: null, }; describe("applyTitleTemplateFn", () => { it("substitutes page title and site name", () => { expect(applyTitleTemplateFn("About", "{pageTitle} | {siteName}", "Studio")).toBe("About | Studio"); }); it("replaces every site-name token but only the first page-title token", () => { expect(applyTitleTemplateFn("P", "{siteName} {pageTitle} {siteName}", "S")).toBe("S P S"); }); it("falls back to a default template when the token is missing", () => { expect(applyTitleTemplateFn("About", "Just Site", "Studio")).toBe("About | Studio"); }); }); describe("buildLocaleAlternates", () => { it("builds canonical, hreflang, and x-default against the default locale", () => { const alt = buildLocaleAlternates("/about", "ar"); expect(alt.canonical).toBe("https://mohfarawati.de/about"); expect(alt.languages.ar).toBe("https://mohfarawati.de/about"); expect(alt.languages.de).toBe("https://mohfarawati.de/de/about"); expect(alt.languages.en).toBe("https://mohfarawati.de/en/about"); expect(alt.languages["x-default"]).toBe("https://mohfarawati.de/about"); }); it("shifts prefixes when the default locale changes", () => { const alt = buildLocaleAlternates("/about", "en"); expect(alt.canonical).toBe("https://mohfarawati.de/about"); expect(alt.languages.en).toBe("https://mohfarawati.de/about"); expect(alt.languages.de).toBe("https://mohfarawati.de/de/about"); }); }); describe("buildAppMetadataFromConfig", () => { it("uses summary twitter card and omits og images when unset", () => { const settings = buildDefaultSiteSettings("Studio"); const metadata = buildAppMetadataFromConfig(settings, noBindings); expect(metadata.title).toBe("Studio"); expect(metadata.twitter).toMatchObject({ card: "summary" }); expect(metadata.openGraph?.images).toBeUndefined(); expect(metadata.metadataBase?.toString()).toBe("https://mohfarawati.de/"); }); it("uses summary_large_image and a versioned favicon when bindings exist", () => { const settings = buildDefaultSiteSettings("Studio"); const metadata = buildAppMetadataFromConfig(settings, { ...noBindings, favicon: { assetId: "f", url: "/uploads/media/site-settings/favicon.svg", version: "v9" }, defaultOgImage: { assetId: "og", url: "/uploads/media/site-settings/og.png", version: "v9" }, }); expect(metadata.twitter).toMatchObject({ card: "summary_large_image" }); expect(metadata.icons).toMatchObject({ icon: [{ url: "/favicon.ico?v=v9" }] }); }); }); describe("buildLocalizedMetadataFromConfig", () => { it("applies the title template and localized description by default", () => { const settings = buildDefaultSiteSettings("Studio"); settings.locales.en.siteDescription = "English description"; const metadata = buildLocalizedMetadataFromConfig({ settings, bindings: noBindings, locale: "en", pathname: "/about", title: "About", }); expect(metadata.title).toBe("About | Studio"); expect(metadata.description).toBe("English description"); expect(metadata.openGraph?.locale).toBe("en_US"); }); it("can skip the title template (homepage)", () => { const settings = buildDefaultSiteSettings("Studio"); const metadata = buildLocalizedMetadataFromConfig({ settings, bindings: noBindings, locale: "ar", pathname: "/", title: "الرئيسية", applyTitleTemplate: false, }); expect(metadata.title).toBe("الرئيسية"); }); it("prefers an explicit description over the locale default", () => { const settings = buildDefaultSiteSettings("Studio"); const metadata = buildLocalizedMetadataFromConfig({ settings, bindings: noBindings, locale: "de", pathname: "/x", title: "T", description: " Custom desc ", }); expect(metadata.description).toBe("Custom desc"); }); }); describe("seo-aware metadata", () => { const settings = buildDefaultSiteSettings("Studio"); it("indexes by default and emits verification + twitter handle when configured", () => { const seo = { ...buildDefaultSeoSettings(), googleSiteVerification: "g123", bingSiteVerification: "b456", twitterHandle: "@moh", }; const metadata = buildAppMetadataFromConfig(settings, noBindings, seo); expect(metadata.robots).toMatchObject({ index: true, follow: true }); expect(metadata.verification).toEqual({ google: "g123", other: { "msvalidate.01": "b456" } }); expect(metadata.twitter).toMatchObject({ site: "@moh", creator: "@moh" }); expect(metadata.openGraph).toMatchObject({ locale: "de_DE", alternateLocale: ["en_US", "ar_AR"] }); }); it("emits noindex everywhere when indexing is disabled", () => { const seo = { ...buildDefaultSeoSettings(), allowIndexing: false }; expect(buildAppMetadataFromConfig(settings, noBindings, seo).robots).toMatchObject({ index: false }); const page = buildLocalizedMetadataFromConfig({ settings, bindings: noBindings, seo, locale: "en", pathname: "/about", title: "About" }); expect(page.robots).toMatchObject({ index: false, follow: false }); }); it("supports per-page noindex, article type and a page-specific image", () => { const published = new Date("2026-01-02T00:00:00Z"); const page = buildLocalizedMetadataFromConfig({ settings, bindings: { ...noBindings, defaultOgImage: { assetId: "a", url: "/og.png", version: "1" } }, locale: "en", pathname: "/portfolio/x", title: "X", image: "/uploads/media/covers/x.png", type: "article", publishedTime: published, }); expect(page.robots).toMatchObject({ index: true }); expect(page.openGraph).toMatchObject({ type: "article", publishedTime: published.toISOString(), images: [{ url: "https://mohfarawati.de/uploads/media/covers/x.png", alt: "X" }], }); const thanks = buildLocalizedMetadataFromConfig({ settings, bindings: noBindings, locale: "en", pathname: "/success", title: "Thanks", noIndex: true }); expect(thanks.robots).toMatchObject({ index: false }); }); it("falls back to the default og image when no page image is given", () => { const page = buildLocalizedMetadataFromConfig({ settings, bindings: { ...noBindings, defaultOgImage: { assetId: "a", url: "/og.png", version: "1" } }, locale: "de", pathname: "/", title: "Home", }); expect(page.openGraph).toMatchObject({ images: [{ url: "https://mohfarawati.de/og.png", alt: "Home" }] }); }); }); describe("json-ld", () => { const settings = buildDefaultSiteSettings("Studio"); it("builds a WebSite + Person graph linked by @id", () => { const seo = { ...buildDefaultSeoSettings(), structuredDataName: "Moh", structuredDataJobTitle: "Designer", sameAs: ["https://x.com/moh"] }; const graph = buildSiteJsonLd({ settings, seo, bindings: noBindings, locale: "de" })["@graph"] as Array>; expect(graph[0]).toMatchObject({ "@type": "WebSite", publisher: { "@id": "https://mohfarawati.de/#person" } }); expect(graph[1]).toMatchObject({ "@type": "Person", name: "Moh", jobTitle: "Designer", sameAs: ["https://x.com/moh"] }); }); it("uses Organization shape when configured", () => { const seo = { ...buildDefaultSeoSettings(), structuredDataType: "Organization" as const, structuredDataJobTitle: "Studio" }; const graph = buildSiteJsonLd({ settings, seo, bindings: noBindings, locale: "en" })["@graph"] as Array>; expect(graph[1]).toMatchObject({ "@type": "Organization", slogan: "Studio" }); }); it("builds a CreativeWork per project with localized url", () => { const work = buildProjectJsonLd({ settings, seo: buildDefaultSeoSettings(), locale: "en", pathname: "/portfolio/x", title: "X", description: "D", image: "/c.png", datePublished: new Date("2026-01-01T00:00:00Z"), clientName: "ACME", }); expect(work).toMatchObject({ "@type": "CreativeWork", url: "https://mohfarawati.de/en/portfolio/x", image: "https://mohfarawati.de/c.png", sourceOrganization: { "@type": "Organization", name: "ACME" }, author: { "@id": "https://mohfarawati.de/#person" }, }); }); it("escapes < so the payload cannot close the script tag", () => { expect(serializeJsonLd({ name: "" })).not.toContain(""); }); });