@@ -93,8 +93,59 @@ export default async function DocPage({ params }: Param) {
9393 getDocContributorsByDocId ( docIdFromPage ) ;
9494 const Mdx = page . data . body ;
9595
96+ // SEO 结构化数据
97+ const siteUrl =
98+ process . env . NEXT_PUBLIC_SITE_URL || "https://involutionhell.com" ;
99+ const slugPath = ( slug ?? [ ] ) . join ( "/" ) ;
100+ const docUrl = slugPath ? `${ siteUrl } /docs/${ slugPath } ` : `${ siteUrl } /docs` ;
101+
102+ // TechArticle: 让 docs 在 Google 搜索结果上更可能展示为技术文章卡片
103+ const articleJsonLd = {
104+ "@context" : "https://schema.org" ,
105+ "@type" : "TechArticle" ,
106+ headline : page . data . title ,
107+ description : page . data . description ,
108+ url : docUrl ,
109+ inLanguage : locale === "en" ? "en-US" : "zh-CN" ,
110+ publisher : {
111+ "@type" : "Organization" ,
112+ name : "Involution Hell" ,
113+ url : siteUrl ,
114+ } ,
115+ } ;
116+
117+ // BreadcrumbList: 按 slug 层级生成面包屑(Google 搜索结果里的那种层级链接)
118+ const breadcrumbItems = [
119+ { name : "Involution Hell" , url : siteUrl } ,
120+ { name : "Docs" , url : `${ siteUrl } /docs` } ,
121+ ...( slug ?? [ ] ) . map ( ( seg , idx ) => ( {
122+ name : decodeURIComponent ( seg ) ,
123+ url : `${ siteUrl } /docs/${ slug ! . slice ( 0 , idx + 1 ) . join ( "/" ) } ` ,
124+ } ) ) ,
125+ ] ;
126+ const breadcrumbJsonLd = {
127+ "@context" : "https://schema.org" ,
128+ "@type" : "BreadcrumbList" ,
129+ itemListElement : breadcrumbItems . map ( ( item , idx ) => ( {
130+ "@type" : "ListItem" ,
131+ position : idx + 1 ,
132+ name : item . name ,
133+ item : item . url ,
134+ } ) ) ,
135+ } ;
136+
96137 return (
97138 < >
139+ < script
140+ type = "application/ld+json"
141+ // eslint-disable-next-line react/no-danger
142+ dangerouslySetInnerHTML = { { __html : JSON . stringify ( articleJsonLd ) } }
143+ />
144+ < script
145+ type = "application/ld+json"
146+ // eslint-disable-next-line react/no-danger
147+ dangerouslySetInnerHTML = { { __html : JSON . stringify ( breadcrumbJsonLd ) } }
148+ />
98149 < DocsPage toc = { page . data . toc } >
99150 < DocsBody >
100151 < div className = "mb-6 flex flex-col gap-3 border-b border-border pb-6 md:mb-8 md:flex-row md:items-start md:justify-between" >
@@ -144,8 +195,35 @@ export async function generateMetadata({ params }: Param): Promise<Metadata> {
144195 notFound ( ) ;
145196 }
146197
198+ // 规范化 slug → canonical 路径。用户访问 /docs/ai/rl(原文)或 /docs/ai/rl.en(翻译版)
199+ // 都统一指向原始 slug,避免两个 URL 竞争同一份内容的 PageRank。
200+ const slugPath = ( slug ?? [ ] ) . join ( "/" ) ;
201+ const canonical = slugPath ? `/docs/${ slugPath } ` : "/docs" ;
202+
203+ // hreflang:告诉搜索引擎该文档有哪些语言版本。
204+ // 翻译版文件命名是 `<slug>.en.mdx` / `<slug>.zh.mdx`,URL 靠 cookie 切换,
205+ // 两种语言走同一 canonical URL,因此 hreflang 都指向自己。
206+ const languages : Record < string , string > = {
207+ "zh-CN" : canonical ,
208+ "en-US" : canonical ,
209+ "x-default" : canonical ,
210+ } ;
211+
147212 return {
148213 title : page . data . title ,
149214 description : page . data . description ,
215+ alternates : { canonical, languages } ,
216+ openGraph : {
217+ type : "article" ,
218+ title : page . data . title ,
219+ description : page . data . description ,
220+ url : canonical ,
221+ locale : locale === "en" ? "en_US" : "zh_CN" ,
222+ } ,
223+ twitter : {
224+ card : "summary_large_image" ,
225+ title : page . data . title ,
226+ description : page . data . description ,
227+ } ,
150228 } ;
151229}
0 commit comments