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}`; }