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
+6
View File
@@ -1,3 +1,4 @@
import type { Metadata } from "next";
import messages from "../../../messages/en/index";
import {
ASSETS,
@@ -6,6 +7,7 @@ import {
structureBeforeCrisisPath,
} from "../../../lib/assetUtils";
import { getTranslation } from "../../../lib/i18n/getTranslation";
import { routeMetadata } from "../../../lib/siteMetadata";
import AboutHeader from "../../components/type/AboutHeader";
import type { AboutHeaderSegment } from "../../components/type/AboutHeader";
import Stats from "../../components/sections/Stats";
@@ -18,6 +20,10 @@ import type { FaqAccordionItem } from "../../components/sections/Accordion";
import QuoteBlock from "../../components/sections/QuoteBlock";
import AskOrganizer from "../../components/sections/AskOrganizer";
export const metadata: Metadata = routeMetadata("/about", {
title: messages.metadata.about.title,
});
function asArray<T>(value: unknown): T[] {
return Array.isArray(value) ? value : [];
}
+12 -10
View File
@@ -8,6 +8,8 @@ import {
getRelatedBlogPosts,
} from "../../../../lib/content";
import { logger } from "../../../../lib/logger";
import { routeMetadata } from "../../../../lib/siteMetadata";
import messages from "../../../../messages/en/index";
import ContentBanner from "../../../components/sections/ContentBanner";
import AskOrganizer from "../../../components/sections/AskOrganizer";
import ContentTemplateDecorativeShapes from "../../_components/ContentTemplateDecorativeShapes";
@@ -62,13 +64,13 @@ export async function generateMetadata({
const post = getBlogPostBySlug(slug);
if (!post) {
return {
title: "Post Not Found",
description: "The requested blog post could not be found.",
};
return routeMetadata(`/blog/${slug}`, {
title: messages.metadata.blog.notFoundTitle,
description: messages.metadata.blog.fallbackDescription,
});
}
return {
return routeMetadata(`/blog/${slug}`, {
title: post.frontmatter.title,
description: post.frontmatter.description,
authors: [{ name: post.frontmatter.author }],
@@ -78,8 +80,8 @@ export async function generateMetadata({
type: "article",
publishedTime: post.frontmatter.date,
authors: [post.frontmatter.author],
url: `https://communityrule.com/blog/${slug}`,
siteName: "CommunityRule",
url: `/blog/${slug}`,
siteName: messages.metadata.siteName,
},
twitter: {
card: "summary_large_image",
@@ -87,12 +89,12 @@ export async function generateMetadata({
description: post.frontmatter.description,
creator: "@communityrule",
},
};
});
} catch (error) {
logger.error("Error generating metadata:", error);
return {
title: "Blog Post",
description: "A blog post from our community.",
title: messages.metadata.blog.fallbackTitle,
description: messages.metadata.blog.fallbackDescription,
};
}
}
+16 -16
View File
@@ -1,26 +1,27 @@
import { getAllBlogPosts } from "../../../lib/content";
import ContentThumbnailTemplate from "../../components/content/ContentThumbnailTemplate";
import type { Metadata } from "next";
import messages from "../../../messages/en/index";
import { routeMetadata } from "../../../lib/siteMetadata";
export const metadata: Metadata = {
title: "Blog - CommunityRule",
description:
"Learn about community governance, decision-making, and building successful organizations.",
const blogMeta = messages.metadata.blog;
export const metadata: Metadata = routeMetadata("/blog", {
title: blogMeta.title,
description: blogMeta.description,
openGraph: {
title: "Blog - CommunityRule",
description:
"Learn about community governance, decision-making, and building successful organizations.",
url: "https://communityrule.com/blog",
siteName: "CommunityRule",
title: blogMeta.title,
description: blogMeta.description,
url: "/blog",
siteName: messages.metadata.siteName,
type: "website",
},
twitter: {
card: "summary_large_image",
title: "Blog - CommunityRule",
description:
"Learn about community governance, decision-making, and building successful organizations.",
title: blogMeta.title,
description: blogMeta.description,
},
};
});
export default function BlogPage() {
const posts = getAllBlogPosts();
@@ -34,11 +35,10 @@ export default function BlogPage() {
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<div className="text-center mb-12">
<h1 className="text-4xl md:text-5xl font-bold text-[var(--color-content-default-primary)] mb-4">
Blog
{blogMeta.title}
</h1>
<p className="text-lg text-[var(--color-content-default-secondary)] max-w-2xl mx-auto">
Learn about community governance, decision-making, and building
successful organizations.
{blogMeta.description}
</p>
</div>
+5 -1
View File
@@ -5,6 +5,7 @@
import type { Metadata } from "next";
import messages from "../../../messages/en/index";
import { legalPathForSlug } from "../../../lib/legalSyntheticPost";
import { routeMetadata } from "../../../lib/siteMetadata";
import LegalDocumentPage, {
legalPageMetadata,
} from "../_components/LegalDocumentPage";
@@ -13,7 +14,10 @@ const SLUG = "cookies" as const;
const PATH = legalPathForSlug(SLUG);
export function generateMetadata(): Metadata {
return legalPageMetadata(messages.metadata.cookies, messages.pages.cookies);
return routeMetadata(
PATH,
legalPageMetadata(messages.metadata.cookies, messages.pages.cookies),
);
}
export default function CookiesPage() {
+4 -3
View File
@@ -10,6 +10,7 @@ import {
buildHowItWorksSyntheticPost,
HOW_IT_WORKS_SENTINEL_SLUG,
} from "../../../lib/howItWorksSyntheticPost";
import { routeMetadata } from "../../../lib/siteMetadata";
import ContentBanner from "../../components/sections/ContentBanner";
import ContentTemplateDecorativeShapes from "../_components/ContentTemplateDecorativeShapes";
import AskOrganizer from "../../components/sections/AskOrganizer";
@@ -29,7 +30,7 @@ export async function generateMetadata(): Promise<Metadata> {
const meta = messages.metadata.howItWorks;
const page = messages.pages.howItWorks;
return {
return routeMetadata("/how-it-works", {
title: meta.title,
description: meta.description,
keywords: meta.keywords,
@@ -37,9 +38,9 @@ export async function generateMetadata(): Promise<Metadata> {
title: page.banner.title,
description: page.banner.description,
type: "website",
siteName: "CommunityRule",
siteName: messages.metadata.siteName,
},
};
});
}
export default function HowItWorksPage() {
+6
View File
@@ -1,10 +1,16 @@
import type { Metadata } from "next";
import messages from "../../../messages/en/index";
import { getTranslation } from "../../../lib/i18n/getTranslation";
import { routeMetadata } from "../../../lib/siteMetadata";
import ContentThumbnailTemplate from "../../components/content/ContentThumbnailTemplate";
import ContentLockup from "../../components/type/ContentLockup";
import AskOrganizer from "../../components/sections/AskOrganizer";
import { getAllBlogPosts } from "../../../lib/content";
export const metadata: Metadata = routeMetadata("/learn", {
title: messages.metadata.learn.title,
});
export default function LearnPage() {
const allPosts = getAllBlogPosts();
+4
View File
@@ -1,11 +1,15 @@
import type { Metadata } from "next";
import dynamic from "next/dynamic";
import { Suspense } from "react";
import messages from "../../messages/en/index";
import { getTranslation } from "../../lib/i18n/getTranslation";
import { routeMetadata } from "../../lib/siteMetadata";
import HeroBanner from "../components/sections/HeroBanner";
import AskOrganizer from "../components/sections/AskOrganizer";
import { MarketingRuleStackSection } from "./_components/MarketingRuleStackSection";
export const metadata: Metadata = routeMetadata("/");
// Code split below-the-fold components to reduce initial bundle size
const LogoWall = dynamic(() => import("../components/sections/LogoWall"), {
loading: () => (
+5 -1
View File
@@ -5,6 +5,7 @@
import type { Metadata } from "next";
import messages from "../../../messages/en/index";
import { legalPathForSlug } from "../../../lib/legalSyntheticPost";
import { routeMetadata } from "../../../lib/siteMetadata";
import LegalDocumentPage, {
legalPageMetadata,
} from "../_components/LegalDocumentPage";
@@ -13,7 +14,10 @@ const SLUG = "privacy" as const;
const PATH = legalPathForSlug(SLUG);
export function generateMetadata(): Metadata {
return legalPageMetadata(messages.metadata.privacy, messages.pages.privacy);
return routeMetadata(
PATH,
legalPageMetadata(messages.metadata.privacy, messages.pages.privacy),
);
}
export default function PrivacyPage() {
+5 -4
View File
@@ -3,6 +3,7 @@ import { notFound } from "next/navigation";
import messages from "../../../../messages/en/index";
import { getPublicPublishedRuleById } from "../../../../lib/server/publishedRules";
import { parsePublishedDocumentForCommunityRuleDisplay } from "../../../../lib/create/publishedDocumentToDisplaySections";
import { routeMetadata } from "../../../../lib/siteMetadata";
import CommunityRule from "../../../components/type/CommunityRule";
import HeaderLockup from "../../../components/type/HeaderLockup";
@@ -25,22 +26,22 @@ export async function generateMetadata({
typeof rule.summary === "string" && rule.summary.trim().length > 0
? rule.summary
: undefined;
return {
return routeMetadata(`/rules/${rule.id}`, {
title: rule.title,
description,
openGraph: {
title: rule.title,
description,
type: "article",
url: `https://communityrule.com/rules/${rule.id}`,
siteName: "CommunityRule",
url: `/rules/${rule.id}`,
siteName: messages.metadata.siteName,
},
twitter: {
card: "summary_large_image",
title: rule.title,
description,
},
};
});
}
export default async function PublicRuleDetailPage({ params }: PageProps) {
+7
View File
@@ -1,7 +1,14 @@
import type { Metadata } from "next";
import messages from "../../../messages/en/index";
import { listRuleTemplatesFromDb } from "../../../lib/server/ruleTemplates";
import { routeMetadata } from "../../../lib/siteMetadata";
import { gridEntriesForFullCatalogWithFallback } from "../../../lib/templates/templateGridPresentation";
import TemplatesPageClient from "./TemplatesPageClient";
export const metadata: Metadata = routeMetadata("/templates", {
title: messages.metadata.templates.title,
});
export default async function TemplatesPage() {
const rows = await listRuleTemplatesFromDb();
const initialGridEntries = gridEntriesForFullCatalogWithFallback(rows);
+5 -1
View File
@@ -5,6 +5,7 @@
import type { Metadata } from "next";
import messages from "../../../messages/en/index";
import { legalPathForSlug } from "../../../lib/legalSyntheticPost";
import { routeMetadata } from "../../../lib/siteMetadata";
import LegalDocumentPage, {
legalPageMetadata,
} from "../_components/LegalDocumentPage";
@@ -13,7 +14,10 @@ const SLUG = "terms" as const;
const PATH = legalPathForSlug(SLUG);
export function generateMetadata(): Metadata {
return legalPageMetadata(messages.metadata.terms, messages.pages.terms);
return routeMetadata(
PATH,
legalPageMetadata(messages.metadata.terms, messages.pages.terms),
);
}
export default function TermsPage() {
+4 -3
View File
@@ -12,6 +12,7 @@ import {
USE_CASE_DETAIL_SLUGS,
useCaseContentKeyForSlug,
} from "../../../../lib/useCaseSyntheticPost";
import { routeMetadata } from "../../../../lib/siteMetadata";
import ContentBanner from "../../../components/sections/ContentBanner";
import AskOrganizer from "../../../components/sections/AskOrganizer";
import type { AskOrganizerVariant } from "../../../components/sections/AskOrganizer/AskOrganizer.types";
@@ -34,7 +35,7 @@ export async function generateMetadata({ params }: PageProps): Promise<Metadata>
const contentKey = useCaseContentKeyForSlug(slug);
const meta = messages.metadata.useCasesDetail[contentKey];
return {
return routeMetadata(`/use-cases/${slug}`, {
title: meta.title,
description: meta.description,
keywords: meta.keywords,
@@ -42,9 +43,9 @@ export async function generateMetadata({ params }: PageProps): Promise<Metadata>
title: meta.title,
description: meta.description,
type: "website",
siteName: "CommunityRule",
siteName: messages.metadata.siteName,
},
};
});
}
export default async function UseCaseDetailPage({ params }: PageProps) {
+4 -3
View File
@@ -5,6 +5,7 @@ import { Suspense } from "react";
import messages from "../../../messages/en/index";
import { CONTENT_CATALOG_SLUG_ORDER } from "../../../lib/assetUtils";
import { getAllBlogPosts, getRelatedBlogPosts } from "../../../lib/content";
import { routeMetadata } from "../../../lib/siteMetadata";
import PageHeader from "../../components/type/PageHeader";
import CaseStudy from "../../components/cards/CaseStudy";
import UseCasesOrgs from "../../components/sections/UseCasesOrgs";
@@ -60,7 +61,7 @@ export async function generateMetadata(): Promise<Metadata> {
const description = messages.metadata.useCases.description;
const keywords = messages.metadata.useCases.keywords;
return {
return routeMetadata("/use-cases", {
title,
description,
keywords,
@@ -68,9 +69,9 @@ export async function generateMetadata(): Promise<Metadata> {
title,
description,
type: "website",
siteName: "CommunityRule",
siteName: messages.metadata.siteName,
},
};
});
}
export default function UseCasesPage() {