import { afterEach, describe, expect, it } from "vitest"; import metadataMessages from "../../messages/en/metadata.json"; import { articleShareImages, buildRobots, metadataBaseUrl, NO_INDEX_ROBOTS, originFromHost, PRODUCTION_SITE_ORIGIN, resolveRequestHost, robotsForPublishedRule, routeMetadata, sitemapUrl, templatesShareImages, } from "../../lib/siteMetadata"; const ORIGINAL_SITE_INDEXING = process.env.SITE_INDEXING; const ORIGINAL_APP_ORIGIN = process.env.CLOUDRON_APP_ORIGIN; afterEach(() => { if (ORIGINAL_SITE_INDEXING === undefined) { delete process.env.SITE_INDEXING; } else { process.env.SITE_INDEXING = ORIGINAL_SITE_INDEXING; } if (ORIGINAL_APP_ORIGIN === undefined) { delete process.env.CLOUDRON_APP_ORIGIN; } else { process.env.CLOUDRON_APP_ORIGIN = ORIGINAL_APP_ORIGIN; } }); describe("routeMetadata", () => { const defaultImage = { url: "/assets/marketing/og-image.png", width: 1200, height: 630, alt: metadataMessages.shareImage.defaultAlt, }; it("sets a path canonical, stripping trailing slashes and query strings", () => { expect(routeMetadata("/about").alternates?.canonical).toBe("/about"); expect(routeMetadata("/about/").alternates?.canonical).toBe("/about"); expect(routeMetadata("about").alternates?.canonical).toBe("/about"); expect(routeMetadata("/").alternates?.canonical).toBe("/"); expect(routeMetadata("/blog/post?x=1#top").alternates?.canonical).toBe( "/blog/post", ); }); it("sets og:url from the same path so metadataBase can add the host", () => { expect(routeMetadata("/about").openGraph?.url).toBe("/about"); expect(routeMetadata("/").openGraph?.url).toBe("/"); }); it("ships a 1200×630 default share image on og and twitter", () => { const meta = routeMetadata("/learn"); expect(meta.openGraph?.images).toEqual([defaultImage]); expect(meta.twitter?.images).toEqual([defaultImage.url]); }); it("keeps caller metadata while overriding a parent canonical", () => { const meta = routeMetadata("/learn", { title: "Learn", description: "Copy", }); expect(meta.title).toBe("Learn"); expect(meta.description).toBe("Copy"); expect(meta.alternates?.canonical).toBe("/learn"); expect(meta.openGraph?.url).toBe("/learn"); }); it("lets templates and articles replace the default share image", () => { const templates = templatesShareImages(); const templateMeta = routeMetadata("/templates", { openGraph: { images: templates }, }); expect(templateMeta.openGraph?.images).toEqual(templates); expect(templateMeta.twitter?.images).toEqual([templates[0]?.url]); const article = articleShareImages( "how-chaos-concentrates-control", "How Chaos Concentrates Control", ); const articleMeta = routeMetadata("/blog/how-chaos-concentrates-control", { openGraph: { images: article }, }); expect(articleMeta.openGraph?.images).toEqual([ { url: "/content/blog/how-chaos-concentrates-control-og.png", width: 1200, height: 630, alt: "How Chaos Concentrates Control", }, ]); expect(articleMeta.twitter?.images).toEqual([ "/content/blog/how-chaos-concentrates-control-og.png", ]); }); }); describe("sitemapUrl", () => { it("resolves paths against the production origin", () => { expect(sitemapUrl("/")).toBe(PRODUCTION_SITE_ORIGIN); expect(sitemapUrl("/about")).toBe(`${PRODUCTION_SITE_ORIGIN}/about`); }); }); describe("resolveRequestHost", () => { it("prefers x-forwarded-host over Host", () => { expect( resolveRequestHost("staging.communityrule.info", "0.0.0.0:3000"), ).toBe("staging.communityrule.info"); }); it("uses CLOUDRON_APP_ORIGIN when Host is the standalone bind address", () => { process.env.CLOUDRON_APP_ORIGIN = "https://staging.communityrule.info"; expect(resolveRequestHost(null, "0.0.0.0:3000")).toBe( "staging.communityrule.info", ); }); }); describe("originFromHost", () => { it("uses CLOUDRON_APP_ORIGIN when no host is given", () => { process.env.CLOUDRON_APP_ORIGIN = "https://staging.communityrule.info"; expect(originFromHost(null)).toBe("https://staging.communityrule.info"); expect(metadataBaseUrl().origin).toBe("https://staging.communityrule.info"); }); it("falls back to production when neither host nor app origin is set", () => { delete process.env.CLOUDRON_APP_ORIGIN; expect(originFromHost(null)).toBe(PRODUCTION_SITE_ORIGIN); }); it("builds https origins from public hostnames", () => { expect(originFromHost("staging.communityrule.info")).toBe( "https://staging.communityrule.info", ); expect(originFromHost("communityrule.com")).toBe(PRODUCTION_SITE_ORIGIN); }); it("keeps http for localhost so local previews resolve", () => { expect(originFromHost("localhost:3000")).toBe("http://localhost:3000"); }); }); describe("buildRobots", () => { it("disallows crawlers on staging and unknown hosts", () => { expect(buildRobots("staging.communityrule.info")).toEqual({ rules: { userAgent: "*", disallow: "/" }, }); expect(buildRobots("localhost:3000")).toEqual({ rules: { userAgent: "*", disallow: "/" }, }); }); it("allows production hosts and advertises the sitemap", () => { expect(buildRobots("communityrule.info")).toEqual({ rules: { userAgent: "*", allow: "/" }, sitemap: `${PRODUCTION_SITE_ORIGIN}/sitemap.xml`, }); expect(buildRobots("communityrule.com")).toEqual({ rules: { userAgent: "*", allow: "/" }, sitemap: `${PRODUCTION_SITE_ORIGIN}/sitemap.xml`, }); }); it("honors SITE_INDEXING overrides", () => { process.env.SITE_INDEXING = "false"; expect(buildRobots("communityrule.info").rules).toEqual({ userAgent: "*", disallow: "/", }); process.env.SITE_INDEXING = "true"; expect(buildRobots("staging.communityrule.info").sitemap).toBe( `${PRODUCTION_SITE_ORIGIN}/sitemap.xml`, ); }); }); describe("robotsForPublishedRule", () => { it("leaves listed rules indexable and noindexes unlisted guest-claim pages", () => { expect(robotsForPublishedRule(true)).toBeUndefined(); expect(robotsForPublishedRule(false)).toEqual(NO_INDEX_ROBOTS); }); }); describe("page title segments", () => { it("uses a shared em-dash template and does not bake a site suffix into page titles", () => { expect(metadataMessages.titleTemplate).toBe("%s — CommunityRule"); const suffixes = [ " — CommunityRule", " · CommunityRule", " - CommunityRule", ]; const titles: string[] = []; const walk = (value: unknown) => { if (value && typeof value === "object") { for (const [key, child] of Object.entries( value as Record, )) { if (key === "title" && typeof child === "string") { titles.push(child); } else { walk(child); } } } }; walk(metadataMessages); for (const title of titles) { if (title === metadataMessages.home.title) continue; for (const suffix of suffixes) { expect(title.endsWith(suffix)).toBe(false); } } }); });