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:
@@ -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}`;
|
||||
}
|
||||
Reference in New Issue
Block a user