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
+129
View File
@@ -0,0 +1,129 @@
import type { Metadata, MetadataRoute } from "next";
/** Production origin used for `metadataBase`, canonical resolution, and sitemap URLs. */
export const PRODUCTION_SITE_ORIGIN = "https://communityrule.com";
const INDEXABLE_HOSTS = new Set([
"communityrule.com",
"www.communityrule.com",
"communityrule.info",
"www.communityrule.info",
]);
export const NO_INDEX_ROBOTS = { index: false, follow: false } as const;
function hostnameFromHostHeader(host: string | null | undefined): string {
if (!host) return "";
return host.split(",")[0]?.trim().split(":")[0]?.toLowerCase() ?? "";
}
function isLoopbackHost(hostname: string): boolean {
return (
hostname === "localhost" ||
hostname === "127.0.0.1" ||
hostname === "0.0.0.0" ||
hostname === "::1"
);
}
/**
* Prefer the public hostname Cloudron's proxy forwards. Standalone binds
* `0.0.0.0`, which is not a crawlable host — fall back to `CLOUDRON_APP_ORIGIN`.
*/
export function resolveRequestHost(
forwardedHost: string | null,
host: string | null,
): string | null {
if (forwardedHost) {
const first = forwardedHost.split(",")[0]?.trim();
if (first) return first;
}
if (host) {
const hostname = hostnameFromHostHeader(host);
if (!isLoopbackHost(hostname)) return host;
}
const origin = process.env.CLOUDRON_APP_ORIGIN?.trim();
if (origin) {
try {
return new URL(origin).host;
} catch {
return host;
}
}
return host;
}
/**
* Staging, restore-drill hosts, and local dev are not indexed. Override with
* `SITE_INDEXING=true` / `false` when the hostname check is not enough.
*/
function isIndexableHost(host: string | null | undefined): boolean {
const override = process.env.SITE_INDEXING?.trim().toLowerCase();
if (override === "true") return true;
if (override === "false") return false;
return INDEXABLE_HOSTS.has(hostnameFromHostHeader(host));
}
function canonicalPath(pathname: string): string {
const trimmed = pathname.trim();
if (trimmed === "" || trimmed === "/") return "/";
const withLeading = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
const withoutQuery = withoutQueryOrHash(withLeading);
if (withoutQuery.length > 1 && withoutQuery.endsWith("/")) {
return withoutQuery.slice(0, -1);
}
return withoutQuery;
}
function withoutQueryOrHash(pathname: string): string {
const q = pathname.indexOf("?");
const h = pathname.indexOf("#");
let end = pathname.length;
if (q !== -1) end = Math.min(end, q);
if (h !== -1) end = Math.min(end, h);
return pathname.slice(0, end) || "/";
}
/** Merge `alternates.canonical` from the page path (resolved via `metadataBase`). */
export function routeMetadata(
pathname: string,
metadata: Metadata = {},
): Metadata {
return {
...metadata,
alternates: {
...metadata.alternates,
canonical: canonicalPath(pathname),
},
};
}
export function buildRobots(
host: string | null | undefined,
): MetadataRoute.Robots {
if (!isIndexableHost(host)) {
return {
rules: {
userAgent: "*",
disallow: "/",
},
};
}
return {
rules: {
userAgent: "*",
allow: "/",
},
sitemap: `${PRODUCTION_SITE_ORIGIN}/sitemap.xml`,
};
}
export function sitemapUrl(pathname: string): string {
const path = canonicalPath(pathname);
if (path === "/") return PRODUCTION_SITE_ORIGIN;
return `${PRODUCTION_SITE_ORIGIN}${path}`;
}