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
@@ -6,6 +6,7 @@ import { routeMetadata } from "../../../lib/siteMetadata";
|
||||
|
||||
export const metadata: Metadata = routeMetadata("/create", {
|
||||
title: messages.metadata.create.title,
|
||||
description: messages.metadata.create.description,
|
||||
});
|
||||
|
||||
export default function CreateFlowLayout({ children }: { children: ReactNode }) {
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { Metadata } from "next";
|
||||
import type { ReactNode } from "react";
|
||||
import messages from "../../../../../messages/en/index";
|
||||
import { routeMetadata } from "../../../../../lib/siteMetadata";
|
||||
import { getRuleTemplateBySlug } from "../../../../../lib/server/ruleTemplates";
|
||||
import { templateReviewDocument } from "../../../../../lib/templates/governanceTemplateCatalog";
|
||||
|
||||
type LayoutProps = {
|
||||
children: ReactNode;
|
||||
params: Promise<{ slug: string }>;
|
||||
};
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: LayoutProps): Promise<Metadata> {
|
||||
const { slug: raw } = await params;
|
||||
const slug = decodeURIComponent(raw);
|
||||
const row = await getRuleTemplateBySlug(slug);
|
||||
const document = templateReviewDocument(slug, row);
|
||||
|
||||
const title = document?.title ?? messages.metadata.create.title;
|
||||
const description =
|
||||
document?.description || messages.metadata.create.description;
|
||||
return routeMetadata(`/create/review-template/${encodeURIComponent(slug)}`, {
|
||||
title: {
|
||||
absolute: messages.metadata.titleTemplate.replace("%s", title),
|
||||
},
|
||||
description,
|
||||
openGraph: { title, description },
|
||||
twitter: { title, description },
|
||||
});
|
||||
}
|
||||
|
||||
export default function TemplateReviewLayout({ children }: LayoutProps) {
|
||||
return children;
|
||||
}
|
||||
@@ -1,19 +1,18 @@
|
||||
import dynamic from "next/dynamic";
|
||||
import type { ReactNode } from "react";
|
||||
import { Suspense, type ReactNode } from "react";
|
||||
import FooterWithOrigin from "../../components/navigation/FooterWithOrigin";
|
||||
|
||||
/** Profile uses the public marketing footer; other `(app)` routes stay footer-free. */
|
||||
const Footer = dynamic(() => import("../../components/navigation/Footer"), {
|
||||
loading: () => (
|
||||
<footer className="w-full min-h-[200px] bg-[var(--color-surface-default-primary)]" />
|
||||
),
|
||||
ssr: true,
|
||||
});
|
||||
|
||||
export default function ProfileLayout({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
<Footer />
|
||||
<Suspense
|
||||
fallback={
|
||||
<footer className="min-h-[200px] w-full bg-[var(--color-surface-default-primary)]" />
|
||||
}
|
||||
>
|
||||
<FooterWithOrigin />
|
||||
</Suspense>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -6,6 +6,8 @@ import {
|
||||
type LegalDocumentMessages,
|
||||
type LegalDocumentSlug,
|
||||
} from "../../../lib/legalSyntheticPost";
|
||||
import { getRequestSiteOrigin } from "../../../lib/requestSiteOrigin";
|
||||
import { absoluteSiteUrl } from "../../../lib/siteMetadata";
|
||||
import ContentBanner from "../../components/sections/ContentBanner";
|
||||
|
||||
type LegalDocumentPageProps = {
|
||||
@@ -97,11 +99,12 @@ export function legalPageMetadata(
|
||||
* articles, Ask Organizer, and decorative shapes.
|
||||
* https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=19003-23305
|
||||
*/
|
||||
export default function LegalDocumentPage({
|
||||
export default async function LegalDocumentPage({
|
||||
slug,
|
||||
path,
|
||||
page,
|
||||
}: LegalDocumentPageProps) {
|
||||
const origin = await getRequestSiteOrigin();
|
||||
const post = buildLegalSyntheticPost(slug, page);
|
||||
|
||||
const structuredData = {
|
||||
@@ -109,12 +112,12 @@ export default function LegalDocumentPage({
|
||||
"@type": "WebPage",
|
||||
name: page.banner.title,
|
||||
description: page.banner.description,
|
||||
url: `https://communityrule.com${path}`,
|
||||
url: absoluteSiteUrl(path, origin),
|
||||
dateModified: page.banner.date,
|
||||
publisher: {
|
||||
"@type": "Organization",
|
||||
name: "CommunityRule",
|
||||
url: "https://communityrule.com",
|
||||
url: absoluteSiteUrl("/", origin),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -126,13 +129,13 @@ export default function LegalDocumentPage({
|
||||
"@type": "ListItem",
|
||||
position: 1,
|
||||
name: "Home",
|
||||
item: "https://communityrule.com",
|
||||
item: absoluteSiteUrl("/", origin),
|
||||
},
|
||||
{
|
||||
"@type": "ListItem",
|
||||
position: 2,
|
||||
name: page.banner.title,
|
||||
item: `https://communityrule.com${path}`,
|
||||
item: absoluteSiteUrl(path, origin),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -8,7 +8,9 @@ import {
|
||||
getRelatedBlogPosts,
|
||||
} from "../../../../lib/content";
|
||||
import { logger } from "../../../../lib/logger";
|
||||
import { getRequestSiteOrigin } from "../../../../lib/requestSiteOrigin";
|
||||
import {
|
||||
absoluteSiteUrl,
|
||||
articleShareImages,
|
||||
routeMetadata,
|
||||
} from "../../../../lib/siteMetadata";
|
||||
@@ -130,6 +132,8 @@ export default async function BlogPostPage({ params }: PageProps) {
|
||||
3,
|
||||
);
|
||||
|
||||
const origin = await getRequestSiteOrigin();
|
||||
|
||||
// Generate structured data for search engines
|
||||
const structuredData = {
|
||||
"@context": "https://schema.org",
|
||||
@@ -143,19 +147,19 @@ export default async function BlogPostPage({ params }: PageProps) {
|
||||
publisher: {
|
||||
"@type": "Organization",
|
||||
name: "CommunityRule",
|
||||
url: "https://communityrule.com",
|
||||
url: absoluteSiteUrl("/", origin),
|
||||
logo: {
|
||||
"@type": "ImageObject",
|
||||
url: "https://communityrule.com/assets/logos/community-rule.svg",
|
||||
url: absoluteSiteUrl("/assets/logos/community-rule.svg", origin),
|
||||
},
|
||||
},
|
||||
datePublished: post.frontmatter.date,
|
||||
dateModified: post.frontmatter.date,
|
||||
mainEntityOfPage: {
|
||||
"@type": "WebPage",
|
||||
"@id": `https://communityrule.com/blog/${post.slug}`,
|
||||
"@id": absoluteSiteUrl(`/blog/${post.slug}`, origin),
|
||||
},
|
||||
url: `https://communityrule.com/blog/${post.slug}`,
|
||||
url: absoluteSiteUrl(`/blog/${post.slug}`, origin),
|
||||
articleSection: "Community Building",
|
||||
keywords: ["community", "governance", "decision making", "collaboration"],
|
||||
};
|
||||
@@ -169,19 +173,19 @@ export default async function BlogPostPage({ params }: PageProps) {
|
||||
"@type": "ListItem",
|
||||
position: 1,
|
||||
name: "Home",
|
||||
item: "https://communityrule.com",
|
||||
item: absoluteSiteUrl("/", origin),
|
||||
},
|
||||
{
|
||||
"@type": "ListItem",
|
||||
position: 2,
|
||||
name: "Blog",
|
||||
item: "https://communityrule.com/blog",
|
||||
item: absoluteSiteUrl("/blog", origin),
|
||||
},
|
||||
{
|
||||
"@type": "ListItem",
|
||||
position: 3,
|
||||
name: post.frontmatter.title,
|
||||
item: `https://communityrule.com/blog/${post.slug}`,
|
||||
item: absoluteSiteUrl(`/blog/${post.slug}`, origin),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -10,7 +10,8 @@ import {
|
||||
buildHowItWorksSyntheticPost,
|
||||
HOW_IT_WORKS_SENTINEL_SLUG,
|
||||
} from "../../../lib/howItWorksSyntheticPost";
|
||||
import { routeMetadata } from "../../../lib/siteMetadata";
|
||||
import { getRequestSiteOrigin } from "../../../lib/requestSiteOrigin";
|
||||
import { absoluteSiteUrl, routeMetadata } from "../../../lib/siteMetadata";
|
||||
import ContentBanner from "../../components/sections/ContentBanner";
|
||||
import ContentTemplateDecorativeShapes from "../_components/ContentTemplateDecorativeShapes";
|
||||
import AskOrganizer from "../../components/sections/AskOrganizer";
|
||||
@@ -36,14 +37,15 @@ export async function generateMetadata(): Promise<Metadata> {
|
||||
keywords: meta.keywords,
|
||||
openGraph: {
|
||||
title: page.banner.title,
|
||||
description: page.banner.description,
|
||||
description: meta.description,
|
||||
type: "website",
|
||||
siteName: messages.metadata.siteName,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export default function HowItWorksPage() {
|
||||
export default async function HowItWorksPage() {
|
||||
const origin = await getRequestSiteOrigin();
|
||||
const page = messages.pages.howItWorks;
|
||||
const syntheticPost = buildHowItWorksSyntheticPost(page);
|
||||
|
||||
@@ -61,12 +63,12 @@ export default function HowItWorksPage() {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebPage",
|
||||
name: page.banner.title,
|
||||
description: page.banner.description,
|
||||
url: "https://communityrule.com/how-it-works",
|
||||
description: messages.metadata.howItWorks.description,
|
||||
url: absoluteSiteUrl("/how-it-works", origin),
|
||||
publisher: {
|
||||
"@type": "Organization",
|
||||
name: "CommunityRule",
|
||||
url: "https://communityrule.com",
|
||||
url: absoluteSiteUrl("/", origin),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -78,13 +80,13 @@ export default function HowItWorksPage() {
|
||||
"@type": "ListItem",
|
||||
position: 1,
|
||||
name: "Home",
|
||||
item: "https://communityrule.com",
|
||||
item: absoluteSiteUrl("/", origin),
|
||||
},
|
||||
{
|
||||
"@type": "ListItem",
|
||||
position: 2,
|
||||
name: page.banner.title,
|
||||
item: "https://communityrule.com/how-it-works",
|
||||
item: absoluteSiteUrl("/how-it-works", origin),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -1,22 +1,12 @@
|
||||
import dynamic from "next/dynamic";
|
||||
import { Suspense, type ReactNode } from "react";
|
||||
import ConditionalNavigation from "../components/navigation/ConditionalNavigation";
|
||||
import FooterWithOrigin from "../components/navigation/FooterWithOrigin";
|
||||
import SkipToContent from "../components/navigation/SkipToContent";
|
||||
import { MessagesProvider } from "../contexts/MessagesContext";
|
||||
import { AuthModalProvider } from "../contexts/AuthModalContext";
|
||||
import { MAIN_CONTENT_ID } from "../../lib/mainContent";
|
||||
import marketingMessages from "../../messages/en/marketing";
|
||||
|
||||
// Site footer is part of the public marketing chrome only — not rendered for
|
||||
// signed-in product surfaces, admin dashboards, or dev previews. See
|
||||
// `.cursor/rules/routes.mdc` for the full chrome composition map.
|
||||
const Footer = dynamic(() => import("../components/navigation/Footer"), {
|
||||
loading: () => (
|
||||
<footer className="bg-[var(--color-surface-default-primary)] w-full min-h-[200px]" />
|
||||
),
|
||||
ssr: true,
|
||||
});
|
||||
|
||||
export default function MarketingLayout({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<MessagesProvider messages={marketingMessages}>
|
||||
@@ -37,7 +27,13 @@ export default function MarketingLayout({ children }: { children: ReactNode }) {
|
||||
>
|
||||
{children}
|
||||
</main>
|
||||
<Footer />
|
||||
<Suspense
|
||||
fallback={
|
||||
<footer className="min-h-[200px] w-full bg-[var(--color-surface-default-primary)]" />
|
||||
}
|
||||
>
|
||||
<FooterWithOrigin />
|
||||
</Suspense>
|
||||
</AuthModalProvider>
|
||||
</MessagesProvider>
|
||||
);
|
||||
|
||||
@@ -8,7 +8,18 @@ import HeroBanner from "../components/sections/HeroBanner";
|
||||
import AskOrganizer from "../components/sections/AskOrganizer";
|
||||
import { MarketingRuleStackSection } from "./_components/MarketingRuleStackSection";
|
||||
|
||||
export const metadata: Metadata = routeMetadata("/");
|
||||
export const metadata: Metadata = routeMetadata("/", {
|
||||
title: { absolute: messages.metadata.home.title },
|
||||
description: messages.metadata.home.description,
|
||||
openGraph: {
|
||||
title: messages.metadata.home.title,
|
||||
description: messages.metadata.home.description,
|
||||
},
|
||||
twitter: {
|
||||
title: messages.metadata.home.title,
|
||||
description: messages.metadata.home.description,
|
||||
},
|
||||
});
|
||||
|
||||
// Code split below-the-fold components to reduce initial bundle size
|
||||
const LogoWall = dynamic(() => import("../components/sections/LogoWall"), {
|
||||
|
||||
@@ -10,6 +10,7 @@ import TemplatesPageClient from "./TemplatesPageClient";
|
||||
|
||||
export const metadata: Metadata = routeMetadata("/templates", {
|
||||
title: messages.metadata.templates.title,
|
||||
description: messages.metadata.templates.description,
|
||||
openGraph: {
|
||||
images: templatesShareImages(),
|
||||
},
|
||||
|
||||
@@ -12,7 +12,8 @@ import {
|
||||
USE_CASE_DETAIL_SLUGS,
|
||||
useCaseContentKeyForSlug,
|
||||
} from "../../../../lib/useCaseSyntheticPost";
|
||||
import { routeMetadata } from "../../../../lib/siteMetadata";
|
||||
import { getRequestSiteOrigin } from "../../../../lib/requestSiteOrigin";
|
||||
import { absoluteSiteUrl, 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";
|
||||
@@ -56,6 +57,7 @@ export default async function UseCaseDetailPage({ params }: PageProps) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const origin = await getRequestSiteOrigin();
|
||||
const detail = messages.pages.useCasesDetail;
|
||||
const entry = getUseCaseDetailEntry(slug, detail);
|
||||
const syntheticPost = buildUseCaseSyntheticPost(slug, detail);
|
||||
@@ -69,11 +71,11 @@ export default async function UseCaseDetailPage({ params }: PageProps) {
|
||||
"@type": "WebPage",
|
||||
name: entry.banner.title,
|
||||
description: entry.banner.description,
|
||||
url: `https://communityrule.com/use-cases/${slug}`,
|
||||
url: absoluteSiteUrl(`/use-cases/${slug}`, origin),
|
||||
publisher: {
|
||||
"@type": "Organization",
|
||||
name: "CommunityRule",
|
||||
url: "https://communityrule.com",
|
||||
url: absoluteSiteUrl("/", origin),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -85,19 +87,19 @@ export default async function UseCaseDetailPage({ params }: PageProps) {
|
||||
"@type": "ListItem",
|
||||
position: 1,
|
||||
name: "Home",
|
||||
item: "https://communityrule.com",
|
||||
item: absoluteSiteUrl("/", origin),
|
||||
},
|
||||
{
|
||||
"@type": "ListItem",
|
||||
position: 2,
|
||||
name: "Learn",
|
||||
item: "https://communityrule.com/learn",
|
||||
item: absoluteSiteUrl("/learn", origin),
|
||||
},
|
||||
{
|
||||
"@type": "ListItem",
|
||||
position: 3,
|
||||
name: entry.banner.title,
|
||||
item: `https://communityrule.com/use-cases/${slug}`,
|
||||
item: absoluteSiteUrl(`/use-cases/${slug}`, origin),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -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} />
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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,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}`}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -17,11 +17,6 @@ export interface SchemaWebSite {
|
||||
"@type": "WebSite";
|
||||
name: string;
|
||||
url: string;
|
||||
potentialAction?: {
|
||||
"@type": "SearchAction";
|
||||
target: string;
|
||||
"query-input": string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface SchemaHowTo {
|
||||
@@ -108,14 +103,10 @@ export function useSchemaData(
|
||||
url: string;
|
||||
sameAs?: string[];
|
||||
}
|
||||
| {
|
||||
| {
|
||||
type: "WebSite";
|
||||
name: string;
|
||||
url: string;
|
||||
potentialAction?: {
|
||||
target: string;
|
||||
"query-input": string;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: "HowTo";
|
||||
@@ -168,13 +159,6 @@ export function useSchemaData(
|
||||
"@type": "WebSite",
|
||||
name: config.name,
|
||||
url: config.url,
|
||||
...(config.potentialAction && {
|
||||
potentialAction: {
|
||||
"@type": "SearchAction",
|
||||
target: config.potentialAction.target,
|
||||
"query-input": config.potentialAction["query-input"],
|
||||
},
|
||||
}),
|
||||
} as SchemaWebSite;
|
||||
|
||||
case "HowTo":
|
||||
|
||||
Reference in new issue
Block a user