retypeset-odyssey 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +168 -0
- package/astro.config.ts +18 -0
- package/default-config.yaml +136 -0
- package/discover-collections.ts +160 -0
- package/integration.ts +394 -0
- package/package.json +105 -0
- package/patches/@qwik.dev__partytown@0.11.2.patch +98 -0
- package/public/_redirects +819 -0
- package/public/feeds/atom-style.xsl +105 -0
- package/public/feeds/rss-style.xsl +105 -0
- package/public/fonts/EarlySummer-VF-Split/00785494587e3487ac63a0e7e4fa30f0.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/08e5d941a4c76fad7b68e7a937ebb21f.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/1268e5072156188d601f1eeb4473655d.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/12a385475353c815d7a5add53ee51e37.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/12b11ca08223c65a21fc731d59dcfc11.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/16d6676d3cb645c520ee6df8a1f89afd.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/2912a75ffef95e7a5ae9e2b2311ad61d.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/298d96ea561e419a4104bc9fc18499ce.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/2a2c71acc17ec39f6780835899e53096.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/2a7e2d0e59d3f638074c50fab39fdef1.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/36931fc4370e1670ed76af5d3feccba2.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/3a68fdc792e4a9e0399a04e32d0cc2e3.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/4054d6a4d6b37719b51e0f71da6e7cd9.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/429cb25f825c3cbde6bfac5b36ae9675.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/42a9efc11298368ecdc1b85ab46f0b4f.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/432018d2bdc9df92a7662056eb2b1261.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/44a6fb782f2a01560faa0f95248b60ef.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/45367b060e8ba0aa2507e6b91b86620b.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/571db7564bda7c1a93542881b8976f4b.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/58d55eeef4cf455e86a1142b1f3110d3.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/59ea41e77309160a0f63cdc76a010202.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/5d19d9174e568db4755981aa2e4ab380.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/5e811eb3b4175ee93d7ec000bf4631c2.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/6268e0cd5d66d6fe05b331f259e7b9e4.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/6549844aa3d833ca06a68a8e839db465.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/714b459658a7321ceeb1e1386ce165c2.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/7511d97a469915013683eae06cb21cd9.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/7784b4ebe543d13f62f6f6e05beb0b2e.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/77c9bea70b3c6ab24e1497d5468c825b.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/789ebea9e81df623e930b86de98fbfab.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/885bb7ab0717e8a47fc17f953adcdbf1.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/896c58aff69a9a857764cee0663bc56d.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/8fb6fc01c59d1e3ad1910b58dec7f5e7.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/95be5462b91b9a0458797cdc89d94cb5.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/9a5b2724f983ca0fc0d5ff8d10c41396.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/9ffe17f9c0e4cc4356cb3f08ffdb9c6d.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/EarlySummer-VF-Subset.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/EarlySummerSerif License.txt +91 -0
- package/public/fonts/EarlySummer-VF-Split/a097ef49be62cd2565aca45600e1e3ac.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/a17a1ae6063088e5b3a48c06b816929a.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/a83fdcfc5ecf2f6996704b0c02758689.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/a8cf15ff9b71e59407d8406866ff6f99.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/af530ed51dd519e4456f8a5e259e908b.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/b195a8924915deec4aa9c3ec777cc93f.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/b4b6bb5df9239dd67b52ca858fd2a506.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/b7592e1e027923f19e0e55dfdac69668.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/b965859f69d8ccceaf0e2d6292afbcfb.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/bbe9333f1ff242bd96ecb23ff9e723b1.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/be758580e295339ea98f0240b9869f24.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/c07099e1d025617f6d40966986e1941b.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/c1b593dda62fdeb7dde3af02016da282.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/c89f0335910a68a0958f2846108370e8.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/ca49aa409fdedd3f2f894cd20a16640a.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/ccd4a28d2f63797e0183c87792e20b75.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/d2718da923fce8e7ea229d65e306e92c.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/d893e9b307d96041e9cfcbd03761b9f4.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/dafaedaee41b75e21479d4ff324b6a34.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/db392af65f1867e5fd580eed2195df99.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/dc7c73a9e5577143ccd11e05ab55cb39.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/de396881189f747eba67685298363242.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/df625b213228bba22a7733d4eff8f148.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/e6e60b384f220b893ef31a926ece829a.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/e6e8ce2c5972ab665630bb705383d0fb.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/e963c7ed7104c2d6d68fcb5f952fe2f5.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/e966b23b4cd7783f43e31032d41784f4.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/edaac57c3856ec13128f4c6c3e00975c.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/ee54e0d86edf068c6c9cbddb76a856fe.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/f612c78a5544ff2dd3e8296ac3e58344.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/f9e539bd9b7bf999c3da82f5403ec3b6.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/fa5863b923ac15993c52a619f699ee63.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/fc759e56ec6f6e6d3d4cb163d62fb557.woff2 +0 -0
- package/public/fonts/Font Subset List/CJK Common Characters.txt +7534 -0
- package/public/fonts/Font Subset List/EarlySummer Subset.txt +3 -0
- package/public/fonts/Font Subset List/Japanese Kana + Korean Letters.txt +6123 -0
- package/public/fonts/Font Subset List/Latin + Cyrillic + Greek + Arabic Glyphs.txt +121 -0
- package/public/fonts/Font Subset List/unicode_range.py +49 -0
- package/public/fonts/NotoSansSC-Bold.otf +0 -0
- package/public/fonts/NotoSansSC-Regular.otf +0 -0
- package/public/fonts/STIX-Italic-VF.woff2 +0 -0
- package/public/fonts/STIX-VF.woff2 +0 -0
- package/public/fonts/Snell-Black-SF.woff2 +0 -0
- package/public/fonts/Snell-Bold-SF.woff2 +0 -0
- package/public/giscus/theme-dark.css +208 -0
- package/public/giscus/theme-light.css +208 -0
- package/public/icons/favicon.svg +4 -0
- package/public/icons/og-logo.png +0 -0
- package/public/robots.txt +4 -0
- package/public/sounds/tap_01.wav +0 -0
- package/public/sounds/tap_02.wav +0 -0
- package/public/sounds/tap_03.wav +0 -0
- package/public/sounds/tap_04.wav +0 -0
- package/public/sounds/tap_05.wav +0 -0
- package/public/sounds/type_01.wav +0 -0
- package/public/sounds/type_02.wav +0 -0
- package/public/sounds/type_03.wav +0 -0
- package/public/sounds/type_04.wav +0 -0
- package/public/sounds/type_05.wav +0 -0
- package/scripts/apply-lqip.ts +276 -0
- package/scripts/format-posts.ts +105 -0
- package/scripts/migration/README.md +52 -0
- package/scripts/migration/migrate-hexo.ts +185 -0
- package/scripts/migration/validate-abbrlinks.ts +161 -0
- package/scripts/new-post.ts +52 -0
- package/scripts/seo/generate-legacy-redirects.ts +407 -0
- package/scripts/update-theme.ts +46 -0
- package/src/assets/icons/copy-check.svg +3 -0
- package/src/assets/icons/copy-icon.svg +4 -0
- package/src/assets/icons/go-back.svg +3 -0
- package/src/assets/icons/heading-anchor.svg +4 -0
- package/src/assets/icons/lang-en.svg +3 -0
- package/src/assets/icons/lang-ja.svg +5 -0
- package/src/assets/icons/lang-zh.svg +5 -0
- package/src/assets/icons/language-switcher.svg +3 -0
- package/src/assets/icons/pin-icon.svg +3 -0
- package/src/assets/icons/search-icon.svg +3 -0
- package/src/assets/icons/theme-toggle.svg +3 -0
- package/src/assets/icons/toc-icon.svg +3 -0
- package/src/assets/icons/top-icon.svg +3 -0
- package/src/assets/lqip-map.json +10 -0
- package/src/components/Button.astro +152 -0
- package/src/components/CategoryList.astro +66 -0
- package/src/components/Comment/Giscus.astro +119 -0
- package/src/components/Comment/Index.astro +30 -0
- package/src/components/Comment/Twikoo.astro +114 -0
- package/src/components/Comment/Waline.astro +149 -0
- package/src/components/FloatingButtons.astro +101 -0
- package/src/components/Footer.astro +74 -0
- package/src/components/Header.astro +62 -0
- package/src/components/JournalList.astro +56 -0
- package/src/components/Navbar.astro +69 -0
- package/src/components/NoteList.astro +56 -0
- package/src/components/Pagination.astro +267 -0
- package/src/components/PostDate.astro +80 -0
- package/src/components/PostList.astro +87 -0
- package/src/components/SearchModal.astro +340 -0
- package/src/components/TagList.astro +135 -0
- package/src/components/Widgets/BackButton.astro +43 -0
- package/src/components/Widgets/CodeCopyButton.astro +47 -0
- package/src/components/Widgets/GithubCard.astro +110 -0
- package/src/components/Widgets/ImageZoom.astro +135 -0
- package/src/components/Widgets/MediaEmbed.astro +127 -0
- package/src/components/Widgets/SoundEffect.astro +179 -0
- package/src/components/Widgets/TOC.astro +198 -0
- package/src/config-schema.ts +164 -0
- package/src/config.ts +127 -0
- package/src/config.ts.example +205 -0
- package/src/content/about/_example-about-en.md +6 -0
- package/src/content/about/about-en.md +21 -0
- package/src/content/about/about-ja.md +21 -0
- package/src/content/about/about-zh.md +24 -0
- package/src/content.config.ts +247 -0
- package/src/env.d.ts +25 -0
- package/src/i18n/config.ts +65 -0
- package/src/i18n/lang.ts +70 -0
- package/src/i18n/path.ts +160 -0
- package/src/i18n/ui.ts +214 -0
- package/src/layouts/Head.astro +203 -0
- package/src/layouts/Layout.astro +69 -0
- package/src/pages/404.astro +20 -0
- package/src/pages/[...lang]/[...page].astro +48 -0
- package/src/pages/[...lang]/about.astro +28 -0
- package/src/pages/[...lang]/atom.xml.ts +14 -0
- package/src/pages/[...lang]/categories/index.astro +35 -0
- package/src/pages/[...lang]/journals/[slug].astro +89 -0
- package/src/pages/[...lang]/journals/index.astro +55 -0
- package/src/pages/[...lang]/journals/page/[page].astro +66 -0
- package/src/pages/[...lang]/notes/[slug].astro +88 -0
- package/src/pages/[...lang]/notes/index.astro +55 -0
- package/src/pages/[...lang]/notes/page/[page].astro +66 -0
- package/src/pages/[...lang]/posts/[slug].astro +101 -0
- package/src/pages/[...lang]/rss.xml.ts +14 -0
- package/src/pages/[...lang]/search.astro +65 -0
- package/src/pages/[...lang]/tags/[tag].astro +53 -0
- package/src/pages/[...lang]/tags/index.astro +36 -0
- package/src/pages/_dynamic/list.astro +101 -0
- package/src/pages/_dynamic/slug.astro +100 -0
- package/src/pages/og/[...image].ts +114 -0
- package/src/pages/robots.txt.ts +20 -0
- package/src/plugins/rehype-code-copy-button.mjs +82 -0
- package/src/plugins/rehype-external-links.mjs +18 -0
- package/src/plugins/rehype-heading-anchor.mjs +55 -0
- package/src/plugins/rehype-image-processor.mjs +77 -0
- package/src/plugins/remark-container-directives.mjs +135 -0
- package/src/plugins/remark-leaf-directives.mjs +184 -0
- package/src/plugins/remark-reading-time.mjs +11 -0
- package/src/styles/comment.css +205 -0
- package/src/styles/extension.css +180 -0
- package/src/styles/font.css +111 -0
- package/src/styles/global.css +91 -0
- package/src/styles/lqip.css +71 -0
- package/src/styles/markdown.css +276 -0
- package/src/styles/transition.css +173 -0
- package/src/types/global.d.ts +22 -0
- package/src/types/index.d.ts +111 -0
- package/src/utils/cache.ts +32 -0
- package/src/utils/content.ts +819 -0
- package/src/utils/description.ts +147 -0
- package/src/utils/dynamic-collections.ts +155 -0
- package/src/utils/feed.ts +238 -0
- package/src/utils/page.ts +107 -0
- package/tsconfig.json +13 -0
- package/uno.config.ts +75 -0
package/src/i18n/lang.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { Language } from '@/i18n/config'
|
|
2
|
+
import { allLocales, base, defaultLocale, moreLocales } from '@/config'
|
|
3
|
+
import { langMap } from '@/i18n/config'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Get the short language code for the `[...lang]` route parameter
|
|
7
|
+
*
|
|
8
|
+
* @param lang Current language code (e.g. 'en')
|
|
9
|
+
* @returns Route parameter value (e.g. 'en') or undefined (root path '/')
|
|
10
|
+
*/
|
|
11
|
+
export function getLangRouteParam(lang: Language): string | undefined {
|
|
12
|
+
return lang === defaultLocale ? undefined : lang
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Get the corresponding short language code from the complete current locale value
|
|
17
|
+
*
|
|
18
|
+
* @param locale Current locale value (e.g. 'en-US')
|
|
19
|
+
* @returns Corresponding language code (e.g. 'en') or default locale
|
|
20
|
+
*/
|
|
21
|
+
export function getLangFromLocale(locale: string | undefined): Language {
|
|
22
|
+
if (!locale) {
|
|
23
|
+
return defaultLocale
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const match = Object.entries(langMap).find(([, codes]) =>
|
|
27
|
+
(codes as readonly string[]).includes(locale),
|
|
28
|
+
)
|
|
29
|
+
return (match?.[0] ?? defaultLocale) as Language
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Get the language code from the current path
|
|
34
|
+
*
|
|
35
|
+
* @param path Current page path
|
|
36
|
+
* @returns Language code detected from path or default locale
|
|
37
|
+
*/
|
|
38
|
+
export function getLangFromPath(path: string): Language {
|
|
39
|
+
const pathWithoutBase = base && path.startsWith(base)
|
|
40
|
+
? path.slice(base.length)
|
|
41
|
+
: path
|
|
42
|
+
|
|
43
|
+
const normalized = pathWithoutBase.replace(/^\/+/, '')
|
|
44
|
+
if (!normalized) {
|
|
45
|
+
return defaultLocale
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const [firstSegment] = normalized.split('/')
|
|
49
|
+
const maybeLang = firstSegment.replace(/\.html$/, '') as Language
|
|
50
|
+
|
|
51
|
+
return moreLocales.includes(maybeLang) ? maybeLang : defaultLocale
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Get the next language code in the global language cycle
|
|
56
|
+
*
|
|
57
|
+
* @param currentLang Current language code
|
|
58
|
+
* @returns Next language code in the global cycle
|
|
59
|
+
*/
|
|
60
|
+
export function getNextGlobalLang(currentLang: Language): Language {
|
|
61
|
+
// Get index of current language
|
|
62
|
+
const currentIndex = allLocales.indexOf(currentLang)
|
|
63
|
+
if (currentIndex === -1) {
|
|
64
|
+
return defaultLocale
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Calculate and return next language in cycle
|
|
68
|
+
const nextIndex = (currentIndex + 1) % allLocales.length
|
|
69
|
+
return allLocales[nextIndex]
|
|
70
|
+
}
|
package/src/i18n/path.ts
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import type { Language } from '@/i18n/config'
|
|
2
|
+
import { allLocales, base, defaultLocale } from '@/config'
|
|
3
|
+
import { getLangFromPath, getNextGlobalLang } from '@/i18n/lang'
|
|
4
|
+
|
|
5
|
+
function stripLeadingLang(path: string, lang: Language): string {
|
|
6
|
+
const normalized = path.replace(/^\/+/, '')
|
|
7
|
+
if (!normalized) {
|
|
8
|
+
return '/'
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const [first, ...rest] = normalized.split('/')
|
|
12
|
+
const firstNoHtml = first.replace(/\.html$/, '')
|
|
13
|
+
if (firstNoHtml !== lang) {
|
|
14
|
+
return path
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// /en , /en.html -> /
|
|
18
|
+
if (rest.length === 0) {
|
|
19
|
+
return '/'
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const restPath = `/${rest.join('/')}`
|
|
23
|
+
// Guard against bad states like /en/en.html
|
|
24
|
+
if (restPath === `/${lang}` || restPath === `/${lang}.html`) {
|
|
25
|
+
return '/'
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return restPath
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Get path to a specific tag page with language support
|
|
33
|
+
*/
|
|
34
|
+
export function getTagPath(tagName: string, lang: Language): string {
|
|
35
|
+
const encodedTag = encodeURIComponent(tagName)
|
|
36
|
+
const tagPath = lang === defaultLocale
|
|
37
|
+
? `/tags/${encodedTag}`
|
|
38
|
+
: `/${lang}/tags/${encodedTag}`
|
|
39
|
+
|
|
40
|
+
return base ? `${base}${tagPath}` : tagPath
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Get path to a specific post page with language and category support
|
|
45
|
+
*/
|
|
46
|
+
export function getPostPath(slug: string, lang: Language): string {
|
|
47
|
+
const postPath = lang === defaultLocale
|
|
48
|
+
? `/posts/${slug}`
|
|
49
|
+
: `/${lang}/posts/${slug}`
|
|
50
|
+
|
|
51
|
+
return base ? `${base}${postPath}` : postPath
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Get path to a specific note page with language support
|
|
56
|
+
*/
|
|
57
|
+
export function getNotePath(slug: string, lang: Language): string {
|
|
58
|
+
const notePath = lang === defaultLocale
|
|
59
|
+
? `/notes/${slug}`
|
|
60
|
+
: `/${lang}/notes/${slug}`
|
|
61
|
+
|
|
62
|
+
return base ? `${base}${notePath}` : notePath
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Get path to a specific journal page with language support
|
|
67
|
+
*/
|
|
68
|
+
export function getJournalPath(slug: string, lang: Language): string {
|
|
69
|
+
const journalPath = lang === defaultLocale
|
|
70
|
+
? `/journals/${slug}`
|
|
71
|
+
: `/${lang}/journals/${slug}`
|
|
72
|
+
|
|
73
|
+
return base ? `${base}${journalPath}` : journalPath
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Generate localized path based on current language
|
|
78
|
+
*
|
|
79
|
+
* @param path Path to localize
|
|
80
|
+
* @param currentLang Current language code
|
|
81
|
+
* @returns Localized path with language prefix
|
|
82
|
+
*/
|
|
83
|
+
export function getLocalizedPath(path: string, currentLang?: Language) {
|
|
84
|
+
// Astro 6 with build.format: 'file' emits dist/index.html and reports
|
|
85
|
+
// `Astro.url.pathname` as `/index.html` for the homepage, so we also
|
|
86
|
+
// need to recognise the stripped `index` form as "the homepage".
|
|
87
|
+
const normalizedPath = path
|
|
88
|
+
.replace(/^\/|\/$/g, '')
|
|
89
|
+
.replace(/\.html$/, '')
|
|
90
|
+
const isHomepage = normalizedPath === '' || normalizedPath === 'index'
|
|
91
|
+
const lang = currentLang ?? getLangFromPath(path)
|
|
92
|
+
|
|
93
|
+
const langPrefix = lang === defaultLocale ? '' : `/${lang}`
|
|
94
|
+
// Don't add trailing slash since trailingSlash is set to 'never'
|
|
95
|
+
const localizedPath = isHomepage
|
|
96
|
+
? `${langPrefix || '/'}`
|
|
97
|
+
: `${langPrefix}/${normalizedPath}`
|
|
98
|
+
|
|
99
|
+
return base ? `${base}${localizedPath}` : localizedPath
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Build path for next language
|
|
104
|
+
*
|
|
105
|
+
* @param currentPath Current page path
|
|
106
|
+
* @param currentLang Current language code
|
|
107
|
+
* @param nextLang Next language code to switch to
|
|
108
|
+
* @returns Path for next language
|
|
109
|
+
*/
|
|
110
|
+
export function getNextLangPath(currentPath: string, currentLang: Language, nextLang: Language): string {
|
|
111
|
+
const pathWithoutBase = base && currentPath.startsWith(base)
|
|
112
|
+
? currentPath.slice(base.length)
|
|
113
|
+
: currentPath
|
|
114
|
+
|
|
115
|
+
const pagePath = currentLang === defaultLocale
|
|
116
|
+
? pathWithoutBase
|
|
117
|
+
: stripLeadingLang(pathWithoutBase, currentLang)
|
|
118
|
+
|
|
119
|
+
return getLocalizedPath(pagePath, nextLang)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Get next language path from global language list
|
|
124
|
+
*
|
|
125
|
+
* @param currentPath Current page path
|
|
126
|
+
* @returns Path for next supported language
|
|
127
|
+
*/
|
|
128
|
+
export function getNextGlobalLangPath(currentPath: string): string {
|
|
129
|
+
const currentLang = getLangFromPath(currentPath)
|
|
130
|
+
const nextLang = getNextGlobalLang(currentLang)
|
|
131
|
+
return getNextLangPath(currentPath, currentLang, nextLang)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Get next language path from supported language list
|
|
136
|
+
*
|
|
137
|
+
* @param currentPath Current page path
|
|
138
|
+
* @param supportedLangs List of supported language codes
|
|
139
|
+
* @returns Path for next supported language
|
|
140
|
+
*/
|
|
141
|
+
export function getNextSupportedLangPath(currentPath: string, supportedLangs: Language[]): string {
|
|
142
|
+
if (supportedLangs.length === 0) {
|
|
143
|
+
return getNextGlobalLangPath(currentPath)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Sort supported languages by global priority
|
|
147
|
+
const langPriority = new Map<Language, number>(
|
|
148
|
+
allLocales.map((lang, index) => [lang, index]),
|
|
149
|
+
)
|
|
150
|
+
const sortedLangs = [...supportedLangs].sort(
|
|
151
|
+
(a, b) => (langPriority.get(a) ?? 0) - (langPriority.get(b) ?? 0),
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
// Get current language and next in cycle
|
|
155
|
+
const currentLang = getLangFromPath(currentPath)
|
|
156
|
+
const currentIndex = sortedLangs.indexOf(currentLang)
|
|
157
|
+
const nextLang = sortedLangs[(currentIndex + 1) % sortedLangs.length]
|
|
158
|
+
|
|
159
|
+
return getNextLangPath(currentPath, currentLang, nextLang)
|
|
160
|
+
}
|
package/src/i18n/ui.ts
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import type { Language } from '@/i18n/config'
|
|
2
|
+
|
|
3
|
+
interface Translation {
|
|
4
|
+
title: string
|
|
5
|
+
subtitle: string
|
|
6
|
+
description: string
|
|
7
|
+
posts: string
|
|
8
|
+
notes?: string
|
|
9
|
+
notesIntro?: string
|
|
10
|
+
notesEmpty?: string
|
|
11
|
+
journals?: string
|
|
12
|
+
journalsIntro?: string
|
|
13
|
+
journalsEmpty?: string
|
|
14
|
+
tags: string
|
|
15
|
+
about: string
|
|
16
|
+
search: string
|
|
17
|
+
searchPlaceholder: string
|
|
18
|
+
searchNoResults: string
|
|
19
|
+
searchResultsFound: string
|
|
20
|
+
toc: string
|
|
21
|
+
tech?: string
|
|
22
|
+
life?: string
|
|
23
|
+
science?: string
|
|
24
|
+
categories?: string
|
|
25
|
+
categoriesInTotal?: string
|
|
26
|
+
tagsInTotal?: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const ui: Record<Language, Translation> = {
|
|
30
|
+
'de': {
|
|
31
|
+
title: 'Neusatz',
|
|
32
|
+
subtitle: 'Die Schönheit der Typografie wiederbeleben',
|
|
33
|
+
description: 'Retypeset ist ein statisches Blog-Theme basierend auf dem Astro-Framework, auf Deutsch bekannt als "Neusatz". Dieses Theme, inspiriert von traditioneller Typografie, etabliert einen neuen visuellen Standard und gestaltet alle Seiten neu, um ein Leseerlebnis ähnlich dem gedruckter Bücher zu schaffen und die Schönheit des Satzes wiederzubeleben. Jedes Element ist bis ins kleinste Detail durchdacht, Eleganz zeigt sich auch im kleinsten Raum.',
|
|
34
|
+
posts: 'Beiträge',
|
|
35
|
+
tags: 'Schlagwörter',
|
|
36
|
+
about: 'Über',
|
|
37
|
+
search: 'Suche',
|
|
38
|
+
searchPlaceholder: 'Suchbegriff eingeben...',
|
|
39
|
+
searchNoResults: 'Keine Ergebnisse gefunden',
|
|
40
|
+
searchResultsFound: 'Ergebnisse gefunden',
|
|
41
|
+
toc: 'Inhaltsverzeichnis',
|
|
42
|
+
},
|
|
43
|
+
'en': {
|
|
44
|
+
title: 'Life Odyssey',
|
|
45
|
+
subtitle: 'A journey through life, technology, and reflections',
|
|
46
|
+
description: 'A personal blog about life, technology, and reflections.',
|
|
47
|
+
posts: 'Posts',
|
|
48
|
+
notes: 'Notes',
|
|
49
|
+
notesIntro: 'Personal study notes (often rough, incomplete, and subject to change).',
|
|
50
|
+
notesEmpty: 'No notes yet.',
|
|
51
|
+
journals: 'Journal',
|
|
52
|
+
journalsIntro: 'Personal journal entries (diary-like and unpolished).',
|
|
53
|
+
journalsEmpty: 'No journal entries yet.',
|
|
54
|
+
tags: 'Tags',
|
|
55
|
+
about: 'About',
|
|
56
|
+
search: 'Search',
|
|
57
|
+
searchPlaceholder: 'Type to search...',
|
|
58
|
+
searchNoResults: 'No results found',
|
|
59
|
+
searchResultsFound: 'results found',
|
|
60
|
+
toc: 'Table of Contents',
|
|
61
|
+
tech: 'Tech',
|
|
62
|
+
life: 'Life',
|
|
63
|
+
science: 'Science',
|
|
64
|
+
categories: 'Categories',
|
|
65
|
+
categoriesInTotal: 'categories in total',
|
|
66
|
+
tagsInTotal: 'tags in total',
|
|
67
|
+
},
|
|
68
|
+
'es': {
|
|
69
|
+
title: 'Retipografía',
|
|
70
|
+
subtitle: 'Reviviendo la belleza tipográfica',
|
|
71
|
+
description: 'Retypeset es un tema de blog estático basado en el framework Astro. Inspirado por Typography, Retypeset establece un nuevo estándar visual y reimagina el diseño de todas las páginas, creando una experiencia de lectura similar a la de los libros impresos, reviviendo la belleza de la tipografía. Detalles en cada mirada, elegancia en cada espacio.',
|
|
72
|
+
posts: 'Artículos',
|
|
73
|
+
tags: 'Etiquetas',
|
|
74
|
+
about: 'Sobre',
|
|
75
|
+
search: 'Buscar',
|
|
76
|
+
searchPlaceholder: 'Escribe para buscar...',
|
|
77
|
+
searchNoResults: 'No se encontraron resultados',
|
|
78
|
+
searchResultsFound: 'resultados encontrados',
|
|
79
|
+
toc: 'Índice',
|
|
80
|
+
},
|
|
81
|
+
'fr': {
|
|
82
|
+
title: 'Retypographie',
|
|
83
|
+
subtitle: 'Raviver la beauté de la typographie',
|
|
84
|
+
description: 'Retypeset est un thème de blog statique basé sur le framework Astro, connu en français sous le nom de "Retypographie". Ce thème, inspiré par la typographie traditionnelle, établit une nouvelle norme visuelle et réorganise toutes les pages pour créer une expérience de lecture semblable à celle des livres imprimés, ravivant ainsi la beauté de la mise en page. Chaque élément est soigné dans les moindres détails, l\'élégance se manifeste dans les plus petits espaces.',
|
|
85
|
+
posts: 'Articles',
|
|
86
|
+
tags: 'Étiquettes',
|
|
87
|
+
about: 'À propos',
|
|
88
|
+
search: 'Recherche',
|
|
89
|
+
searchPlaceholder: 'Tapez pour rechercher...',
|
|
90
|
+
searchNoResults: 'Aucun résultat trouvé',
|
|
91
|
+
searchResultsFound: 'résultats trouvés',
|
|
92
|
+
toc: 'Table des matières',
|
|
93
|
+
},
|
|
94
|
+
'ja': {
|
|
95
|
+
title: 'Life Odyssey',
|
|
96
|
+
subtitle: '人生、技術、そして思索の旅',
|
|
97
|
+
description: '人生、技術、思索についての個人ブログ。',
|
|
98
|
+
posts: '記事',
|
|
99
|
+
notes: 'ノート',
|
|
100
|
+
notesIntro: '学習・問題演習中の個人メモです。未整理・未完成の内容が含まれ、予告なく更新されます。',
|
|
101
|
+
notesEmpty: 'ノートはまだありません。',
|
|
102
|
+
journals: '日記',
|
|
103
|
+
journalsIntro: '日々の記録です(私的な内容を含み、未整理なことがあります)。',
|
|
104
|
+
journalsEmpty: '日記はまだありません。',
|
|
105
|
+
tags: 'タグ',
|
|
106
|
+
about: '概要',
|
|
107
|
+
search: '検索',
|
|
108
|
+
searchPlaceholder: '検索キーワードを入力...',
|
|
109
|
+
searchNoResults: '結果が見つかりません',
|
|
110
|
+
searchResultsFound: '件の結果',
|
|
111
|
+
toc: '目次',
|
|
112
|
+
tech: '技術',
|
|
113
|
+
life: '生活',
|
|
114
|
+
science: '科学',
|
|
115
|
+
categories: 'カテゴリー',
|
|
116
|
+
tagsInTotal: '個のタグ',
|
|
117
|
+
},
|
|
118
|
+
'ko': {
|
|
119
|
+
title: '재조판',
|
|
120
|
+
subtitle: '판형의 아름다움을 재현하다',
|
|
121
|
+
description: 'Retypeset은 Astro 프레임워크를 기반으로 한 정적 블로그 테마로, 한국어로는 "재조판"이라고 합니다. 이 테마는 활판 인쇄에서 디자인 영감을 얻어, 새로운 시각적 기준을 확립하고 모든 페이지를 재구성하여 종이책과 같은 독서 경험을 제공하며 판형의 아름다움을 되살립니다. 모든 것이 세부적인 디테일이며, 작은 공간에서도 우아함이 느껴집니다.',
|
|
122
|
+
posts: '게시물',
|
|
123
|
+
tags: '태그',
|
|
124
|
+
about: '소개',
|
|
125
|
+
search: '검색',
|
|
126
|
+
searchPlaceholder: '검색어를 입력하세요...',
|
|
127
|
+
searchNoResults: '결과를 찾을 수 없습니다',
|
|
128
|
+
searchResultsFound: '개의 결과',
|
|
129
|
+
toc: '목차',
|
|
130
|
+
},
|
|
131
|
+
'pl': {
|
|
132
|
+
title: 'Przeskład',
|
|
133
|
+
subtitle: 'Ożywiając piękno typografii',
|
|
134
|
+
description: 'Retypeset to statyczny motyw bloga oparty na frameworku Astro, w języku polskim znany jako "Przeskład". Ten motyw, inspirowany typografią drukarską, ustanawia nowy standard wizualny i reorganizuje wszystkie strony, tworząc doświadczenie czytelnicze przypominające papierowe książki, przywracając piękno układu tekstu. Każdy element jest dopracowany w szczegółach, elegancja zawarta w najmniejszej przestrzeni.',
|
|
135
|
+
posts: 'Artykuły',
|
|
136
|
+
tags: 'Tagi',
|
|
137
|
+
about: 'O stronie',
|
|
138
|
+
search: 'Szukaj',
|
|
139
|
+
searchPlaceholder: 'Wpisz, aby wyszukać...',
|
|
140
|
+
searchNoResults: 'Nie znaleziono wyników',
|
|
141
|
+
searchResultsFound: 'wyników',
|
|
142
|
+
toc: 'Spis treści',
|
|
143
|
+
},
|
|
144
|
+
'pt': {
|
|
145
|
+
title: 'Retipografia',
|
|
146
|
+
subtitle: 'Reviva a beleza da tipografia',
|
|
147
|
+
description: 'Retypeset é um tema de blog estático baseado no framework Astro. Inspirado pela tipografia, o Retypeset estabelece um novo padrão visual e reimagina o layout de todas as páginas, criando uma experiência de leitura reminiscente de livros físicos, revivendo a beleza da tipografia. Cada detalhe é visível, elegância em cada espaço.',
|
|
148
|
+
posts: 'Artigos',
|
|
149
|
+
tags: 'Tags',
|
|
150
|
+
about: 'Sobre',
|
|
151
|
+
search: 'Pesquisar',
|
|
152
|
+
searchPlaceholder: 'Digite para pesquisar...',
|
|
153
|
+
searchNoResults: 'Nenhum resultado encontrado',
|
|
154
|
+
searchResultsFound: 'resultados encontrados',
|
|
155
|
+
toc: 'Sumário',
|
|
156
|
+
},
|
|
157
|
+
'ru': {
|
|
158
|
+
title: 'Переверстка',
|
|
159
|
+
subtitle: 'Возрождая красоту типографики',
|
|
160
|
+
description: 'Retypeset — это статическая тема блога, основанная на фреймворке Astro. Вдохновленная Typography, Retypeset устанавливает новый визуальный стандарт и переосмысливает компоновку всех страниц, создавая опыт чтения, напоминающий печатные книги, возрождая красоту типографики. Детали в каждом взгляде, элегантность в каждом пространстве.',
|
|
161
|
+
posts: 'Посты',
|
|
162
|
+
tags: 'Теги',
|
|
163
|
+
about: 'О себе',
|
|
164
|
+
search: 'Поиск',
|
|
165
|
+
searchPlaceholder: 'Введите для поиска...',
|
|
166
|
+
searchNoResults: 'Результаты не найдены',
|
|
167
|
+
searchResultsFound: 'результатов',
|
|
168
|
+
toc: 'Оглавление',
|
|
169
|
+
},
|
|
170
|
+
'zh': {
|
|
171
|
+
title: 'Life Odyssey',
|
|
172
|
+
subtitle: '人生若只如初见',
|
|
173
|
+
description: '关于生活、技术与思考的个人博客。',
|
|
174
|
+
posts: '文章',
|
|
175
|
+
notes: '笔记',
|
|
176
|
+
notesIntro: '这里主要是我学习、刷题时的随手记录,偏自用,可能不完整或随时修改。',
|
|
177
|
+
notesEmpty: '还没有笔记。',
|
|
178
|
+
journals: '日记',
|
|
179
|
+
journalsIntro: 'this is my odyssey',
|
|
180
|
+
journalsEmpty: '还没有日记。',
|
|
181
|
+
tags: '标签',
|
|
182
|
+
about: '关于',
|
|
183
|
+
search: '搜索',
|
|
184
|
+
searchPlaceholder: '输入关键词搜索...',
|
|
185
|
+
searchNoResults: '没有找到结果',
|
|
186
|
+
searchResultsFound: '个结果',
|
|
187
|
+
toc: '目录',
|
|
188
|
+
tech: '技术',
|
|
189
|
+
life: '生活',
|
|
190
|
+
science: '科研',
|
|
191
|
+
categories: '分类',
|
|
192
|
+
categoriesInTotal: '个分类',
|
|
193
|
+
tagsInTotal: '个标签',
|
|
194
|
+
},
|
|
195
|
+
'zh-tw': {
|
|
196
|
+
title: '重新編排',
|
|
197
|
+
subtitle: '再現版式之美',
|
|
198
|
+
description: 'Retypeset是一款基於Astro框架的靜態部落格主題,中文名為重新編排。本主題以活版印字為設計靈感,通過建立全新的視覺規範,對所有頁面進行重新編排,打造紙質書頁般的閱讀體驗,再現版式之美。所見皆為細節,方寸盡顯優雅。',
|
|
199
|
+
posts: '文章',
|
|
200
|
+
tags: '標籤',
|
|
201
|
+
about: '關於',
|
|
202
|
+
search: '搜尋',
|
|
203
|
+
searchPlaceholder: '輸入關鍵字搜尋...',
|
|
204
|
+
searchNoResults: '沒有找到結果',
|
|
205
|
+
searchResultsFound: '個結果',
|
|
206
|
+
toc: '目錄',
|
|
207
|
+
tech: '技術',
|
|
208
|
+
life: '生活',
|
|
209
|
+
science: '科研',
|
|
210
|
+
categories: '分類',
|
|
211
|
+
categoriesInTotal: '個分類',
|
|
212
|
+
tagsInTotal: '個標籤',
|
|
213
|
+
},
|
|
214
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { ClientRouter } from 'astro:transitions'
|
|
3
|
+
import katexCSS from 'katex/dist/katex.min.css?url'
|
|
4
|
+
import { allLocales, base, defaultLocale, themeConfig } from '@/config'
|
|
5
|
+
import { ui } from '@/i18n/ui'
|
|
6
|
+
import { getPageInfo } from '@/utils/page'
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
postTitle?: string
|
|
10
|
+
postDescription?: string
|
|
11
|
+
postSlug?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Props and Language
|
|
15
|
+
const { postTitle, postDescription, postSlug } = Astro.props
|
|
16
|
+
const { currentLang } = getPageInfo(Astro.url.pathname)
|
|
17
|
+
const currentUI = ui[currentLang as keyof typeof ui] ?? {}
|
|
18
|
+
const lang = currentLang === defaultLocale ? '' : `${currentLang}/`
|
|
19
|
+
|
|
20
|
+
// Site Configuration
|
|
21
|
+
const { title, subtitle, description, i18nTitle, author, favicon } = themeConfig.site
|
|
22
|
+
const { mode: defaultMode, light: { background: lightMode }, dark: { background: darkMode } } = themeConfig.color
|
|
23
|
+
const { katex: katexEnabled, reduceMotion } = themeConfig.global
|
|
24
|
+
const { verification = {}, twitterID = '', googleAnalyticsID = '', umamiAnalyticsID = '', apiflashKey = '' } = themeConfig.seo ?? {}
|
|
25
|
+
const { google = '', bing = '', yandex = '', baidu = '' } = verification
|
|
26
|
+
const { customGoogleAnalyticsJS = '', customUmamiAnalyticsJS = '' } = themeConfig.preload ?? {}
|
|
27
|
+
|
|
28
|
+
// Site Metadata
|
|
29
|
+
const metaTheme = defaultMode === 'dark' ? darkMode : lightMode
|
|
30
|
+
const siteTitle = i18nTitle ? currentUI.title : title
|
|
31
|
+
const siteSubtitle = i18nTitle ? currentUI.subtitle : subtitle
|
|
32
|
+
const siteDescription = i18nTitle ? currentUI.description : description
|
|
33
|
+
|
|
34
|
+
// Page Metadata
|
|
35
|
+
const pageTitle = postTitle ? `${postTitle} | ${siteTitle}` : `${siteTitle} - ${siteSubtitle}`
|
|
36
|
+
const pageDescription = postDescription || siteDescription
|
|
37
|
+
const pageImage = postSlug
|
|
38
|
+
? new URL(`${base}/og/${postSlug}.png`, Astro.url.origin)
|
|
39
|
+
: apiflashKey
|
|
40
|
+
? `https://api.apiflash.com/v1/urltoimage?access_key=${apiflashKey}&url=${Astro.url}&format=png&width=1500&height=788&ttl=259200&wait_until=network_idle&no_tracking=true`
|
|
41
|
+
: `https://api.apiflash.com/v1/urltoimage?access_key=02a837b6188f4ba0a7fd9fbeff03a83e&url=https://retypeset.radishzz.cc/${lang}&format=png&width=1500&height=788&ttl=604800&wait_until=network_idle&no_tracking=true`
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
<head>
|
|
45
|
+
<!-- Basic Info -->
|
|
46
|
+
<meta charset="utf-8" />
|
|
47
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
|
48
|
+
{favicon.toLowerCase().endsWith('.svg') && <link rel="icon" type="image/svg+xml" href={`${base}${favicon}`} />}
|
|
49
|
+
{favicon.toLowerCase().endsWith('.png') && <link rel="icon" type="image/png" href={`${base}${favicon}`} />}
|
|
50
|
+
{favicon.toLowerCase().endsWith('.ico') && <link rel="icon" type="image/x-icon" href={`${base}${favicon}`} />}
|
|
51
|
+
<title>{pageTitle}</title>
|
|
52
|
+
<meta name="description" content={pageDescription} />
|
|
53
|
+
<meta name="author" content={author} />
|
|
54
|
+
<meta name="generator" content={Astro.generator} />
|
|
55
|
+
<meta name="theme-color" content={metaTheme} />
|
|
56
|
+
|
|
57
|
+
<!-- Preload -->
|
|
58
|
+
<link rel="preload" href={`${base}/fonts/EarlySummer-VF-Split/EarlySummer-VF-Subset.woff2`} as="font" type="font/woff2" crossorigin />
|
|
59
|
+
<link rel="preload" href={`${base}/fonts/Snell-Black-SF.woff2`} as="font" type="font/woff2" crossorigin />
|
|
60
|
+
<link rel="preload" href={`${base}/fonts/Snell-Bold-SF.woff2`} as="font" type="font/woff2" crossorigin />
|
|
61
|
+
<link rel="preload" href={`${base}/fonts/STIX-VF.woff2`} as="font" type="font/woff2" crossorigin />
|
|
62
|
+
<link rel="preload" href={`${base}/fonts/STIX-Italic-VF.woff2`} as="font" type="font/woff2" crossorigin />
|
|
63
|
+
{katexEnabled && <link rel="stylesheet" href={katexCSS} media="print" onload={`this.media='all'`} />}
|
|
64
|
+
|
|
65
|
+
<!-- Site Links -->
|
|
66
|
+
<link rel="canonical" href={Astro.url} />
|
|
67
|
+
<link rel="sitemap" href={`${base}/sitemap-index.xml`} />
|
|
68
|
+
<link rel="alternate" href={`${base}/rss.xml`} type="application/rss+xml" title="RSS Feed" />
|
|
69
|
+
<link rel="alternate" href={`${base}/atom.xml`} type="application/atom+xml" title="Atom Feed" />
|
|
70
|
+
{allLocales.map(lang => (
|
|
71
|
+
<link
|
|
72
|
+
rel="alternate"
|
|
73
|
+
href={`${Astro.url.origin}${base}${lang === defaultLocale ? '' : `/${lang}/`}`}
|
|
74
|
+
hreflang={lang === 'zh-tw' ? 'zh-TW' : lang}
|
|
75
|
+
/>
|
|
76
|
+
))}
|
|
77
|
+
|
|
78
|
+
<!-- Open Graph -->
|
|
79
|
+
<meta property="og:type" content={postTitle ? 'article' : 'website'} />
|
|
80
|
+
<meta property="og:url" content={Astro.url} />
|
|
81
|
+
<meta property="og:title" content={pageTitle} />
|
|
82
|
+
<meta property="og:description" content={pageDescription} />
|
|
83
|
+
<meta property="og:image" content={pageImage} />
|
|
84
|
+
<meta name="twitter:card" content="summary_large_image" />
|
|
85
|
+
{twitterID && <meta name="twitter:site" content={twitterID} />}
|
|
86
|
+
|
|
87
|
+
<!-- Site Verification -->
|
|
88
|
+
{google && <meta name="google-site-verification" content={google} />}
|
|
89
|
+
{bing && <meta name="msvalidate.01" content={bing} />}
|
|
90
|
+
{yandex && <meta name="yandex-verification" content={yandex} />}
|
|
91
|
+
{baidu && <meta name="baidu-site-verification" content={baidu} />}
|
|
92
|
+
|
|
93
|
+
<!-- Global View Transition -->
|
|
94
|
+
<ClientRouter fallback="none" />
|
|
95
|
+
|
|
96
|
+
<!-- Theme Toggle -->
|
|
97
|
+
<script
|
|
98
|
+
is:inline
|
|
99
|
+
define:vars={{
|
|
100
|
+
defaultMode,
|
|
101
|
+
lightMode,
|
|
102
|
+
darkMode,
|
|
103
|
+
reduceMotion,
|
|
104
|
+
}}
|
|
105
|
+
>
|
|
106
|
+
(function () {
|
|
107
|
+
// Check if current theme is dark
|
|
108
|
+
// Priority: localStorage theme > default theme > system preference
|
|
109
|
+
function isCurrentDark() {
|
|
110
|
+
const currentTheme = localStorage.getItem('theme')
|
|
111
|
+
if (currentTheme) {
|
|
112
|
+
return currentTheme === 'dark'
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (defaultMode !== 'auto') {
|
|
116
|
+
return defaultMode === 'dark'
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// If defaultMode is auto, follow system preference
|
|
120
|
+
return window.matchMedia('(prefers-color-scheme: dark)').matches
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Initialize theme
|
|
124
|
+
function initTheme(doc = document) {
|
|
125
|
+
const isDark = isCurrentDark()
|
|
126
|
+
doc.documentElement.classList.toggle('dark', isDark)
|
|
127
|
+
|
|
128
|
+
// Update meta theme-color tag
|
|
129
|
+
const metaThemeColor = doc.head.querySelector('meta[name="theme-color"]')
|
|
130
|
+
if (metaThemeColor) {
|
|
131
|
+
metaThemeColor.setAttribute('content', isDark ? darkMode : lightMode)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Check and init motion preference
|
|
136
|
+
// Condition: theme config || system preference || browser capability
|
|
137
|
+
function initMotionPref(doc = document) {
|
|
138
|
+
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
|
139
|
+
const supportsViewTransitions = 'startViewTransition' in doc
|
|
140
|
+
const shouldReduceMotion = reduceMotion || prefersReducedMotion || !supportsViewTransitions
|
|
141
|
+
|
|
142
|
+
doc.documentElement.classList.toggle('reduce-motion', shouldReduceMotion)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Update theme before page transition to prevent flashing
|
|
146
|
+
document.addEventListener('astro:before-swap', ({ newDocument }) => {
|
|
147
|
+
initTheme(newDocument)
|
|
148
|
+
initMotionPref(newDocument)
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
// Initialize theme on first load
|
|
152
|
+
initTheme()
|
|
153
|
+
initMotionPref()
|
|
154
|
+
})()
|
|
155
|
+
</script>
|
|
156
|
+
|
|
157
|
+
<!-- Google Analytics -->
|
|
158
|
+
<!-- Define gtag on window object for proper Partytown forwarding -->
|
|
159
|
+
<!-- See https://github.com/QwikDev/partytown/issues/382 -->
|
|
160
|
+
{googleAnalyticsID && (
|
|
161
|
+
<>
|
|
162
|
+
<script
|
|
163
|
+
is:inline
|
|
164
|
+
type="text/partytown"
|
|
165
|
+
src={customGoogleAnalyticsJS || `https://www.googletagmanager.com/gtag/js?id=${googleAnalyticsID}`}
|
|
166
|
+
/>
|
|
167
|
+
<script
|
|
168
|
+
is:inline
|
|
169
|
+
type="text/partytown"
|
|
170
|
+
define:vars={{
|
|
171
|
+
googleAnalyticsID,
|
|
172
|
+
customGoogleAnalyticsJS,
|
|
173
|
+
}}
|
|
174
|
+
>
|
|
175
|
+
window.dataLayer = window.dataLayer || []
|
|
176
|
+
window.gtag = function () {
|
|
177
|
+
// eslint-disable-next-line prefer-rest-params
|
|
178
|
+
dataLayer.push(arguments)
|
|
179
|
+
}
|
|
180
|
+
window.gtag('js', new Date())
|
|
181
|
+
|
|
182
|
+
if (customGoogleAnalyticsJS) {
|
|
183
|
+
window.gtag('config', googleAnalyticsID, {
|
|
184
|
+
transport_url: new URL(customGoogleAnalyticsJS).origin,
|
|
185
|
+
})
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
window.gtag('config', googleAnalyticsID)
|
|
189
|
+
}
|
|
190
|
+
</script>
|
|
191
|
+
</>
|
|
192
|
+
)}
|
|
193
|
+
|
|
194
|
+
<!-- Umami Analytics -->
|
|
195
|
+
{umamiAnalyticsID && (
|
|
196
|
+
<script
|
|
197
|
+
is:inline
|
|
198
|
+
type="text/partytown"
|
|
199
|
+
src={customUmamiAnalyticsJS || 'https://cloud.umami.is/script.js'}
|
|
200
|
+
data-website-id={umamiAnalyticsID}
|
|
201
|
+
/>
|
|
202
|
+
)}
|
|
203
|
+
</head>
|