Give each route its own title and canonical, and add robots.txt plus a sitemap.

The root layout was stamping the homepage title and production root onto every page. Routes now supply a page segment through one em-dash template, canonicals follow the request path, and non-production hosts disallow crawlers.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
adilallo
2026-09-09 16:45:19 -06:00
co-authored by Cursor
parent e4e61c0c9b
commit b2b272d2cc
32 changed files with 573 additions and 72 deletions
+39
View File
@@ -0,0 +1,39 @@
import { describe, expect, it } from "vitest";
import { getAllBlogPosts } from "../../lib/content";
import { buildPublicSitemap } from "../../lib/publicSitemap";
import { PRODUCTION_SITE_ORIGIN, sitemapUrl } from "../../lib/siteMetadata";
import { USE_CASE_DETAIL_SLUGS } from "../../lib/useCaseSyntheticPost";
describe("buildPublicSitemap", () => {
it("lists public marketing routes, blog posts, and use cases", () => {
const urls = buildPublicSitemap().map((entry) => entry.url);
expect(urls).toContain(PRODUCTION_SITE_ORIGIN);
expect(urls).toContain(sitemapUrl("/about"));
expect(urls).toContain(sitemapUrl("/learn"));
expect(urls).toContain(sitemapUrl("/templates"));
expect(urls).toContain(sitemapUrl("/blog"));
expect(urls).toContain(sitemapUrl("/use-cases"));
expect(urls).toContain(sitemapUrl("/how-it-works"));
expect(urls).toContain(sitemapUrl("/privacy"));
expect(urls).toContain(sitemapUrl("/terms"));
expect(urls).toContain(sitemapUrl("/cookies"));
for (const post of getAllBlogPosts()) {
expect(urls).toContain(sitemapUrl(`/blog/${post.slug}`));
}
for (const slug of USE_CASE_DETAIL_SLUGS) {
expect(urls).toContain(sitemapUrl(`/use-cases/${slug}`));
expect(urls).toContain(sitemapUrl(`/use-cases/${slug}/rule`));
}
});
it("omits signed-in, admin, and API routes", () => {
const urls = buildPublicSitemap().map((entry) => entry.url);
expect(urls.some((url) => url.includes("/login"))).toBe(false);
expect(urls.some((url) => url.includes("/profile"))).toBe(false);
expect(urls.some((url) => url.includes("/create"))).toBe(false);
expect(urls.some((url) => url.includes("/monitor"))).toBe(false);
expect(urls.some((url) => url.includes("/api/"))).toBe(false);
});
});
+138
View File
@@ -0,0 +1,138 @@
import { afterEach, describe, expect, it } from "vitest";
import metadataMessages from "../../messages/en/metadata.json";
import {
buildRobots,
PRODUCTION_SITE_ORIGIN,
resolveRequestHost,
routeMetadata,
sitemapUrl,
} 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", () => {
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("keeps caller metadata while overriding a parent canonical", () => {
expect(
routeMetadata("/learn", { title: "Learn", description: "Copy" }),
).toEqual({
title: "Learn",
description: "Copy",
alternates: { canonical: "/learn" },
});
});
});
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("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("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);
}
}
});
});