fix(docs): align SEO metadata with page content

Co-authored-by: Junior Garcia <jrgarciadev@gmail.com>
This commit is contained in:
Cursor Agent
2026-08-13 21:28:43 +00:00
committed by Junior Garcia
parent 56f2278c25
commit 2e65a8b5a9
9 changed files with 55 additions and 39 deletions
@@ -26,6 +26,7 @@ import {PRContributors, fetchPRContributors} from "@/components/pr-contributors"
import StatusChip from "@/components/status-chip"; import StatusChip from "@/components/status-chip";
import {siteConfig} from "@/config/site"; import {siteConfig} from "@/config/site";
import {getComponentCount, getExampleCount} from "@/demos"; import {getComponentCount, getExampleCount} from "@/demos";
import {getDocsSeoMetadata} from "@/lib/docs-seo";
import {getBreadcrumbJsonLd, getTechArticleJsonLd} from "@/lib/json-ld"; import {getBreadcrumbJsonLd, getTechArticleJsonLd} from "@/lib/json-ld";
import {absoluteUrl, getLocalizedAlternates, localizedPath, stripLocale} from "@/lib/seo"; import {absoluteUrl, getLocalizedAlternates, localizedPath, stripLocale} from "@/lib/seo";
import {source} from "@/lib/source"; import {source} from "@/lib/source";
@@ -38,28 +39,6 @@ import {
// import { getGithubLastEdit } from "fumadocs-core/server"; // import { getGithubLastEdit } from "fumadocs-core/server";
const componentStatusIcons = ["preview", "new", "updated"]; const componentStatusIcons = ["preview", "new", "updated"];
const ENGLISH_SEO_OVERRIDES: Record<string, {description: string; title: string}> = {
"/docs/react/components": {
description:
"Browse accessible HeroUI React components for forms, overlays, navigation, data display, and more, built with React Aria and Tailwind CSS v4.",
title: "HeroUI React Components Accessible UI Library",
},
"/docs/react/components/button": {
description:
"Build accessible React buttons with HeroUI. Explore variants, sizes, loading and disabled states, icon buttons, events, and ButtonGroup patterns.",
title: "HeroUI Button Accessible React Button Component",
},
"/docs/react/components/select": {
description:
"Build accessible React select menus with HeroUI. Learn options, sections, validation, disabled states, controlled values, and customizable listboxes.",
title: "HeroUI Select Accessible React Select Component",
},
"/docs/react/getting-started": {
description:
"Get started with HeroUI v3, an accessible React UI library built on React Aria and Tailwind CSS v4. Learn installation, principles, and customization.",
title: "HeroUI v3 React UI Library Getting Started",
},
};
const getRawMDXContent = cache(async (pagePath: string): Promise<string> => { const getRawMDXContent = cache(async (pagePath: string): Promise<string> => {
try { try {
@@ -111,6 +90,7 @@ export default async function Page(props: {params: Promise<{lang: string; slug?:
const slugParts = params.slug ?? []; const slugParts = params.slug ?? [];
const pagePath = `/docs/${slugParts.join("/")}`; const pagePath = `/docs/${slugParts.join("/")}`;
const pageUrl = absoluteUrl(localizedPath(params.lang, pagePath)); const pageUrl = absoluteUrl(localizedPath(params.lang, pagePath));
const seoMetadata = getDocsSeoMetadata(params.lang, pagePath);
const breadcrumbItems = [ const breadcrumbItems = [
{name: "Home", url: absoluteUrl(localizedPath(params.lang))}, {name: "Home", url: absoluteUrl(localizedPath(params.lang))},
@@ -132,8 +112,8 @@ export default async function Page(props: {params: Promise<{lang: string; slug?:
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: JSON.stringify( __html: JSON.stringify(
getTechArticleJsonLd({ getTechArticleJsonLd({
description: page.data.description ?? "", description: seoMetadata?.description ?? page.data.description ?? "",
title: page.data.title, title: seoMetadata?.title ?? page.data.title,
url: pageUrl, url: pageUrl,
}), }),
), ),
@@ -219,7 +199,7 @@ export async function generateMetadata(props: {
const url = page.url; const url = page.url;
const unlocalizedPath = stripLocale(url); const unlocalizedPath = stripLocale(url);
const alternates = getLocalizedAlternates({locale: params.lang, path: unlocalizedPath}); const alternates = getLocalizedAlternates({locale: params.lang, path: unlocalizedPath});
const seoOverride = params.lang === "en" ? ENGLISH_SEO_OVERRIDES[unlocalizedPath] : undefined; const seoOverride = getDocsSeoMetadata(params.lang, unlocalizedPath);
const description = seoOverride?.description ?? page.data.description; const description = seoOverride?.description ?? page.data.description;
const title = seoOverride?.title ?? page.data.title; const title = seoOverride?.title ?? page.data.title;
+1 -1
View File
@@ -68,7 +68,7 @@ export default async function ThemeBuilderPage({params}: {params: Promise<{lang:
className="grid h-dvh grid-rows-[auto_1fr_auto] bg-background px-4 sm:overflow-hidden sm:px-6" className="grid h-dvh grid-rows-[auto_1fr_auto] bg-background px-4 sm:overflow-hidden sm:px-6"
id={THEME_BUILDER_PAGE_ID} id={THEME_BUILDER_PAGE_ID}
> >
<h1 className="sr-only">{dict.themes.metaTitle}</h1> <h1 className="sr-only">{dict.themes.heading}</h1>
<BuilderHeader /> <BuilderHeader />
<ThemeBuilderContent /> <ThemeBuilderContent />
<div className="mx-auto hidden items-center justify-between gap-4 py-6 max-[1200px]:flex-col sm:flex"> <div className="mx-auto hidden items-center justify-between gap-4 py-6 max-[1200px]:flex-col sm:flex">
+6 -2
View File
@@ -8,6 +8,8 @@ import {notFound} from "next/navigation";
import {ImageResponse} from "next/og"; import {ImageResponse} from "next/og";
import {HeroUILogo} from "@/components/heroui-logo"; import {HeroUILogo} from "@/components/heroui-logo";
import {getDocsSeoMetadata} from "@/lib/docs-seo";
import {stripLocale} from "@/lib/seo";
import {source} from "@/lib/source"; import {source} from "@/lib/source";
interface GenerateProps { interface GenerateProps {
@@ -93,8 +95,10 @@ export const GET = async (_req: Request, {params}: {params: Promise<{slug: strin
if (!page) notFound(); if (!page) notFound();
const seoMetadata = getDocsSeoMetadata(page.locale, stripLocale(page.url));
return generateOGImage({ return generateOGImage({
description: page.data.description, description: seoMetadata?.description ?? page.data.description,
fonts: [ fonts: [
{ {
data: interRegular, data: interRegular,
@@ -108,7 +112,7 @@ export const GET = async (_req: Request, {params}: {params: Promise<{slug: strin
}, },
], ],
icon: <HeroUILogo size={58} />, icon: <HeroUILogo size={58} />,
title: page.data.title, title: seoMetadata?.title ?? page.data.title,
}); });
}; };
+2 -1
View File
@@ -117,8 +117,9 @@
"androidComingSoon": "Android · 即将推出" "androidComingSoon": "Android · 即将推出"
}, },
"themes": { "themes": {
"heading": "主题编辑器",
"metaTitle": "HeroUI 主题编辑器 自定义 React UI", "metaTitle": "HeroUI 主题编辑器 自定义 React UI",
"metaDescription": "通过可视化控件自定义 HeroUI React 主题,调整颜色、字体与圆角,实时预览组件并复制可用于生产环境的 Tailwind CSS v4 变量。", "metaDescription": "通过可视化控件自定义 HeroUI React 主题,调整颜色、字体与圆角,实时预览组件并复制可用于生产环境的 HeroUI CSS 变量。",
"theme": "主题", "theme": "主题",
"custom": "自定义", "custom": "自定义",
"designTheme": "设计主题", "designTheme": "设计主题",
+2 -1
View File
@@ -117,8 +117,9 @@
"androidComingSoon": "Android · Coming soon" "androidComingSoon": "Android · Coming soon"
}, },
"themes": { "themes": {
"heading": "Theme Builder",
"metaTitle": "HeroUI Theme Builder Customize React UI", "metaTitle": "HeroUI Theme Builder Customize React UI",
"metaDescription": "Customize HeroUI React themes visually. Tune colors, typography, and radius, preview components live, and copy production-ready Tailwind CSS v4 variables.", "metaDescription": "Customize HeroUI React themes visually. Tune colors, typography, and radius, preview components live, and copy production-ready HeroUI CSS variables.",
"theme": "Theme", "theme": "Theme",
"custom": "Custom", "custom": "Custom",
"designTheme": "Design theme", "designTheme": "Design theme",
+34
View File
@@ -0,0 +1,34 @@
export interface DocsSeoMetadata {
description: string;
title: string;
}
const ENGLISH_DOCS_SEO_METADATA: Readonly<Record<string, DocsSeoMetadata>> = {
"/docs/react/components": {
description:
"Browse accessible HeroUI React components for forms, overlays, navigation, data display, and more, built with React Aria and Tailwind CSS v4.",
title: "HeroUI React Components Accessible UI Library",
},
"/docs/react/components/button": {
description:
"Build accessible React buttons with HeroUI. Explore variants, sizes, icon-only states, custom styles, ripple effects, render props, and BEM classes.",
title: "HeroUI Button Accessible React Button Component",
},
"/docs/react/components/select": {
description:
"Build accessible React select inputs with HeroUI. Explore single and multiple selection, async loading, sections, disabled items, and controlled values.",
title: "HeroUI Select Accessible React Select Component",
},
"/docs/react/getting-started": {
description:
"Meet HeroUI v3, an accessible React UI library built on React Aria and Tailwind CSS v4. Explore its design approach, ecosystem, and common questions.",
title: "Introduction to HeroUI v3 React UI Library",
},
};
export function getDocsSeoMetadata(
locale: string | undefined,
unlocalizedPath: string,
): DocsSeoMetadata | undefined {
return locale === "en" ? ENGLISH_DOCS_SEO_METADATA[unlocalizedPath] : undefined;
}
+1 -1
View File
@@ -65,7 +65,7 @@ node scripts/get_hooks_migration_guide.mjs
### Direct URLs ### Direct URLs
Migration docs (preview): `https://heroui-git-docs-migration-heroui.vercel.app/docs/react/migration/{filename}` Migration docs (preview): use a concrete guide URL from the examples below, and never fetch a URL that still contains a placeholder.
Examples: Examples:
+2 -4
View File
@@ -76,8 +76,7 @@ node scripts/get_docs.mjs /docs/native/getting-started/theming
### Direct MDX URLs ### Direct MDX URLs
Use a concrete kebab-case component slug in every MDX request. Never request an unresolved Component docs: fetch `.mdx` with a concrete kebab-case slug. Run `node scripts/list_components.mjs` when the slug is unknown, and never fetch a URL that still contains a placeholder.
template URL.
Examples: Examples:
@@ -85,8 +84,7 @@ Examples:
- Dialog: `https://heroui.com/docs/native/components/dialog.mdx` - Dialog: `https://heroui.com/docs/native/components/dialog.mdx`
- TextField: `https://heroui.com/docs/native/components/text-field.mdx` - TextField: `https://heroui.com/docs/native/components/text-field.mdx`
For getting-started guides, use a concrete topic URL such as Getting started guides: use a concrete topic URL such as `https://heroui.com/docs/native/getting-started/quick-start.mdx`.
`https://heroui.com/docs/native/getting-started/quick-start.mdx`.
**Important:** Always fetch component docs before implementing. The MDX docs include complete examples, props, anatomy, and API references. **Important:** Always fetch component docs before implementing. The MDX docs include complete examples, props, anatomy, and API references.
+2 -4
View File
@@ -98,8 +98,7 @@ node scripts/get_docs.mjs /docs/react/getting-started/theming
### Direct MDX URLs ### Direct MDX URLs
Use a concrete kebab-case component slug in every MDX request. Never request an unresolved Component docs: fetch `.mdx` with a concrete kebab-case slug. Run `node scripts/list_components.mjs` when the slug is unknown, and never fetch a URL that still contains a placeholder.
template URL.
Examples: Examples:
@@ -107,8 +106,7 @@ Examples:
- Modal: `https://heroui.com/docs/react/components/modal.mdx` - Modal: `https://heroui.com/docs/react/components/modal.mdx`
- Form: `https://heroui.com/docs/react/components/form.mdx` - Form: `https://heroui.com/docs/react/components/form.mdx`
For getting-started guides, use a concrete topic URL such as Getting started guides: use a concrete topic URL such as `https://heroui.com/docs/react/getting-started/quick-start.mdx`.
`https://heroui.com/docs/react/getting-started/quick-start.mdx`.
**Important:** Always fetch component docs before implementing. The MDX docs include complete examples, props, anatomy, and API references. **Important:** Always fetch component docs before implementing. The MDX docs include complete examples, props, anatomy, and API references.