Give every public route its own description, and keep canonicals and structured data on the host that serves the page.
Decorative images on the marketing pages stay quiet, and every dialog close button is named Close. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
0b6e8b00dd
commit
c180e79c38
52 files changed
+363
-209
No files matched your search
@@ -0,0 +1,12 @@
|
||||
import { headers } from "next/headers";
|
||||
import { originFromHost, resolveRequestHost } from "./siteMetadata";
|
||||
|
||||
/** Public origin of this request. Prerender falls back inside `originFromHost`. */
|
||||
export async function getRequestSiteOrigin(): Promise<string> {
|
||||
const headerStore = await headers();
|
||||
const host = resolveRequestHost(
|
||||
headerStore.get("x-forwarded-host"),
|
||||
headerStore.get("host"),
|
||||
);
|
||||
return originFromHost(host);
|
||||
}
|
||||
+39
-6
@@ -7,8 +7,11 @@ import {
|
||||
} from "./assetUtils";
|
||||
import metadataMessages from "../messages/en/metadata.json";
|
||||
|
||||
/** Production origin used for sitemap URLs and as the metadata-host fallback. */
|
||||
export const PRODUCTION_SITE_ORIGIN = "https://communityrule.com";
|
||||
/**
|
||||
* Production origin. Sitemap URLs and prerendered canonicals use this when
|
||||
* no request host and no `CLOUDRON_APP_ORIGIN` are available.
|
||||
*/
|
||||
export const PRODUCTION_SITE_ORIGIN = "https://communityrule.info";
|
||||
|
||||
/** Open Graph / Twitter large-card raster size. */
|
||||
const OG_IMAGE_SIZE = { width: 1200, height: 630 } as const;
|
||||
@@ -115,6 +118,31 @@ export function metadataBaseUrl(): URL {
|
||||
return new URL(originFromHost(null));
|
||||
}
|
||||
|
||||
/** Origin baked into prerendered pages: app origin when set, otherwise production. */
|
||||
export function configuredSiteOrigin(): string {
|
||||
return originFromHost(null);
|
||||
}
|
||||
|
||||
/** Absolute URL for canonicals and JSON-LD. Same origin as `metadataBase`. */
|
||||
export function absoluteSiteUrl(
|
||||
pathname: string,
|
||||
origin: string = configuredSiteOrigin(),
|
||||
): string {
|
||||
const path = canonicalPath(pathname);
|
||||
if (path === "/") return origin;
|
||||
return `${origin}${path}`;
|
||||
}
|
||||
|
||||
/** WebSite block. No SearchAction — the site has no search page. */
|
||||
export function websiteJsonLd(origin: string) {
|
||||
return {
|
||||
"@context": "https://schema.org" as const,
|
||||
"@type": "WebSite" as const,
|
||||
name: "CommunityRule",
|
||||
url: origin,
|
||||
};
|
||||
}
|
||||
|
||||
type ShareImage = {
|
||||
url: string;
|
||||
width: number;
|
||||
@@ -207,8 +235,13 @@ export function routeMetadata(
|
||||
const defaultImages = defaultShareImages();
|
||||
const openGraph = metadata.openGraph ?? {};
|
||||
const twitter = metadata.twitter ?? {};
|
||||
const pageTitle = typeof metadata.title === "string" ? metadata.title : undefined;
|
||||
const pageDescription =
|
||||
typeof metadata.description === "string" ? metadata.description : undefined;
|
||||
const mergedOpenGraph = {
|
||||
images: defaultImages,
|
||||
...(pageTitle ? { title: pageTitle } : {}),
|
||||
...(pageDescription ? { description: pageDescription } : {}),
|
||||
...openGraph,
|
||||
url: openGraph.url ?? path,
|
||||
};
|
||||
@@ -232,7 +265,9 @@ export function routeMetadata(
|
||||
},
|
||||
openGraph: mergedOpenGraph,
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
card: "summary_large_image" as const,
|
||||
...(pageTitle ? { title: pageTitle } : {}),
|
||||
...(pageDescription ? { description: pageDescription } : {}),
|
||||
images: derivedTwitterImages,
|
||||
...twitter,
|
||||
},
|
||||
@@ -276,7 +311,5 @@ export function buildRobots(
|
||||
}
|
||||
|
||||
export function sitemapUrl(pathname: string): string {
|
||||
const path = canonicalPath(pathname);
|
||||
if (path === "/") return PRODUCTION_SITE_ORIGIN;
|
||||
return `${PRODUCTION_SITE_ORIGIN}${path}`;
|
||||
return absoluteSiteUrl(pathname, PRODUCTION_SITE_ORIGIN);
|
||||
}
|
||||
@@ -165,3 +165,21 @@ export function getGovernanceTemplateCatalogEntry(
|
||||
): GovernanceTemplateCatalogEntry | undefined {
|
||||
return bySlug.get(slug);
|
||||
}
|
||||
|
||||
/**
|
||||
* Title and description for a template review document. A saved row wins;
|
||||
* otherwise the static catalog. Unknown slugs return null so the page can
|
||||
* fall back to the create-flow title.
|
||||
*/
|
||||
export function templateReviewDocument(
|
||||
slug: string,
|
||||
db: { title?: string | null; description?: string | null } | null,
|
||||
): { title: string; description: string } | null {
|
||||
const catalog = bySlug.get(slug);
|
||||
const title = db?.title?.trim() || catalog?.title;
|
||||
if (!title) return null;
|
||||
return {
|
||||
title,
|
||||
description: db?.description?.trim() || catalog?.description || "",
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user