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
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import type { CollectionEntry } from 'astro:content'
|
|
2
|
+
import type { Language } from '@/i18n/config'
|
|
3
|
+
import MarkdownIt from 'markdown-it'
|
|
4
|
+
import { defaultLocale } from '@/config'
|
|
5
|
+
|
|
6
|
+
type ExcerptScene = 'list' | 'meta' | 'og' | 'feed'
|
|
7
|
+
|
|
8
|
+
const markdownParser = new MarkdownIt()
|
|
9
|
+
const excerptLengths: Record<ExcerptScene, { cjk: number, other: number }> = {
|
|
10
|
+
list: {
|
|
11
|
+
cjk: 120,
|
|
12
|
+
other: 240,
|
|
13
|
+
},
|
|
14
|
+
meta: {
|
|
15
|
+
cjk: 120,
|
|
16
|
+
other: 240,
|
|
17
|
+
},
|
|
18
|
+
og: {
|
|
19
|
+
cjk: 70,
|
|
20
|
+
other: 140,
|
|
21
|
+
},
|
|
22
|
+
feed: {
|
|
23
|
+
cjk: 70,
|
|
24
|
+
other: 140,
|
|
25
|
+
},
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const htmlEntityMap: Record<string, string> = {
|
|
29
|
+
'<': '<',
|
|
30
|
+
'>': '>',
|
|
31
|
+
'&': '&',
|
|
32
|
+
'"': '"',
|
|
33
|
+
''': '\'',
|
|
34
|
+
' ': ' ',
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Cleans text by removing HTML tags and normalizing whitespace
|
|
38
|
+
function cleanTextContent(text: string): string {
|
|
39
|
+
// Remove HTML tags
|
|
40
|
+
let cleanText = text.replace(/<[^>]*>/g, '')
|
|
41
|
+
|
|
42
|
+
// Decode HTML entities
|
|
43
|
+
Object.entries(htmlEntityMap).forEach(([entity, char]) => {
|
|
44
|
+
cleanText = cleanText.replace(new RegExp(entity, 'g'), char)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
// Normalize whitespace
|
|
48
|
+
cleanText = cleanText.replace(/\s+/g, ' ')
|
|
49
|
+
|
|
50
|
+
// Normalize CJK punctuation spacing
|
|
51
|
+
cleanText = cleanText.replace(/([。?!:"」』])\s+/g, '$1')
|
|
52
|
+
|
|
53
|
+
return cleanText.trim()
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Creates a clean text excerpt with length limits by language and scene
|
|
57
|
+
function getExcerpt(text: string, lang: Language, scene: ExcerptScene): string {
|
|
58
|
+
const isCJK = (lang: Language) => ['zh', 'zh-tw', 'ja', 'ko'].includes(lang)
|
|
59
|
+
const length = isCJK(lang)
|
|
60
|
+
? excerptLengths[scene].cjk
|
|
61
|
+
: excerptLengths[scene].other
|
|
62
|
+
|
|
63
|
+
const cleanText = cleanTextContent(text)
|
|
64
|
+
const excerpt = cleanText.slice(0, length).trim()
|
|
65
|
+
|
|
66
|
+
// Remove trailing punctuation and add ellipsis
|
|
67
|
+
if (cleanText.length > length) {
|
|
68
|
+
return `${excerpt.replace(/\p{P}+$/u, '')}...`
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return excerpt
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Generates post description from existing description or content
|
|
75
|
+
type DescribableEntry = {
|
|
76
|
+
data: {
|
|
77
|
+
description?: string
|
|
78
|
+
lang?: string
|
|
79
|
+
}
|
|
80
|
+
body?: string
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function getEntryDescription(
|
|
84
|
+
entry: DescribableEntry,
|
|
85
|
+
scene: ExcerptScene,
|
|
86
|
+
): string {
|
|
87
|
+
const lang = (entry.data.lang || defaultLocale) as Language
|
|
88
|
+
|
|
89
|
+
if (entry.data.description) {
|
|
90
|
+
// Only truncate for og scene, return full description for other scenes
|
|
91
|
+
return scene === 'og'
|
|
92
|
+
? getExcerpt(entry.data.description, lang, scene)
|
|
93
|
+
: entry.data.description
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const rawContent = entry.body || ''
|
|
97
|
+
|
|
98
|
+
// Check for <!-- more --> marker (Hexo-style excerpt boundary)
|
|
99
|
+
const moreMarkerRegex = /<!--\s*more\s*-->/i
|
|
100
|
+
const moreMatch = rawContent.match(moreMarkerRegex)
|
|
101
|
+
const hasMoreMarker = moreMatch && moreMatch.index !== undefined
|
|
102
|
+
|
|
103
|
+
// Get content to process (before <!-- more --> if exists)
|
|
104
|
+
let contentToProcess = rawContent
|
|
105
|
+
if (hasMoreMarker) {
|
|
106
|
+
contentToProcess = rawContent.substring(0, moreMatch.index)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const cleanContent = contentToProcess
|
|
110
|
+
.replace(/<!--[\s\S]*?-->/g, '') // Remove remaining HTML comments
|
|
111
|
+
.replace(/```[\s\S]*?```/g, '') // Remove code blocks
|
|
112
|
+
.replace(/^\s*#{1,6}\s+\S.*$/gm, '') // Remove Markdown headings
|
|
113
|
+
.replace(/^\s*::.*$/gm, '') // Remove directive containers
|
|
114
|
+
.replace(/^\s*>\s*\[!.*\]$/gm, '') // Remove GitHub admonition markers
|
|
115
|
+
.replace(/\n{2,}/g, '\n\n') // Normalize newlines
|
|
116
|
+
|
|
117
|
+
const renderedContent = markdownParser.render(cleanContent)
|
|
118
|
+
|
|
119
|
+
// For 'list' scene with <!-- more --> marker, return full content without truncation
|
|
120
|
+
if (scene === 'list' && hasMoreMarker) {
|
|
121
|
+
return cleanTextContent(renderedContent)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Otherwise, apply truncation
|
|
125
|
+
return getExcerpt(renderedContent, lang, scene)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function getPostDescription(
|
|
129
|
+
post: CollectionEntry<'posts'>,
|
|
130
|
+
scene: ExcerptScene,
|
|
131
|
+
): string {
|
|
132
|
+
return getEntryDescription(post, scene)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function getNoteDescription(
|
|
136
|
+
note: CollectionEntry<'notes'>,
|
|
137
|
+
scene: ExcerptScene,
|
|
138
|
+
): string {
|
|
139
|
+
return getEntryDescription(note, scene)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function getJournalDescription(
|
|
143
|
+
journal: CollectionEntry<'journals'>,
|
|
144
|
+
scene: ExcerptScene,
|
|
145
|
+
): string {
|
|
146
|
+
return getEntryDescription(journal, scene)
|
|
147
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic accessors for dynamically-registered content collections.
|
|
3
|
+
*
|
|
4
|
+
* Every folder discovered under `content/` (excluding built-ins and
|
|
5
|
+
* underscore-prefixed folders) is registered with the same schema as `posts`.
|
|
6
|
+
* Templates in `src/pages/_dynamic/` use these helpers to fetch and group
|
|
7
|
+
* entries the same way the posts/notes/journals utilities do — minus the
|
|
8
|
+
* tag/category bookkeeping that only makes sense for posts.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { CollectionEntry } from 'astro:content'
|
|
12
|
+
import type { Language } from '@/i18n/config'
|
|
13
|
+
import { getCollection, render } from 'astro:content'
|
|
14
|
+
import { allLocales, defaultLocale } from '@/config'
|
|
15
|
+
|
|
16
|
+
// All dynamic collections share the same loader+schema as `posts`, so a
|
|
17
|
+
// CollectionEntry<'posts'> is structurally identical. Cast through the
|
|
18
|
+
// `posts` slot so the rest of the codebase's types still line up.
|
|
19
|
+
export type DynamicEntry = CollectionEntry<'posts'>
|
|
20
|
+
|
|
21
|
+
export interface DynamicEntryWithMeta extends DynamicEntry {
|
|
22
|
+
remarkPluginFrontmatter: { minutes: number }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface DynamicGroup {
|
|
26
|
+
baseId: string
|
|
27
|
+
slug: string
|
|
28
|
+
supportedLangs: Language[]
|
|
29
|
+
byLang: Partial<Record<Language, DynamicEntry>>
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function slugifyPathSegment(input: string): string {
|
|
33
|
+
return input
|
|
34
|
+
.normalize('NFKC')
|
|
35
|
+
.trim()
|
|
36
|
+
.toLowerCase()
|
|
37
|
+
.replace(/[\s_]+/g, '-')
|
|
38
|
+
.replace(/[^\p{Letter}\p{Number}]+/gu, '-')
|
|
39
|
+
.replace(/-+/g, '-')
|
|
40
|
+
.replace(/^-|-$/g, '')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function getBaseId(entry: DynamicEntry): string {
|
|
44
|
+
const id = entry.id.trim()
|
|
45
|
+
for (const lang of allLocales) {
|
|
46
|
+
const suffix = `.${lang}`
|
|
47
|
+
if (id.endsWith(suffix))
|
|
48
|
+
return id.slice(0, -suffix.length)
|
|
49
|
+
}
|
|
50
|
+
return id
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Read entries from a dynamic collection. Returns `[]` if the collection
|
|
55
|
+
* is not registered (no folder, or `enabled: false`) instead of crashing —
|
|
56
|
+
* this matches the behaviour of the built-in note/journal utilities.
|
|
57
|
+
*/
|
|
58
|
+
async function safeGetCollection(name: string): Promise<DynamicEntry[]> {
|
|
59
|
+
try {
|
|
60
|
+
const entries = await getCollection(
|
|
61
|
+
name as 'posts',
|
|
62
|
+
({ data }: DynamicEntry) => !data.draft,
|
|
63
|
+
)
|
|
64
|
+
return entries
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
68
|
+
if (
|
|
69
|
+
message.includes(`The collection "${name}" does not exist`)
|
|
70
|
+
|| message.includes(`The collection "${name}" is empty`)
|
|
71
|
+
) {
|
|
72
|
+
return []
|
|
73
|
+
}
|
|
74
|
+
throw error
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function getDynamicGroups(collection: string): Promise<DynamicGroup[]> {
|
|
79
|
+
const entries = await safeGetCollection(collection)
|
|
80
|
+
|
|
81
|
+
const buckets = new Map<string, DynamicEntry[]>()
|
|
82
|
+
for (const entry of entries) {
|
|
83
|
+
const baseId = getBaseId(entry)
|
|
84
|
+
const list = buckets.get(baseId) ?? []
|
|
85
|
+
list.push(entry)
|
|
86
|
+
buckets.set(baseId, list)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const slugToBaseId = new Map<string, string>()
|
|
90
|
+
const results: DynamicGroup[] = []
|
|
91
|
+
|
|
92
|
+
for (const [baseId, members] of buckets) {
|
|
93
|
+
const slug = slugifyPathSegment(baseId) || baseId
|
|
94
|
+
const existing = slugToBaseId.get(slug)
|
|
95
|
+
if (existing && existing !== baseId) {
|
|
96
|
+
throw new Error(
|
|
97
|
+
`Duplicate slug "${slug}" in collection "${collection}" `
|
|
98
|
+
+ `from "${existing}" and "${baseId}"`,
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
slugToBaseId.set(slug, baseId)
|
|
102
|
+
|
|
103
|
+
const baseEntry = members.find(e => !e.data.lang)
|
|
104
|
+
const zhEntry = members.find(e => e.data.lang === 'zh')
|
|
105
|
+
const enEntry = members.find(e => e.data.lang === 'en')
|
|
106
|
+
const jaEntry = members.find(e => e.data.lang === 'ja')
|
|
107
|
+
|
|
108
|
+
// Best-effort base-language inference, mirroring the post/note logic:
|
|
109
|
+
// a sole base file alongside an explicit `zh` translation is taken as en.
|
|
110
|
+
const inferredEnFromBase = !enEntry && !!zhEntry ? baseEntry : undefined
|
|
111
|
+
const inferredZhFromBase = !zhEntry ? baseEntry : undefined
|
|
112
|
+
|
|
113
|
+
const byLang: DynamicGroup['byLang'] = {
|
|
114
|
+
zh: zhEntry ?? inferredZhFromBase,
|
|
115
|
+
en: enEntry ?? inferredEnFromBase,
|
|
116
|
+
ja: jaEntry,
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const supportedLangs = allLocales.filter(lang => byLang[lang])
|
|
120
|
+
|
|
121
|
+
results.push({ baseId, slug, supportedLangs, byLang })
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return results
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function getDynamicSlug(entry: DynamicEntry): string {
|
|
128
|
+
return slugifyPathSegment(getBaseId(entry)) || getBaseId(entry)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function attachMeta(entry: DynamicEntry): Promise<DynamicEntryWithMeta> {
|
|
132
|
+
const { remarkPluginFrontmatter } = await render(entry)
|
|
133
|
+
return {
|
|
134
|
+
...entry,
|
|
135
|
+
remarkPluginFrontmatter: remarkPluginFrontmatter as { minutes: number },
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export async function getDynamicEntries(
|
|
140
|
+
collection: string,
|
|
141
|
+
lang?: Language,
|
|
142
|
+
): Promise<DynamicEntryWithMeta[]> {
|
|
143
|
+
const currentLang = lang && allLocales.includes(lang) ? lang : defaultLocale
|
|
144
|
+
const groups = await getDynamicGroups(collection)
|
|
145
|
+
const selected = groups
|
|
146
|
+
.map(group => group.byLang[currentLang])
|
|
147
|
+
.filter(Boolean) as DynamicEntry[]
|
|
148
|
+
|
|
149
|
+
const enhanced = await Promise.all(selected.map(attachMeta))
|
|
150
|
+
|
|
151
|
+
const sortKey = (entry: DynamicEntry) =>
|
|
152
|
+
(entry.data.updated ?? entry.data.published).valueOf()
|
|
153
|
+
|
|
154
|
+
return enhanced.sort((a, b) => sortKey(b) - sortKey(a))
|
|
155
|
+
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import type { APIContext, ImageMetadata } from 'astro'
|
|
2
|
+
import type { Language } from '@/i18n/config'
|
|
3
|
+
import { getImage } from 'astro:assets'
|
|
4
|
+
import { Feed } from 'feed'
|
|
5
|
+
import MarkdownIt from 'markdown-it'
|
|
6
|
+
import { parse } from 'node-html-parser'
|
|
7
|
+
import sanitizeHtml from 'sanitize-html'
|
|
8
|
+
import { base, defaultLocale, themeConfig } from '@/config'
|
|
9
|
+
import { getPostPath } from '@/i18n/path'
|
|
10
|
+
import { ui } from '@/i18n/ui'
|
|
11
|
+
import { memoize } from '@/utils/cache'
|
|
12
|
+
import { getPosts, getPostSlug } from '@/utils/content'
|
|
13
|
+
import { getPostDescription } from '@/utils/description'
|
|
14
|
+
|
|
15
|
+
const markdownParser = new MarkdownIt()
|
|
16
|
+
const { title, description, i18nTitle, url, author } = themeConfig.site
|
|
17
|
+
const { follow } = themeConfig.seo ?? {}
|
|
18
|
+
|
|
19
|
+
// >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
|
|
20
|
+
// Dynamically import all images from /content/posts
|
|
21
|
+
const imagesGlob = import.meta.glob<{ default: ImageMetadata }>(
|
|
22
|
+
'/content/posts/**/*.{jpeg,jpg,png,gif,webp}',
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Converts relative image paths to absolute URLs
|
|
27
|
+
*
|
|
28
|
+
* @param srcPath - Relative image path from markdown content
|
|
29
|
+
* @param baseUrl - Site base URL
|
|
30
|
+
* @returns Optimized image URL or null if processing fails
|
|
31
|
+
*/
|
|
32
|
+
async function _getAbsoluteImageUrl(srcPath: string, baseUrl: string) {
|
|
33
|
+
// Remove relative path prefixes (../ and ./) from image source path
|
|
34
|
+
const prefixRemoved = srcPath.replace(/^(?:\.\.\/)+|^\.\//, '')
|
|
35
|
+
const absolutePath = `/content/posts/${prefixRemoved}`
|
|
36
|
+
const imageImporter = imagesGlob[absolutePath]
|
|
37
|
+
|
|
38
|
+
if (!imageImporter) {
|
|
39
|
+
return null
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Import image module and extract its metadata
|
|
43
|
+
const imageMetadata = await imageImporter()
|
|
44
|
+
.then(importedModule => importedModule.default)
|
|
45
|
+
.catch((error) => {
|
|
46
|
+
console.warn(`Failed to import image: ${absolutePath}`, error)
|
|
47
|
+
return null
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
if (!imageMetadata) {
|
|
51
|
+
return null
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Create optimized image from metadata
|
|
55
|
+
const optimizedImage = await getImage({ src: imageMetadata })
|
|
56
|
+
return new URL(optimizedImage.src, baseUrl).toString()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Export memoized version
|
|
60
|
+
const getAbsoluteImageUrl = memoize(_getAbsoluteImageUrl)
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Fix relative image paths in HTML content
|
|
64
|
+
*
|
|
65
|
+
* @param htmlContent HTML content string
|
|
66
|
+
* @param baseUrl Base URL of the site
|
|
67
|
+
* @returns Processed HTML string with all image paths converted to absolute URLs
|
|
68
|
+
*/
|
|
69
|
+
async function fixRelativeImagePaths(htmlContent: string, baseUrl: string): Promise<string> {
|
|
70
|
+
const htmlDoc = parse(htmlContent)
|
|
71
|
+
const images = htmlDoc.getElementsByTagName('img')
|
|
72
|
+
const imagePromises = []
|
|
73
|
+
|
|
74
|
+
for (const img of images) {
|
|
75
|
+
const src = img.getAttribute('src')
|
|
76
|
+
if (!src) {
|
|
77
|
+
continue
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
imagePromises.push((async () => {
|
|
81
|
+
try {
|
|
82
|
+
// Skip absolute URLs and unsupported sources
|
|
83
|
+
if (!src.startsWith('./') && !src.startsWith('../') && !src.startsWith('_images/')) {
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Process images from content/posts
|
|
88
|
+
const absoluteImageUrl = await getAbsoluteImageUrl(src, baseUrl)
|
|
89
|
+
if (absoluteImageUrl) {
|
|
90
|
+
img.setAttribute('src', absoluteImageUrl)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
console.warn(`Failed to convert relative image path to absolute URL: ${src}`, error)
|
|
95
|
+
}
|
|
96
|
+
})())
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
await Promise.all(imagePromises)
|
|
100
|
+
|
|
101
|
+
return htmlDoc.toString()
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
|
|
106
|
+
* Generate a feed object supporting both RSS and Atom formats
|
|
107
|
+
*
|
|
108
|
+
* @param options Feed generation options
|
|
109
|
+
* @param options.lang Optional language code
|
|
110
|
+
* @returns A Feed instance ready for RSS or Atom output
|
|
111
|
+
*/
|
|
112
|
+
export async function generateFeed({ lang }: { lang?: Language } = {}) {
|
|
113
|
+
const currentUI = ui[lang as keyof typeof ui] ?? ui[defaultLocale as keyof typeof ui] ?? {}
|
|
114
|
+
const siteURL = lang ? `${url}${base}/${lang}` : `${url}${base}/`
|
|
115
|
+
|
|
116
|
+
// Create Feed instance
|
|
117
|
+
const feed = new Feed({
|
|
118
|
+
title: i18nTitle ? currentUI.title : title,
|
|
119
|
+
description: i18nTitle ? currentUI.description : description,
|
|
120
|
+
id: siteURL,
|
|
121
|
+
link: siteURL,
|
|
122
|
+
language: lang ?? themeConfig.global.locale,
|
|
123
|
+
copyright: `Copyright © ${new Date().getFullYear()} ${author}`,
|
|
124
|
+
updated: new Date(),
|
|
125
|
+
generator: 'Astro-Theme-Retypeset with Feed for Node.js',
|
|
126
|
+
|
|
127
|
+
feedLinks: {
|
|
128
|
+
rss: new URL(lang ? `${base}/${lang}/rss.xml` : `${base}/rss.xml`, url).toString(),
|
|
129
|
+
atom: new URL(lang ? `${base}/${lang}/atom.xml` : `${base}/atom.xml`, url).toString(),
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
author: {
|
|
133
|
+
name: author,
|
|
134
|
+
link: `${url}${base}/`,
|
|
135
|
+
},
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
// Language-aware selection (includes inferred base-language posts)
|
|
139
|
+
const recentPosts = (await getPosts(lang))
|
|
140
|
+
.slice(0, 25)
|
|
141
|
+
|
|
142
|
+
// Add posts to feed
|
|
143
|
+
for (const post of recentPosts) {
|
|
144
|
+
const slug = getPostSlug(post)
|
|
145
|
+
const link = new URL(
|
|
146
|
+
getPostPath(slug, (lang ?? defaultLocale) as Language),
|
|
147
|
+
url,
|
|
148
|
+
).toString()
|
|
149
|
+
|
|
150
|
+
// Optimize content processing
|
|
151
|
+
const postContent = post.body
|
|
152
|
+
? sanitizeHtml(
|
|
153
|
+
await fixRelativeImagePaths(
|
|
154
|
+
// Remove HTML comments before rendering markdown
|
|
155
|
+
markdownParser.render(post.body.replace(/<!--[\s\S]*?-->/g, '')),
|
|
156
|
+
`${url}${base}/`,
|
|
157
|
+
),
|
|
158
|
+
{
|
|
159
|
+
// Allow <img> tags in feed content
|
|
160
|
+
allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img']),
|
|
161
|
+
},
|
|
162
|
+
)
|
|
163
|
+
: ''
|
|
164
|
+
|
|
165
|
+
// publishDate -> Atom:<published>, RSS:<pubDate>
|
|
166
|
+
const publishDate = new Date(post.data.published)
|
|
167
|
+
// updateDate -> Atom:<updated>, RSS has no update tag
|
|
168
|
+
const updateDate = post.data.updated ? new Date(post.data.updated) : publishDate
|
|
169
|
+
|
|
170
|
+
feed.addItem({
|
|
171
|
+
title: post.data.title,
|
|
172
|
+
id: link,
|
|
173
|
+
link,
|
|
174
|
+
description: getPostDescription(post, 'feed'),
|
|
175
|
+
content: postContent,
|
|
176
|
+
author: [{
|
|
177
|
+
name: author,
|
|
178
|
+
link: `${url}${base}/`,
|
|
179
|
+
}],
|
|
180
|
+
published: publishDate,
|
|
181
|
+
date: updateDate,
|
|
182
|
+
})
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Add follow verification if available
|
|
186
|
+
if (follow?.feedID && follow?.userID) {
|
|
187
|
+
feed.addExtension({
|
|
188
|
+
name: 'follow_challenge',
|
|
189
|
+
objects: {
|
|
190
|
+
feedId: follow.feedID,
|
|
191
|
+
userId: follow.userID,
|
|
192
|
+
},
|
|
193
|
+
})
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return feed
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
|
|
200
|
+
// Generate RSS 2.0 format feed
|
|
201
|
+
export async function generateRSS(context: APIContext) {
|
|
202
|
+
const feed = await generateFeed({
|
|
203
|
+
lang: context.params?.lang as Language | undefined,
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
// Add XSLT stylesheet to RSS feed
|
|
207
|
+
let rssXml = feed.rss2()
|
|
208
|
+
rssXml = rssXml.replace(
|
|
209
|
+
'<?xml version="1.0" encoding="utf-8"?>',
|
|
210
|
+
`<?xml version="1.0" encoding="utf-8"?>\n<?xml-stylesheet href="${base}/feeds/rss-style.xsl" type="text/xsl"?>`,
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
return new Response(rssXml, {
|
|
214
|
+
headers: {
|
|
215
|
+
'Content-Type': 'application/rss+xml; charset=utf-8',
|
|
216
|
+
},
|
|
217
|
+
})
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Generate Atom 1.0 format feed
|
|
221
|
+
export async function generateAtom(context: APIContext) {
|
|
222
|
+
const feed = await generateFeed({
|
|
223
|
+
lang: context.params?.lang as Language | undefined,
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
// Add XSLT stylesheet to Atom feed
|
|
227
|
+
let atomXml = feed.atom1()
|
|
228
|
+
atomXml = atomXml.replace(
|
|
229
|
+
'<?xml version="1.0" encoding="utf-8"?>',
|
|
230
|
+
`<?xml version="1.0" encoding="utf-8"?>\n<?xml-stylesheet href="${base}/feeds/atom-style.xsl" type="text/xsl"?>`,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
return new Response(atomXml, {
|
|
234
|
+
headers: {
|
|
235
|
+
'Content-Type': 'application/atom+xml; charset=utf-8',
|
|
236
|
+
},
|
|
237
|
+
})
|
|
238
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { base, moreLocales } from '@/config'
|
|
2
|
+
import { getLangFromPath } from '@/i18n/lang'
|
|
3
|
+
import { getLocalizedPath } from '@/i18n/path'
|
|
4
|
+
|
|
5
|
+
function stripHtmlExtension(path: string): string {
|
|
6
|
+
return path.endsWith('.html') ? path.slice(0, -'.html'.length) : path
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// Determine if the path matches a specific page type
|
|
10
|
+
function matchPageType(path: string, prefix: string = '') {
|
|
11
|
+
// Remove base path if configured
|
|
12
|
+
const pathWithoutBase = base && path.startsWith(base)
|
|
13
|
+
? path.slice(base.length)
|
|
14
|
+
: path
|
|
15
|
+
|
|
16
|
+
// Remove leading and trailing slashes from the path
|
|
17
|
+
const normalizedPath = stripHtmlExtension(pathWithoutBase.replace(/^\/|\/$/g, ''))
|
|
18
|
+
|
|
19
|
+
// Homepage check: matches root path ('') or language code ('en')
|
|
20
|
+
//
|
|
21
|
+
// Astro 6 with `build.format: 'file'` + `trailingSlash: 'never'` writes
|
|
22
|
+
// `dist/index.html` and reports `Astro.url.pathname === '/index.html'`
|
|
23
|
+
// for the homepage (Astro 5 used '/'). Normalising past the `.html` strip
|
|
24
|
+
// leaves us with 'index' or '<lang>/index' for the localised variants;
|
|
25
|
+
// treat those as homepage too so excerpt rendering keeps working.
|
|
26
|
+
if (prefix === '') {
|
|
27
|
+
if (normalizedPath === '' || normalizedPath === 'index') {
|
|
28
|
+
return true
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const locales = moreLocales as readonly string[]
|
|
32
|
+
if (locales.includes(normalizedPath)) {
|
|
33
|
+
return true
|
|
34
|
+
}
|
|
35
|
+
if (locales.some(lang => normalizedPath === `${lang}/index`)) {
|
|
36
|
+
return true
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Pagination pages: /2, /3 ... and /en/2, /ja/3 ...
|
|
40
|
+
if (/^\d+$/.test(normalizedPath)) {
|
|
41
|
+
return true
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return locales.some((lang) => {
|
|
45
|
+
if (!normalizedPath.startsWith(`${lang}/`)) {
|
|
46
|
+
return false
|
|
47
|
+
}
|
|
48
|
+
const rest = normalizedPath.slice(lang.length + 1)
|
|
49
|
+
return /^\d+$/.test(rest)
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Ensure strict segment boundary matching to prevent partial matches
|
|
54
|
+
const startsWithSegment = (target: string, segment: string) =>
|
|
55
|
+
target === segment || target.startsWith(`${segment}/`)
|
|
56
|
+
|
|
57
|
+
// Match both default language paths and localized paths
|
|
58
|
+
return startsWithSegment(normalizedPath, prefix)
|
|
59
|
+
|| moreLocales.some(lang => startsWithSegment(normalizedPath, `${lang}/${prefix}`))
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function isHomePage(path: string) {
|
|
63
|
+
return matchPageType(path)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function isPostPage(path: string) {
|
|
67
|
+
return matchPageType(path, 'posts')
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function isNotePage(path: string) {
|
|
71
|
+
return matchPageType(path, 'notes')
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function isJournalPage(path: string) {
|
|
75
|
+
return matchPageType(path, 'journals')
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function isTagPage(path: string) {
|
|
79
|
+
return matchPageType(path, 'tags')
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function isAboutPage(path: string) {
|
|
83
|
+
return matchPageType(path, 'about')
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Returns page context with language, page types and localization helper
|
|
87
|
+
export function getPageInfo(path: string) {
|
|
88
|
+
const currentLang = getLangFromPath(path)
|
|
89
|
+
const isHome = isHomePage(path)
|
|
90
|
+
const isPost = isPostPage(path)
|
|
91
|
+
const isNote = isNotePage(path)
|
|
92
|
+
const isJournal = isJournalPage(path)
|
|
93
|
+
const isTag = isTagPage(path)
|
|
94
|
+
const isAbout = isAboutPage(path)
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
currentLang,
|
|
98
|
+
isHome,
|
|
99
|
+
isPost,
|
|
100
|
+
isNote,
|
|
101
|
+
isJournal,
|
|
102
|
+
isTag,
|
|
103
|
+
isAbout,
|
|
104
|
+
getLocalizedPath: (targetPath: string) =>
|
|
105
|
+
getLocalizedPath(targetPath, currentLang),
|
|
106
|
+
}
|
|
107
|
+
}
|
package/tsconfig.json
ADDED