Skip to content

Commit e60df75

Browse files
committed
Improve SEO metadata for release note posts
1 parent 48ae78c commit e60df75

1 file changed

Lines changed: 191 additions & 10 deletions

File tree

docs/.vitepress/config.mts

Lines changed: 191 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,79 @@
11
import { defineConfig, type HeadConfig } from 'vitepress'
22
import { full as emoji } from 'markdown-it-emoji'
33
import { withMermaid } from 'vitepress-plugin-mermaid'
4+
import { existsSync } from 'node:fs'
5+
import { dirname, resolve } from 'node:path'
6+
import { fileURLToPath } from 'node:url'
47

58
const SITE_URL = 'https://docs.mobileid.ch'
9+
const DOCS_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
10+
const RELEASE_NOTES_POST_LAYOUT = 'release-notes-post'
11+
const SUPPORTED_LANGS = ['en', 'de', 'fr', 'it'] as const
12+
13+
type SupportedLang = typeof SUPPORTED_LANGS[number]
14+
15+
const HREFLANG_BY_LANG: Record<SupportedLang, string> = {
16+
en: 'en',
17+
de: 'de-CH',
18+
fr: 'fr-CH',
19+
it: 'it-CH',
20+
}
21+
22+
const OG_LOCALE_BY_LANG: Record<SupportedLang, string> = {
23+
en: 'en_US',
24+
de: 'de_CH',
25+
fr: 'fr_CH',
26+
it: 'it_CH',
27+
}
28+
29+
function toAbsoluteUrl(url: string): string {
30+
if (/^https?:\/\//.test(url)) return url
31+
return `${SITE_URL}${url.startsWith('/') ? '' : '/'}${url}`
32+
}
33+
34+
function getPageLang(pageData: { relativePath: string; frontmatter: Record<string, any> }): SupportedLang {
35+
const frontmatterLang = pageData.frontmatter.lang
36+
if (SUPPORTED_LANGS.includes(frontmatterLang)) return frontmatterLang
37+
38+
const langFromPath = pageData.relativePath.match(/\.(de|fr|it|en)\.md$/)?.[1]
39+
if (langFromPath && SUPPORTED_LANGS.includes(langFromPath as SupportedLang)) {
40+
return langFromPath as SupportedLang
41+
}
42+
43+
return 'en'
44+
}
45+
46+
function getPagePath(relativePath: string): string {
47+
return relativePath
48+
.replace(/index\.md$/, '')
49+
.replace(/\.md$/, '.html')
50+
}
51+
52+
function getSourceRelativePath(url: string): string {
53+
const normalizedUrl = url.replace(/^\//, '')
54+
if (!normalizedUrl) return 'index.md'
55+
if (normalizedUrl.endsWith('/')) return `${normalizedUrl}index.md`
56+
return normalizedUrl.replace(/\.html$/, '.md')
57+
}
58+
59+
function getAlternatePages(relativePath: string) {
60+
const baseRel = relativePath
61+
.replace(/\.md$/, '')
62+
.replace(/\.(de|fr|it|en)$/, '')
63+
64+
return SUPPORTED_LANGS.flatMap((lang) => {
65+
const candidateRel = lang === 'en' ? `${baseRel}.md` : `${baseRel}.${lang}.md`
66+
if (!existsSync(resolve(DOCS_ROOT, candidateRel))) return []
67+
68+
return [{
69+
lang,
70+
hreflang: HREFLANG_BY_LANG[lang],
71+
ogLocale: OG_LOCALE_BY_LANG[lang],
72+
path: `/${getPagePath(candidateRel)}`,
73+
url: toAbsoluteUrl(`/${getPagePath(candidateRel)}`),
74+
}]
75+
})
76+
}
677

778
// https://vitepress.dev/reference/site-config
879
export default withMermaid(defineConfig({
@@ -16,6 +87,32 @@ export default withMermaid(defineConfig({
1687

1788
sitemap: {
1889
hostname: SITE_URL,
90+
transformItems(items) {
91+
return items
92+
.filter((item) => !String(item.url).startsWith('superpowers/'))
93+
.map((item) => {
94+
const alternates = getAlternatePages(getSourceRelativePath(String(item.url)))
95+
if (alternates.length < 2) return item
96+
97+
const xDefault = alternates.find((alternate) => alternate.lang === 'en')
98+
99+
return {
100+
...item,
101+
links: [
102+
...alternates.map((alternate) => ({
103+
lang: alternate.hreflang,
104+
url: alternate.path,
105+
})),
106+
...(xDefault
107+
? [{
108+
lang: 'x-default',
109+
url: xDefault.path,
110+
}]
111+
: []),
112+
],
113+
}
114+
})
115+
},
19116
},
20117

21118
markdown: {
@@ -30,29 +127,113 @@ export default withMermaid(defineConfig({
30127
['link', { rel: 'shortcut icon', href: '/favicon.ico' }],
31128
['link', { rel: 'apple-touch-icon', sizes: '180x180', href: '/apple-touch-icon.png' }],
32129
['link', { rel: 'manifest', href: '/site.webmanifest' }],
33-
['meta', { name: 'robots', content: 'index, follow' }],
130+
['meta', { name: 'robots', content: 'index, follow, max-image-preview:large' }],
34131
['meta', { property: 'og:site_name', content: 'Mobile ID docs' }],
35-
['meta', { property: 'og:type', content: 'website' }],
36-
['meta', { property: 'og:locale', content: 'en' }],
37132
['meta', { name: 'twitter:card', content: 'summary' }],
38133
],
39134

40135
transformPageData(pageData) {
41-
const pagePath = pageData.relativePath
42-
.replace(/index\.md$/, '')
43-
.replace(/\.md$/, '.html')
136+
const pagePath = getPagePath(pageData.relativePath)
44137
const canonicalUrl = `${SITE_URL}/${pagePath}`
138+
const lang = getPageLang(pageData)
139+
const alternates = getAlternatePages(pageData.relativePath)
45140

46141
const ogTitle = pageData.title || 'Mobile ID docs'
47142
const ogDesc = pageData.description || 'Technical documentation for Mobile ID integration'
48-
49-
pageData.frontmatter.head ??= []
50-
pageData.frontmatter.head.push(
143+
const socialImage = pageData.frontmatter.thumbnail
144+
? toAbsoluteUrl(pageData.frontmatter.thumbnail)
145+
: undefined
146+
const isReleaseNotesPost = pageData.frontmatter.layout === RELEASE_NOTES_POST_LAYOUT
147+
const head: HeadConfig[] = [
51148
['link', { rel: 'canonical', href: canonicalUrl }],
52149
['meta', { property: 'og:title', content: ogTitle }],
53150
['meta', { property: 'og:description', content: ogDesc }],
54151
['meta', { property: 'og:url', content: canonicalUrl }],
55-
)
152+
['meta', { property: 'og:type', content: isReleaseNotesPost ? 'article' : 'website' }],
153+
['meta', { property: 'og:locale', content: OG_LOCALE_BY_LANG[lang] }],
154+
]
155+
156+
if (socialImage) {
157+
head.push(
158+
['meta', { property: 'og:image', content: socialImage }],
159+
['meta', { property: 'og:image:alt', content: ogTitle }],
160+
['meta', { name: 'twitter:card', content: 'summary_large_image' }],
161+
['meta', { name: 'twitter:image', content: socialImage }],
162+
)
163+
}
164+
165+
if (isReleaseNotesPost) {
166+
const publishedDate = pageData.frontmatter.date
167+
? new Date(pageData.frontmatter.date).toISOString()
168+
: undefined
169+
170+
head.push(
171+
['meta', { name: 'twitter:title', content: ogTitle }],
172+
['meta', { name: 'twitter:description', content: ogDesc }],
173+
)
174+
175+
if (publishedDate) {
176+
head.push(['meta', { property: 'article:published_time', content: publishedDate }])
177+
}
178+
179+
if (pageData.frontmatter.author) {
180+
head.push(['meta', { property: 'article:author', content: pageData.frontmatter.author }])
181+
}
182+
183+
head.push(['meta', { property: 'article:section', content: 'Release Notes' }])
184+
185+
if (alternates.length > 1) {
186+
const currentAlternate = alternates.find((alternate) => alternate.lang === lang)
187+
const xDefault = alternates.find((alternate) => alternate.lang === 'en') ?? currentAlternate
188+
189+
for (const alternate of alternates) {
190+
head.push(['link', { rel: 'alternate', hreflang: alternate.hreflang, href: alternate.url }])
191+
}
192+
193+
if (xDefault) {
194+
head.push(['link', { rel: 'alternate', hreflang: 'x-default', href: xDefault.url }])
195+
}
196+
197+
for (const alternate of alternates) {
198+
if (alternate.lang !== lang) {
199+
head.push(['meta', { property: 'og:locale:alternate', content: alternate.ogLocale }])
200+
}
201+
}
202+
}
203+
204+
const articleSchema = {
205+
'@context': 'https://schema.org',
206+
'@type': 'BlogPosting',
207+
headline: ogTitle,
208+
description: ogDesc,
209+
url: canonicalUrl,
210+
mainEntityOfPage: canonicalUrl,
211+
datePublished: publishedDate,
212+
inLanguage: HREFLANG_BY_LANG[lang],
213+
articleSection: 'Release Notes',
214+
image: socialImage ? [socialImage] : undefined,
215+
author: pageData.frontmatter.author
216+
? {
217+
'@type': 'Organization',
218+
name: pageData.frontmatter.author,
219+
}
220+
: undefined,
221+
publisher: {
222+
'@type': 'Organization',
223+
name: 'Swisscom',
224+
url: 'https://www.swisscom.ch/mobileid',
225+
},
226+
}
227+
228+
head.push([
229+
'script',
230+
{ type: 'application/ld+json' },
231+
JSON.stringify(articleSchema),
232+
])
233+
}
234+
235+
pageData.frontmatter.head ??= []
236+
pageData.frontmatter.head.push(...head)
56237
},
57238

58239
themeConfig: {

0 commit comments

Comments
 (0)