Skip to content

Commit b363c23

Browse files
yamadashyclaude
andcommitted
fix(website): Address PR review feedback for SEO config
- Consolidate `supportedLocales`, `localeToBcp47`, and `localeToOgLocale` into a single `localeConfig` map and export it along with the `Locale` type so the locale list lives in one place and can be reused by the main VitePress config later. - Extract the duplicated author block into a shared `siteAuthor` constant referenced by both the global SoftwareApplication graph and the per-page TechArticle. - Give the global `WebSite` node a stable `@id` and reference it from `TechArticle.isPartOf` so search engines see a single linked entity across pages instead of inlined duplicates. - Emit per-page `og:type` (`article` for docs, `website` for the home page) and drop the global `og:type` so the OpenGraph type matches the TechArticle schema. - Add `og:locale:alternate` for every non-current locale alongside the existing `hreflang` alternates so social previews can also route to the matching localized page. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent a7ced84 commit b363c23

1 file changed

Lines changed: 57 additions & 78 deletions

File tree

website/client/.vitepress/config/configShard.ts

Lines changed: 57 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,19 @@ const ogImageUrl = `${siteUrl}/images/og-image-large.png`;
2424
const githubUrl = 'https://github.com/yamadashy/repomix';
2525
const 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+
2740
const googleAnalyticsTag = 'G-7PTT4PLC69';
2841

2942
type 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

9979
const 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

Comments
 (0)