@@ -24,6 +24,19 @@ const ogImageUrl = `${siteUrl}/images/og-image-large.png`;
2424const githubUrl = 'https://github.com/yamadashy/repomix' ;
2525const npmUrl = 'https://www.npmjs.com/package/repomix' ;
2626
27+ // Stable @id for the global WebSite node so per-page schemas (e.g. TechArticle.isPartOf)
28+ // can reference it via `@id` instead of inlining a fresh node, which Google treats
29+ // as a separate entity.
30+ const websiteId = `${ siteUrl } #website` ;
31+
32+ // Shared author block used by both the global SoftwareApplication JSON-LD and the
33+ // per-page TechArticle JSON-LD.
34+ const siteAuthor = {
35+ '@type' : 'Person' as const ,
36+ name : 'Kazuki Yamada' ,
37+ url : 'https://github.com/yamadashy' ,
38+ } ;
39+
2740const googleAnalyticsTag = 'G-7PTT4PLC69' ;
2841
2942type PageHeadContext = {
@@ -37,64 +50,31 @@ type PageHeadContext = {
3750
3851// Order matters here: `en/...` is rewritten to the site root, so English
3952// content emits canonical URLs without a locale prefix. Every other locale
40- // keeps its folder as the URL prefix.
41- const supportedLocales = [
42- 'en' ,
43- 'zh-cn' ,
44- 'zh-tw' ,
45- 'ja' ,
46- 'es' ,
47- 'pt-br' ,
48- 'ko' ,
49- 'de' ,
50- 'fr' ,
51- 'it' ,
52- 'hi' ,
53- 'id' ,
54- 'vi' ,
55- 'ru' ,
56- 'tr' ,
57- ] as const ;
58-
59- type Locale = ( typeof supportedLocales ) [ number ] ;
60-
61- // BCP-47 codes used in `hreflang` and `inLanguage` (Schema.org accepts BCP-47).
62- const localeToBcp47 : Record < Locale , string > = {
63- en : 'en' ,
64- 'zh-cn' : 'zh-CN' ,
65- 'zh-tw' : 'zh-TW' ,
66- ja : 'ja' ,
67- es : 'es' ,
68- 'pt-br' : 'pt-BR' ,
69- ko : 'ko' ,
70- de : 'de' ,
71- fr : 'fr' ,
72- it : 'it' ,
73- hi : 'hi' ,
74- id : 'id' ,
75- vi : 'vi' ,
76- ru : 'ru' ,
77- tr : 'tr' ,
78- } ;
79-
80- // `og:locale` uses underscore form, e.g. `en_US`, `pt_BR`.
81- const localeToOgLocale : Record < Locale , string > = {
82- en : 'en_US' ,
83- 'zh-cn' : 'zh_CN' ,
84- 'zh-tw' : 'zh_TW' ,
85- ja : 'ja_JP' ,
86- es : 'es_ES' ,
87- 'pt-br' : 'pt_BR' ,
88- ko : 'ko_KR' ,
89- de : 'de_DE' ,
90- fr : 'fr_FR' ,
91- it : 'it_IT' ,
92- hi : 'hi_IN' ,
93- id : 'id_ID' ,
94- vi : 'vi_VN' ,
95- ru : 'ru_RU' ,
96- tr : 'tr_TR' ,
97- } ;
53+ // keeps its folder as the URL prefix. Each entry carries its BCP-47 form
54+ // (used in `hreflang` and Schema.org `inLanguage`) and OpenGraph form
55+ // (underscore-separated, e.g. `en_US`). Keeping all three together prevents
56+ // drift when a new locale is added.
57+ export const localeConfig = {
58+ en : { bcp47 : 'en' , og : 'en_US' } ,
59+ 'zh-cn' : { bcp47 : 'zh-CN' , og : 'zh_CN' } ,
60+ 'zh-tw' : { bcp47 : 'zh-TW' , og : 'zh_TW' } ,
61+ ja : { bcp47 : 'ja' , og : 'ja_JP' } ,
62+ es : { bcp47 : 'es' , og : 'es_ES' } ,
63+ 'pt-br' : { bcp47 : 'pt-BR' , og : 'pt_BR' } ,
64+ ko : { bcp47 : 'ko' , og : 'ko_KR' } ,
65+ de : { bcp47 : 'de' , og : 'de_DE' } ,
66+ fr : { bcp47 : 'fr' , og : 'fr_FR' } ,
67+ it : { bcp47 : 'it' , og : 'it_IT' } ,
68+ hi : { bcp47 : 'hi' , og : 'hi_IN' } ,
69+ id : { bcp47 : 'id' , og : 'id_ID' } ,
70+ vi : { bcp47 : 'vi' , og : 'vi_VN' } ,
71+ ru : { bcp47 : 'ru' , og : 'ru_RU' } ,
72+ tr : { bcp47 : 'tr' , og : 'tr_TR' } ,
73+ } as const ;
74+
75+ export type Locale = keyof typeof localeConfig ;
76+
77+ const supportedLocales = Object . keys ( localeConfig ) as Locale [ ] ;
9878
9979const stripPageSuffix = ( rest : string ) =>
10080 rest
@@ -136,47 +116,49 @@ const createPageHead = ({ page, title, description, pageData }: PageHeadContext)
136116
137117 const tags : HeadConfig [ ] = [
138118 [ 'link' , { rel : 'canonical' , href : url } ] ,
119+ [ 'meta' , { property : 'og:type' , content : isHome ? 'website' : 'article' } ] ,
139120 [ 'meta' , { property : 'og:title' , content : title } ] ,
140121 [ 'meta' , { property : 'og:url' , content : url } ] ,
141122 [ 'meta' , { property : 'og:description' , content : description } ] ,
142- [ 'meta' , { property : 'og:locale' , content : localeToOgLocale [ locale ] } ] ,
123+ [ 'meta' , { property : 'og:locale' , content : localeConfig [ locale ] . og } ] ,
143124 [ 'meta' , { name : 'twitter:title' , content : title } ] ,
144125 [ 'meta' , { name : 'twitter:url' , content : url } ] ,
145126 [ 'meta' , { name : 'twitter:description' , content : description } ] ,
146127 ] ;
147128
148129 // hreflang alternates so search engines can surface the right localized
149- // page to each user. `x-default` falls back to English.
130+ // page to each user. `x-default` falls back to English. We also emit an
131+ // `og:locale:alternate` for each non-current locale for social previews
132+ // that honor it.
150133 for ( const alt of supportedLocales ) {
151134 tags . push ( [
152135 'link' ,
153136 {
154137 rel : 'alternate' ,
155- hreflang : localeToBcp47 [ alt ] ,
138+ hreflang : localeConfig [ alt ] . bcp47 ,
156139 href : buildLocaleUrl ( alt , rest ) ,
157140 } ,
158141 ] ) ;
142+ if ( alt !== locale ) {
143+ tags . push ( [ 'meta' , { property : 'og:locale:alternate' , content : localeConfig [ alt ] . og } ] ) ;
144+ }
159145 }
160146 tags . push ( [ 'link' , { rel : 'alternate' , hreflang : 'x-default' , href : buildLocaleUrl ( 'en' , rest ) } ] ) ;
161147
162- // For documentation pages, emit a TechArticle JSON-LD pointing back to the
163- // global WebSite graph so AI/search surfaces can connect article content to
164- // the product entity.
148+ // For documentation pages, emit a TechArticle JSON-LD that points back to
149+ // the global WebSite node by `@id` so AI/search surfaces see a single
150+ // linked entity across pages instead of a fresh inline WebSite per page .
165151 if ( ! isHome ) {
166152 const articleJsonLd = {
167153 '@context' : 'https://schema.org' ,
168154 '@type' : 'TechArticle' ,
169155 headline : title ,
170156 description,
171- inLanguage : localeToBcp47 [ locale ] ,
172- isPartOf : { '@type ' : 'WebSite' , name : siteName , url : siteUrl } ,
157+ inLanguage : localeConfig [ locale ] . bcp47 ,
158+ isPartOf : { '@id ' : websiteId } ,
173159 mainEntityOfPage : { '@type' : 'WebPage' , '@id' : url } ,
174160 image : ogImageUrl ,
175- author : {
176- '@type' : 'Person' ,
177- name : 'Kazuki Yamada' ,
178- url : 'https://github.com/yamadashy' ,
179- } ,
161+ author : siteAuthor ,
180162 } ;
181163 tags . push ( [ 'script' , { type : 'application/ld+json' } , JSON . stringify ( articleJsonLd ) ] ) ;
182164 }
@@ -189,6 +171,7 @@ const jsonLd = {
189171 '@context' : 'https://schema.org' ,
190172 '@graph' : [
191173 {
174+ '@id' : websiteId ,
192175 '@type' : 'WebSite' ,
193176 name : siteName ,
194177 url : siteUrl ,
@@ -214,11 +197,7 @@ const jsonLd = {
214197 softwareRequirements : 'Node.js 22.0.0 or higher' ,
215198 image : `${ siteUrl } /images/repomix-logo.svg` ,
216199 screenshot : ogImageUrl ,
217- author : {
218- '@type' : 'Person' ,
219- name : 'Kazuki Yamada' ,
220- url : 'https://github.com/yamadashy' ,
221- } ,
200+ author : siteAuthor ,
222201 sameAs : [ githubUrl , npmUrl ] ,
223202 featureList : [
224203 'AI-optimized output formats (XML, Markdown, JSON, Plain Text)' ,
@@ -341,8 +320,8 @@ export const configShard = defineConfig({
341320 [ 'link' , { rel : 'preconnect' , href : 'https://challenges.cloudflare.com' , crossorigin : '' } ] ,
342321 [ 'link' , { rel : 'dns-prefetch' , href : 'https://challenges.cloudflare.com' } ] ,
343322
344- // OGP
345- [ 'meta' , { property : 'og:type' , content : 'website' } ] ,
323+ // OGP. `og:type` is emitted per-page from `createPageHead` (article for
324+ // docs, website for the home page) so we do not duplicate it here.
346325 [ 'meta' , { property : 'og:site_name' , content : siteName } ] ,
347326 [ 'meta' , { property : 'og:image' , content : ogImageUrl } ] ,
348327 [ 'meta' , { name : 'twitter:card' , content : 'summary_large_image' } ] ,
0 commit comments