Files
community-rule/tests/unit/siteMetadata.test.ts
T
adilalloandCursor c180e79c38 Give every public route its own description, and keep canonicals and structured data on the host that serves the page.
Decorative images on the marketing pages stay quiet, and every dialog close button is named Close.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-11-04 11:04:52 -07:00

269 lines
8.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { afterEach, describe, expect, it } from "vitest";
import metadataMessages from "../../messages/en/metadata.json";
import {
absoluteSiteUrl,
articleShareImages,
buildRobots,
configuredSiteOrigin,
metadataBaseUrl,
NO_INDEX_ROBOTS,
originFromHost,
PRODUCTION_SITE_ORIGIN,
resolveRequestHost,
robotsForPublishedRule,
routeMetadata,
sitemapUrl,
templatesShareImages,
websiteJsonLd,
} 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.openGraph?.description).toBe("Copy");
expect(meta.openGraph?.title).toBe("Learn");
expect(meta.twitter?.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("absoluteSiteUrl", () => {
it("uses the configured origin, which is production when no app origin is set", () => {
delete process.env.CLOUDRON_APP_ORIGIN;
expect(PRODUCTION_SITE_ORIGIN).toBe("https://communityrule.info");
expect(configuredSiteOrigin()).toBe(PRODUCTION_SITE_ORIGIN);
expect(absoluteSiteUrl("/")).toBe("https://communityrule.info");
expect(absoluteSiteUrl("/about")).toBe("https://communityrule.info/about");
});
it("follows CLOUDRON_APP_ORIGIN so a staging build does not advertise production", () => {
process.env.CLOUDRON_APP_ORIGIN = "https://staging.communityrule.info";
expect(absoluteSiteUrl("/learn")).toBe(
"https://staging.communityrule.info/learn",
);
});
it("builds a WebSite block with no search action", () => {
expect(websiteJsonLd("https://communityrule.info")).toEqual({
"@context": "https://schema.org",
"@type": "WebSite",
name: "CommunityRule",
url: "https://communityrule.info",
});
});
});
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.info")).toBe(PRODUCTION_SITE_ORIGIN);
expect(originFromHost("communityrule.com")).toBe("https://communityrule.com");
});
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, and still lets link unfurlers fetch the page", () => {
const staging = buildRobots("staging.communityrule.info");
expect(staging).toEqual({
rules: [
{ userAgent: "*", disallow: "/" },
{
userAgent: [
"Twitterbot",
"facebookexternalhit",
"Slackbot",
"Discordbot",
"LinkedInBot",
"TelegramBot",
"WhatsApp",
"Bluesky",
],
allow: "/",
},
],
});
expect(buildRobots("localhost:3000")).toEqual(staging);
});
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(
buildRobots("staging.communityrule.info").rules,
);
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<string, unknown>,
)) {
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);
}
}
});
});