Staging no longer advertises production canonicals, and Slack/social previews get a default hero card with template and article overrides. Co-authored-by: Cursor <cursoragent@cursor.com>
223 lines
7.2 KiB
TypeScript
223 lines
7.2 KiB
TypeScript
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<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);
|
||
}
|
||
}
|
||
});
|
||
});
|