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:
adilalloandCursor committed 2026-11-04 11:04:52 -07:00
1 parent 0b6e8b00dd
commit c180e79c38
52 files changed
+363 -209

No files matched your search

@@ -42,6 +42,7 @@ function CaseStudyView({
className = "",
}: CaseStudyProps) {
const Art = SURFACE_ART[surface];
const named = imageAlt.trim().length > 0;
return (
<div
data-figma-node="21993-32352"
@@ -52,8 +53,9 @@ function CaseStudyView({
) : (
<div className="absolute inset-0">
<Art
role="img"
aria-label={imageAlt}
role={named ? "img" : undefined}
aria-label={named ? imageAlt : undefined}
aria-hidden={named ? undefined : true}
data-case-study-art={SURFACE_ART_DATA_KEY[surface]}
width="100%"
height="100%"
@@ -50,8 +50,7 @@ const ContentContainerContainer = memo<ContentContainerProps>(
};
const iconImage = leadingImageSrc ?? getIconImage(post.slug);
const iconAlt =
leadingImageAlt ?? `Icon for ${post.frontmatter.title}`;
const iconAlt = leadingImageAlt ?? "";
const formattedDate = new Date(post.frontmatter.date).toLocaleDateString(
"en-US",
@@ -1,4 +1,5 @@
import { getNavAuthSignedIn } from "../../../lib/server/navAuth";
import { getRequestSiteOrigin } from "../../../lib/requestSiteOrigin";
import ConditionalNavigationClient from "./ConditionalNavigationClient";
/**
@@ -7,5 +8,11 @@ import ConditionalNavigationClient from "./ConditionalNavigationClient";
*/
export default async function ConditionalNavigation() {
const initialSignedIn = await getNavAuthSignedIn();
return <ConditionalNavigationClient initialSignedIn={initialSignedIn} />;
const siteOrigin = await getRequestSiteOrigin();
return (
<ConditionalNavigationClient
initialSignedIn={initialSignedIn}
siteOrigin={siteOrigin}
/>
);
}
@@ -7,6 +7,7 @@ import TopWithPathname from "./Top/TopWithPathname";
export type ConditionalNavigationClientProps = {
initialSignedIn: boolean;
siteOrigin: string;
};
/**
@@ -14,14 +15,16 @@ export type ConditionalNavigationClientProps = {
* parent Server Component (`ConditionalNavigation`) via `initialSignedIn`.
*/
const ConditionalNavigationClient = memo(
({ initialSignedIn }: ConditionalNavigationClientProps) => {
({ initialSignedIn, siteOrigin }: ConditionalNavigationClientProps) => {
const pathname = usePathname();
if (isChromelessNavigationPath(pathname)) {
return null;
}
return <TopWithPathname initialSignedIn={initialSignedIn} />;
return (
<TopWithPathname initialSignedIn={initialSignedIn} siteOrigin={siteOrigin} />
);
},
);
+3 -2
View File
@@ -6,13 +6,14 @@ import Link from "next/link";
import Logo from "../asset/Logo";
import Divider from "../utility/Divider";
import { getAssetPath, ASSETS } from "../../../lib/assetUtils";
import { PRODUCTION_SITE_ORIGIN } from "../../../lib/siteMetadata";
/**
* Figma: "Navigation / Footer" (18411-62917).
* Tiers: smallest viewports (below `md`), `md` through `lg`, `lg` and up.
* Matches `--breakpoint-md: 640px`, `--breakpoint-lg: 1024px` in `app/tailwind.css`.
*/
const Footer = memo(() => {
const Footer = memo(({ siteOrigin = PRODUCTION_SITE_ORIGIN }: { siteOrigin?: string }) => {
const t = useTranslation("footer");
const tChrome = useTranslation("controlsChrome");
@@ -36,7 +37,7 @@ const Footer = memo(() => {
"@type": "Organization",
name: t("organization.name"),
email: t("organization.email"),
url: t("organization.url"),
url: siteOrigin,
sameAs: [
t("social.bluesky.url"),
t("social.gitea.url"),
@@ -0,0 +1,8 @@
import Footer from "./Footer";
import { getRequestSiteOrigin } from "../../../lib/requestSiteOrigin";
/** Footer JSON-LD uses the request origin, same host as the page canonical. */
export default async function FooterWithOrigin() {
const siteOrigin = await getRequestSiteOrigin();
return <Footer siteOrigin={siteOrigin} />;
}
+14 -21
View File
@@ -14,6 +14,7 @@ import Button from "../../buttons/Button";
import AvatarContainer from "../../asset/AvatarContainer";
import Avatar from "../../asset/Avatar";
import { getAssetPath, ASSETS } from "../../../../lib/assetUtils";
import { PRODUCTION_SITE_ORIGIN, websiteJsonLd } from "../../../../lib/siteMetadata";
import { prepareFreshCreateFlowEntrySync } from "../../../(app)/create/utils/prepareFreshCreateFlowEntry";
import { TopView } from "./Top.view";
import type { TopProps } from "./Top.types";
@@ -24,10 +25,10 @@ export const avatarImageSources = [
getAssetPath(ASSETS.AVATAR_1),
] as const;
/** @deprecated Use `avatarImageSources` — alts are resolved in `TopContainer` via `topNav` messages. */
export const avatarImages = avatarImageSources.map((src, index) => ({
/** @deprecated Use `avatarImageSources`. The portraits are decorative. */
export const avatarImages = avatarImageSources.map((src) => ({
src,
alt: `Avatar ${3 - index}`,
alt: "",
}));
/** Padding/type that tracks lg/xl Menu sizes without cloning the items per breakpoint. */
@@ -44,12 +45,17 @@ const FOLDER_CREATE_RULE_RESPONSIVE_CLASS =
"md:p-[var(--space-200)] md:gap-[var(--space-050)] md:text-x-small-label lg:p-[var(--space-300)] lg:gap-[var(--space-150)] lg:text-medium-label xl:p-[var(--space-400)] xl:gap-[var(--space-200)] xl:text-x-large-label";
const TopContainer = memo<TopProps>(
({ folderTop = false, loggedIn = false, profile = false, logIn = true }) => {
({
folderTop = false,
loggedIn = false,
profile = false,
logIn = true,
siteOrigin = PRODUCTION_SITE_ORIGIN,
}) => {
const pathname = usePathname();
const router = useRouter();
const { openLogin } = useAuthModal();
const t = useTranslation("header");
const tTopNav = useTranslation("topNav");
/**
* `Top` is hidden on `/create` routes by ConditionalNavigationClient, so
@@ -64,20 +70,7 @@ const TopContainer = memo<TopProps>(
router.push("/create/informational");
}, [loggedIn, router]);
const schemaData = {
"@context": "https://schema.org",
"@type": "WebSite",
name: "CommunityRule",
url: "https://communityrule.com",
...(folderTop && {
description: tTopNav("schemaDescription"),
}),
potentialAction: {
"@type": "SearchAction",
target: "https://communityrule.com/search?q={search_term_string}",
"query-input": "required name=search_term_string",
},
};
const schemaData = websiteJsonLd(siteOrigin);
const logoSize = folderTop ? "topNavFolderTop" : "topNavHeader";
@@ -188,11 +181,11 @@ const TopContainer = memo<TopProps>(
: "max-sm:hidden lg:-space-x-[var(--space-250)] xl:-space-x-[13px]"
}
>
{avatarImageSources.map((src, index) => (
{avatarImageSources.map((src) => (
<Avatar
key={src}
src={src}
alt={tTopNav(`avatarAlts.${3 - index}`)}
alt=""
size="small"
className={
folderTop
+4 -8
View File
@@ -7,6 +7,8 @@ export interface TopProps {
folderTop?: boolean;
profile?: boolean;
logIn?: boolean;
/** Absolute origin for the WebSite JSON-LD block. */
siteOrigin?: string;
}
export interface TopViewProps {
@@ -15,16 +17,10 @@ export interface TopViewProps {
profile: boolean;
logIn: boolean;
schemaData: {
"@context": string;
"@type": string;
"@context": "https://schema.org";
"@type": "WebSite";
name: string;
url: string;
description?: string;
potentialAction: {
"@type": string;
target: string;
"query-input": string;
};
};
logoSize: "topNavFolderTop" | "topNavHeader";
renderNavigationItems: () => React.ReactNode;
+1 -3
View File
@@ -1,7 +1,6 @@
"use client";
import { memo } from "react";
import Script from "next/script";
import { useTranslation } from "../../../contexts/MessagesContext";
import { ASSETS, getAssetPath } from "../../../../lib/assetUtils";
import Menu from "../Menu";
@@ -32,8 +31,7 @@ function TopView({
if (folderTop) {
return (
<>
<Script
id="top-navigation-schema"
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaData) }}
/>
@@ -35,17 +35,18 @@ function CardStepsView({
/>
</div>
<div className="grid grid-cols-1 gap-y-[var(--space-600)] lg:grid-cols-3 lg:gap-[var(--space-600)]">
<ol className="m-0 grid list-none grid-cols-1 gap-y-[var(--space-600)] p-0 lg:grid-cols-3 lg:gap-[var(--space-600)]">
{steps.map((item, index) => (
<Step
key={index}
number={index + 1}
text={item.text}
iconShape={item.iconShape}
iconColor={item.iconColor}
/>
<li key={item.text} className="min-w-0">
<Step
number={index + 1}
text={item.text}
iconShape={item.iconShape}
iconColor={item.iconColor}
/>
</li>
))}
</div>
</ol>
<div className="text-center">
<Button
@@ -2,7 +2,6 @@
import { memo } from "react";
import Image from "next/image";
import { useTranslation } from "../../../contexts/MessagesContext";
import QuoteDecor from "./QuoteDecor";
import QuoteStatementDecor from "./QuoteStatementDecor";
import type { QuoteBlockViewProps } from "./QuoteBlock.types";
@@ -21,9 +20,6 @@ function QuoteBlockView({
currentAvatarSrc,
onImageError,
}: QuoteBlockViewProps) {
const t = useTranslation("quoteBlock");
const avatarAlt = t("avatarAlt").replace("{author}", author);
if (config.citationLayout) {
return (
<section
@@ -147,7 +143,7 @@ function QuoteBlockView({
{!imageError ? (
<Image
src={currentAvatarSrc}
alt={avatarAlt}
alt=""
width={64}
height={64}
className={`filter sepia ${config.avatar}`}
+2 -2
View File
@@ -18,11 +18,11 @@ const SectionNumber = memo<SectionNumberProps>(({ number }) => {
};
return (
<div className="relative size-[40px] overflow-visible">
<div className="relative size-[40px] overflow-visible" aria-hidden>
{/* eslint-disable-next-line @next/next/no-img-element -- dynamic src from getImageSrc */}
<img
src={getImageSrc(number)}
alt={`Section ${number}`}
alt=""
className="absolute left-1/2 top-1/2 size-[47.37px] max-w-none -translate-x-1/2 -translate-y-1/2 rotate-15"
/>
<div className="absolute inset-0 flex items-center justify-center">
@@ -38,8 +38,9 @@ function TripleStepView({
>
{heading}
</h2>
<ol className="m-0 flex list-none flex-col gap-[var(--space-1000)] p-0">
{steps.map((step, index) => (
<div
<li
key={step.title}
className="flex flex-col items-start gap-[var(--space-400)]"
>
@@ -58,8 +59,9 @@ function TripleStepView({
{step.body}
</p>
</div>
</div>
</li>
))}
</ol>
<div className="flex justify-start">
<Button
href={ctaHref}