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:
@@ -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 : [];
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,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() {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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,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() {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,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() {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user