11import { defineConfig , type HeadConfig } from 'vitepress'
22import { full as emoji } from 'markdown-it-emoji'
33import { 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
58const 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 ( / ^ h t t p s ? : \/ \/ / . 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 ( / \. ( d e | f r | i t | e n ) \. m d $ / ) ?. [ 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 ( / i n d e x \. m d $ / , '' )
49+ . replace ( / \. m d $ / , '.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 ( / \. h t m l $ / , '.md' )
57+ }
58+
59+ function getAlternatePages ( relativePath : string ) {
60+ const baseRel = relativePath
61+ . replace ( / \. m d $ / , '' )
62+ . replace ( / \. ( d e | f r | i t | e n ) $ / , '' )
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
879export 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 ( / i n d e x \. m d $ / , '' )
43- . replace ( / \. m d $ / , '.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