From 4b9f770534607e07ee2b95fd940ff10254fba653 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Thu, 28 May 2026 00:31:34 +0800 Subject: [PATCH] feat(docs): add comprehensive SEO optimization for static docs site Add per-page metadata, Open Graph / Twitter tags, canonical URLs, hreflang, sitemap generation, robots.txt, and JSON-LD structured data for the bilingual (en/cn) documentation site. --- docs/app/[lang]/changelog/page.tsx | 19 +++++ docs/app/[lang]/community/page.tsx | 19 +++++ docs/app/[lang]/docs/[[...slug]]/page.tsx | 68 ++++++++++++++++- docs/app/[lang]/layout.tsx | 46 ++++++++++++ docs/app/[lang]/page.tsx | 34 +++++++++ docs/app/layout.tsx | 42 +++++++++-- docs/lib/metadata.ts | 75 +++++++++++++++++++ docs/package.json | 1 + docs/public/robots.txt | 4 + docs/scripts/generate-sitemap.mjs | 90 +++++++++++++++++++++++ 10 files changed, 391 insertions(+), 7 deletions(-) create mode 100644 docs/lib/metadata.ts create mode 100644 docs/public/robots.txt create mode 100644 docs/scripts/generate-sitemap.mjs diff --git a/docs/app/[lang]/changelog/page.tsx b/docs/app/[lang]/changelog/page.tsx index 04d855b9a..34e28cf14 100644 --- a/docs/app/[lang]/changelog/page.tsx +++ b/docs/app/[lang]/changelog/page.tsx @@ -1,6 +1,8 @@ import { LandingNav } from '@/components/landing/LandingNav'; import { ChangelogRuntime } from '@/components/landing/ChangelogRuntime'; import { fetchChangelog } from '@/lib/changelog'; +import { buildMetadata } from '@/lib/metadata'; +import type { Metadata } from 'next'; const i18n = { en: { @@ -13,6 +15,23 @@ const i18n = { }, }; +export async function generateMetadata({ + params, +}: { + params: Promise<{ lang: string }>; +}): Promise { + const { lang } = await params; + const l = lang === 'cn' ? 'cn' : 'en'; + const t = i18n[l]; + + return buildMetadata({ + title: t.title, + description: t.desc, + path: `/${l}/changelog`, + lang: l, + }); +} + export default async function ChangelogPage({ params }: { params: Promise<{ lang: string }> }) { const { lang } = await params; const l = lang === 'cn' ? 'cn' : 'en'; diff --git a/docs/app/[lang]/community/page.tsx b/docs/app/[lang]/community/page.tsx index 064481577..bc1950723 100644 --- a/docs/app/[lang]/community/page.tsx +++ b/docs/app/[lang]/community/page.tsx @@ -1,4 +1,6 @@ import { LandingNav } from '@/components/landing/LandingNav'; +import { buildMetadata } from '@/lib/metadata'; +import type { Metadata } from 'next'; const channels = [ { @@ -58,6 +60,23 @@ const i18n = { }, }; +export async function generateMetadata({ + params, +}: { + params: Promise<{ lang: string }>; +}): Promise { + const { lang } = await params; + const l = lang === 'cn' ? 'cn' : 'en'; + const t = i18n[l]; + + return buildMetadata({ + title: t.title, + description: t.desc, + path: `/${l}/community`, + lang: l, + }); +} + export default async function CommunityPage({ params }: { params: Promise<{ lang: string }> }) { const { lang } = await params; const l = lang === 'cn' ? 'cn' : 'en'; diff --git a/docs/app/[lang]/docs/[[...slug]]/page.tsx b/docs/app/[lang]/docs/[[...slug]]/page.tsx index 62cf86f97..4af45f99e 100644 --- a/docs/app/[lang]/docs/[[...slug]]/page.tsx +++ b/docs/app/[lang]/docs/[[...slug]]/page.tsx @@ -1,5 +1,6 @@ import { source } from '@/lib/source'; import { notFound } from 'next/navigation'; +import type { Metadata } from 'next'; import { DocsPage, DocsBody, @@ -15,6 +16,28 @@ import { ImageZoom } from 'fumadocs-ui/components/image-zoom'; import { Accordion, Accordions } from 'fumadocs-ui/components/accordion'; import type { MDXContent } from 'mdx/types'; import type { TOCItemType } from 'fumadocs-core/toc'; +import { buildMetadata, SITE_URL } from '@/lib/metadata'; + +export async function generateMetadata({ + params, +}: { + params: Promise<{ lang: string; slug?: string[] }>; +}): Promise { + const { lang, slug } = await params; + const page = source.getPage(slug, lang); + if (!page) return {}; + + const title = page.data.title as string; + const description = (page.data.description as string) || undefined; + + return buildMetadata({ + title, + description: description ?? '', + path: slug ? `/${lang}/docs/${slug.join('/')}` : `/${lang}/docs`, + lang, + ogType: 'article', + }); +} const mdxComponents = { ...defaultMdxComponents, @@ -44,10 +67,51 @@ export default async function Page({ toc: TOCItemType[]; }; + const title = page.data.title as string; + const description = (page.data.description as string) || ''; + const docPath = slug ? `/${lang}/docs/${slug.join('/')}` : `/${lang}/docs`; + + const breadcrumbJsonLd = { + '@context': 'https://schema.org', + '@type': 'BreadcrumbList', + itemListElement: [ + { '@type': 'ListItem', position: 1, name: 'Home', item: SITE_URL }, + { '@type': 'ListItem', position: 2, name: 'Docs', item: `${SITE_URL}/${lang}/docs` }, + ...(slug + ? slug.map((segment, i) => ({ + '@type': 'ListItem' as const, + position: i + 3, + name: segment.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()), + item: `${SITE_URL}/${lang}/docs/${slug.slice(0, i + 1).join('/')}`, + })) + : []), + ], + }; + + const articleJsonLd = { + '@context': 'https://schema.org', + '@type': 'TechArticle', + headline: title, + description, + inLanguage: lang === 'cn' ? 'zh-CN' : 'en', + isPartOf: { + '@type': 'WebSite', + url: SITE_URL, + }, + }; + return ( - {page.data.title} - {page.data.description} +