@windcoder/astro-theme-orchid 0.0.0-stage → 0.2.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.
Files changed (83) hide show
  1. package/README.md +118 -2
  2. package/package.json +70 -5
  3. package/src/components/AlbumCard.astro +73 -0
  4. package/src/components/Analytics.astro +22 -0
  5. package/src/components/BaseLayout.astro +188 -0
  6. package/src/components/Calendar.astro +379 -0
  7. package/src/components/CategoryBar.astro +74 -0
  8. package/src/components/Comment.astro +48 -0
  9. package/src/components/EncryptedContent.astro +42 -0
  10. package/src/components/Footer.astro +31 -0
  11. package/src/components/Header.astro +153 -0
  12. package/src/components/License.astro +29 -0
  13. package/src/components/Live2DWidget.astro +262 -0
  14. package/src/components/MusicPlayer.astro +940 -0
  15. package/src/components/OutdatedCard.astro +53 -0
  16. package/src/components/Pagination.astro +94 -0
  17. package/src/components/PhotoCard.astro +22 -0
  18. package/src/components/PostCard.astro +89 -0
  19. package/src/components/PostNav.astro +40 -0
  20. package/src/components/RecommendedPost.astro +166 -0
  21. package/src/components/SakuraEffect.astro +152 -0
  22. package/src/components/Sidebar.astro +334 -0
  23. package/src/components/Toc.astro +27 -0
  24. package/src/components/analytics/GoogleAnalytics.astro +22 -0
  25. package/src/components/analytics/La51Analytics.astro +52 -0
  26. package/src/components/analytics/MicrosoftClarity.astro +18 -0
  27. package/src/components/analytics/UmamiAnalytics.astro +86 -0
  28. package/src/components/comments/Artalk.astro +51 -0
  29. package/src/components/comments/Disqus.astro +43 -0
  30. package/src/components/comments/Giscus.astro +50 -0
  31. package/src/components/comments/Twikoo.astro +65 -0
  32. package/src/components/comments/Waline.astro +36 -0
  33. package/src/i18n/en.ts +164 -0
  34. package/src/i18n/index.ts +18 -0
  35. package/src/i18n/resolve.ts +49 -0
  36. package/src/i18n/zh-CN.ts +184 -0
  37. package/src/icons.ts +71 -0
  38. package/src/index.ts +411 -0
  39. package/src/plugins/plantuml-encoder.ts +95 -0
  40. package/src/plugins/rehype-diagram.ts +218 -0
  41. package/src/plugins/rehype-ec-vt-rerun.ts +80 -0
  42. package/src/plugins/rehype-email.ts +75 -0
  43. package/src/plugins/rehype-github-card.ts +114 -0
  44. package/src/plugins/rehype.ts +284 -0
  45. package/src/plugins/remark-diagram.ts +78 -0
  46. package/src/plugins/remark-directives.ts +100 -0
  47. package/src/plugins/remark-excerpt.ts +29 -0
  48. package/src/plugins/remark-image-grid.ts +97 -0
  49. package/src/plugins/remark-reading-time.ts +38 -0
  50. package/src/plugins/scripts/diagram-panzoom.js +388 -0
  51. package/src/plugins/scripts/plantuml-theme-switch.js +90 -0
  52. package/src/routes/404.astro +14 -0
  53. package/src/routes/_hydrate.ts +34 -0
  54. package/src/routes/about.astro +25 -0
  55. package/src/routes/api/allPostMeta.json.ts +26 -0
  56. package/src/routes/archive.astro +81 -0
  57. package/src/routes/categories/[category].astro +37 -0
  58. package/src/routes/categories/index.astro +29 -0
  59. package/src/routes/friends.astro +83 -0
  60. package/src/routes/gallery/[album].astro +97 -0
  61. package/src/routes/gallery/index.astro +100 -0
  62. package/src/routes/guestbook.astro +37 -0
  63. package/src/routes/index.astro +47 -0
  64. package/src/routes/og/[...slug].ts +281 -0
  65. package/src/routes/page/[page].astro +53 -0
  66. package/src/routes/posts/[...slug].astro +350 -0
  67. package/src/routes/robots.txt.ts +18 -0
  68. package/src/routes/rss.xml.ts +37 -0
  69. package/src/routes/series/[name].astro +66 -0
  70. package/src/routes/series/index.astro +64 -0
  71. package/src/routes/sponsor.astro +84 -0
  72. package/src/routes/tags/[tag].astro +40 -0
  73. package/src/routes/tags/index.astro +29 -0
  74. package/src/schema.ts +1218 -0
  75. package/src/scripts/app.ts +1041 -0
  76. package/src/scripts/share-poster.ts +468 -0
  77. package/src/styles/global.css +4864 -0
  78. package/src/utils/crypto.ts +46 -0
  79. package/src/utils/gallery.ts +76 -0
  80. package/src/utils/lqip.ts +18 -0
  81. package/src/utils/related.ts +101 -0
  82. package/src/utils.ts +233 -0
  83. package/src/vite.ts +105 -0
@@ -0,0 +1,334 @@
1
+ ---
2
+ import config from "virtual:theme/config";
3
+ // Icon(§4.65):Iconify 标准图标组件,构建期内联渲染
4
+ import { Icon } from "astro-icon/components";
5
+ import { t } from "../i18n/index.js";
6
+ import { resolveThemeIcon } from "../icons.js";
7
+ import {
8
+ collectCategories,
9
+ collectTags,
10
+ daysSince,
11
+ type PostLike,
12
+ } from "../utils.js";
13
+ import Calendar from "./Calendar.astro";
14
+ import type { TocHeading } from "./Toc.astro";
15
+
16
+ interface Props {
17
+ /** 全站已发布文章(用于聚合分类/标签/统计) */
18
+ posts: PostLike[];
19
+ /** list:首页/列表页;post:文章详情页(可选 toc widget) */
20
+ variant?: "list" | "post";
21
+ /** 文章标题锚点(variant=post 时供 toc widget 渲染) */
22
+ headings?: TocHeading[];
23
+ }
24
+
25
+ const { posts, variant = "list", headings = [] } = Astro.props;
26
+ const isPost = variant === "post";
27
+ const tocItems = headings.filter((h) => h.depth >= 2 && h.depth <= 4);
28
+ const cats = [...collectCategories(posts).entries()];
29
+ const tags = [...collectTags(posts).keys()];
30
+
31
+ // 标签 widget(§4.48):仅展示前 count 个热门标签(collectTags 已按文章数降序),
32
+ // 其余收进「…更多」入口跳转 /tags/;count=0 表示全部展示。
33
+ // §4.52 专属参数统一入口:从 widgets 数组 tags 项的 specificConfig 读取
34
+ const tagWidget = config.sidebar.widgets.find((w) => w.type === "tags");
35
+ const tagCount =
36
+ tagWidget?.type === "tags" ? tagWidget.specificConfig.count : 12;
37
+ const shownTags = tagCount > 0 ? tags.slice(0, tagCount) : tags;
38
+ const tagHidden = tags.length - shownTags.length;
39
+
40
+ const runDays = config.since ? daysSince(config.since) : null;
41
+
42
+ // widget 开关与顺序(P2 侧栏改造):config.sidebar.widgets 数组即渲染顺序;
43
+ // 两个方向的显隐(对齐参考模板 showOnPostPage/hideOnNonPostPage):
44
+ // showOnPost=false 仅列表页显示;postOnly=true 仅文章页侧栏显示;文章页整体开关由路由层 onPost 把关
45
+ // hideOnMobile=true 的 widget 在 <768px 隐藏(§4.49 移动端差异化,CSS .side-hide-m)
46
+ const widgets = config.sidebar.widgets.filter(
47
+ (w) => w.enabled && (isPost ? w.showOnPost : !w.postOnly),
48
+ );
49
+
50
+ // §4.50 sticky 侧栏:按 position 拆两组——top 组普通流式,sticky 组包进粘性容器
51
+ // (≥768px 跟随滚动,CSS .side-sticky);组内保持 widgets 数组顺序
52
+ const topWidgets = widgets.filter((w) => w.position !== "sticky");
53
+ const stickyWidgets = widgets.filter((w) => w.position === "sticky");
54
+
55
+ // 公告 widget(P1-17):content 留空整卡不渲染
56
+ const ann = config.announcement;
57
+ const showAnnouncement = ann.content.trim() !== "";
58
+ const annLink = Boolean(ann.link.text && ann.link.url);
59
+
60
+ // 个人资料卡(§4.15):头像/昵称/简介至少配一项才渲染
61
+ const profile = config.profile;
62
+ const showProfile = Boolean(profile.name || profile.avatar || profile.bio);
63
+ const profileInitial = (profile.name || "?").trim().slice(0, 1) || "?";
64
+
65
+ // 广告 widget(P2 保留项,默认关):enabled + 至少一项内容 + 未过期才渲染
66
+ const ad = config.advertisement;
67
+ const adExpired = ad.expireDate ? new Date() > new Date(ad.expireDate) : false;
68
+ const showAd =
69
+ ad.enabled && Boolean(ad.image || ad.content || ad.link.text) && !adExpired;
70
+
71
+ // 友链 widget(决策 8):与友链页同源 themeConfig.friends.list;
72
+ // random=false 时 featured 优先、其余按 weight 降序,取前 count 个。
73
+ // §4.52 专属参数统一入口:count/random 从 widgets 数组 friends 项的 specificConfig 读取
74
+ const fw = config.sidebar.widgets.find((w) => w.type === "friends");
75
+ let sideFriends: typeof config.friends.list = [];
76
+ if (fw?.type === "friends") {
77
+ const sc = fw.specificConfig;
78
+ const enabled = config.friends.list.filter((f) => f.enabled);
79
+ const pool = sc.random
80
+ ? [...enabled].sort(() => Math.random() - 0.5)
81
+ : [...enabled].sort((a, b) => {
82
+ if (a.featured !== b.featured) return a.featured ? -1 : 1;
83
+ return b.weight - a.weight;
84
+ });
85
+ sideFriends = pool.slice(0, sc.count);
86
+ }
87
+ ---
88
+
89
+ <aside class="sidebar">
90
+ {
91
+ [
92
+ { cls: "side-top", list: topWidgets },
93
+ { cls: "side-sticky", list: stickyWidgets },
94
+ ]
95
+ .filter((g) => g.list.length > 0)
96
+ .map((group) => (
97
+ <div class={group.cls}>
98
+ {group.list.map((w) => (
99
+ <>
100
+ {w.type === 'profile' && showProfile && (
101
+ <section class:list={["side-card", "card", "reveal", "profile-card", { "side-hide-m": w.hideOnMobile }]}>
102
+ {profile.url ? (
103
+ <a class="profile-avatar-link" href={profile.url} aria-label={profile.name}>
104
+ {profile.avatar ? (
105
+ <img class="profile-avatar" src={profile.avatar} alt={profile.name} loading="lazy" />
106
+ ) : (
107
+ <span class="profile-avatar avatar-fallback">{profileInitial}</span>
108
+ )}
109
+ </a>
110
+ ) : profile.avatar ? (
111
+ <img class="profile-avatar" src={profile.avatar} alt={profile.name} loading="lazy" />
112
+ ) : (
113
+ <span class="profile-avatar avatar-fallback">{profileInitial}</span>
114
+ )}
115
+ {profile.name && (
116
+ <div class="profile-id">
117
+ {profile.url ? (
118
+ <a class="profile-name" href={profile.url}>{profile.name}</a>
119
+ ) : (
120
+ <span class="profile-name">{profile.name}</span>
121
+ )}
122
+ <span class="profile-bar" aria-hidden="true" />
123
+ </div>
124
+ )}
125
+ {profile.bio && <p class="profile-bio">{profile.bio}</p>}
126
+ {profile.links.length > 0 && (
127
+ <div class="profile-links">
128
+ {profile.links.map((lk) => {
129
+ const external = /^https?:\/\//.test(lk.url);
130
+ // icon 兼容三种写法(§4.65):"<" 开头为内联 SVG(向后兼容);
131
+ // 其余经 icons.ts 解析——Iconify 全名("集:名")或内置简名映射;
132
+ // 未命中跳过图标只渲染文字
133
+ const icon = lk.icon
134
+ ? lk.icon.startsWith("<")
135
+ ? { html: lk.icon }
136
+ : resolveThemeIcon(lk.icon)
137
+ : null;
138
+ return (
139
+ <a
140
+ href={lk.url}
141
+ target={external ? '_blank' : undefined}
142
+ rel={external ? 'noopener noreferrer' : undefined}
143
+ title={lk.name}
144
+ >
145
+ {icon?.name && (
146
+ <span class="profile-link-icon">
147
+ <Icon name={icon.name} />
148
+ </span>
149
+ )}
150
+ {icon?.html && <span class="profile-link-icon" set:html={icon.html} />}
151
+ {lk.showName && <span class="profile-link-name">{lk.name}</span>}
152
+ </a>
153
+ );
154
+ })}
155
+ </div>
156
+ )}
157
+ </section>
158
+ )}
159
+ {w.type === 'announcement' && showAnnouncement && (
160
+ <section class:list={["side-card", "card", "reveal", { "side-hide-m": w.hideOnMobile }]} id="announcement">
161
+ <h3>{ann.title}</h3>
162
+ <p class="side-announcement">{ann.content}</p>
163
+ {(annLink || ann.closable) && (
164
+ <div class="side-announcement-foot">
165
+ {annLink && (
166
+ <a
167
+ class="btn-ghost"
168
+ href={ann.link.url}
169
+ target={ann.link.external ? '_blank' : undefined}
170
+ rel={ann.link.external ? 'noopener noreferrer' : undefined}
171
+ >
172
+ {ann.link.text}
173
+ </a>
174
+ )}
175
+ {ann.closable && (
176
+ <button
177
+ class="side-close"
178
+ type="button"
179
+ aria-label={t('closeAnnouncement')}
180
+ onclick="this.closest('section').style.display='none'"
181
+ >
182
+ ✕
183
+ </button>
184
+ )}
185
+ </div>
186
+ )}
187
+ </section>
188
+ )}
189
+ {w.type === 'categories' && cats.length > 0 && (
190
+ <section class:list={["side-card", "card", "reveal", { "side-hide-m": w.hideOnMobile }]}>
191
+ <h3>{t('categories')}</h3>
192
+ <div class="cat-list">
193
+ {cats.map(([name, list]) => (
194
+ <a href={`/categories/${encodeURIComponent(name)}/`}>
195
+ <span>{name}</span>
196
+ <span class="count">{list.length}</span>
197
+ </a>
198
+ ))}
199
+ </div>
200
+ </section>
201
+ )}
202
+ {w.type === 'tags' && tags.length > 0 && (
203
+ <section class:list={["side-card", "card", "reveal", { "side-hide-m": w.hideOnMobile }]}>
204
+ <h3>{t('tags')}</h3>
205
+ <div class="side-tags">
206
+ {shownTags.map((tag) => (
207
+ <a class="chip" href={`/tags/${encodeURIComponent(tag)}/`}>
208
+ {tag}
209
+ </a>
210
+ ))}
211
+ {tagHidden > 0 && (
212
+ <a class="chip chip-more" href="/tags/">{t('tagsMore')}</a>
213
+ )}
214
+ </div>
215
+ </section>
216
+ )}
217
+ {w.type === 'friends' && sideFriends.length > 0 && (
218
+ <section class:list={["side-card", "card", "reveal", { "side-hide-m": w.hideOnMobile }]}>
219
+ <h3>{t('friendLinks')}</h3>
220
+ <div class="side-friends">
221
+ {sideFriends.map((f) => (
222
+ <a href={f.site} target="_blank" rel="noopener" title={f.title}>
223
+ {f.image ? (
224
+ <img src={f.image} alt={f.title} loading="lazy" />
225
+ ) : (
226
+ <span class="avatar-fallback">{f.title.slice(0, 1)}</span>
227
+ )}
228
+ <span>{f.title}</span>
229
+ </a>
230
+ ))}
231
+ </div>
232
+ </section>
233
+ )}
234
+ {w.type === 'calendar' && (
235
+ <Calendar
236
+ lang={config.lang}
237
+ postTooltip={w.specificConfig.postTooltip}
238
+ hideMobile={w.hideOnMobile}
239
+ />
240
+ )}
241
+ {w.type === 'toc' && isPost && config.toc.enabled && tocItems.length > 0 && (
242
+ <section class:list={["side-card", "card", "reveal", "toc-widget", { "side-hide-m": w.hideOnMobile }]}>
243
+ <h3>{t('tocTitle')}</h3>
244
+ <nav class="side-toc">
245
+ {tocItems.map((h) => (
246
+ <a class={`toc-depth-${h.depth}`} href={`#${h.slug}`}>
247
+ {h.text}
248
+ </a>
249
+ ))}
250
+ </nav>
251
+ </section>
252
+ )}
253
+ {w.type === 'siteInfo' && (
254
+ <section class:list={["side-card", "card", "reveal", { "side-hide-m": w.hideOnMobile }]}>
255
+ <h3>{t('siteInfo')}</h3>
256
+ <div class="site-info">
257
+ <div class="row"><span>{t('posts')}</span><b>{posts.length}</b></div>
258
+ <div class="row"><span>{t('categories')}</span><b>{cats.length}</b></div>
259
+ <div class="row"><span>{t('tags')}</span><b>{tags.length}</b></div>
260
+ {runDays !== null && <div class="row"><span>{t('running')}</span><b>{t('daysCount', { n: runDays })}</b></div>}
261
+ </div>
262
+ </section>
263
+ )}
264
+ {w.type === 'advertisement' && showAd && (
265
+ <section class:list={["side-card", "card", "reveal", "advertisement-widget", { "side-hide-m": w.hideOnMobile }]} id="advertisement">
266
+ <h3>{ad.title}</h3>
267
+ {ad.closable && (
268
+ <button
269
+ class="ad-close"
270
+ type="button"
271
+ aria-label={t('closeAd')}
272
+ onclick="window.__orchidCloseAd && window.__orchidCloseAd(this)"
273
+ >
274
+ ✕
275
+ </button>
276
+ )}
277
+ <div class="ad-body">
278
+ {ad.image &&
279
+ (ad.imageLink ? (
280
+ <a href={ad.imageLink} target="_blank" rel="noopener">
281
+ <img src={ad.image} alt={ad.imageAlt} loading="lazy" />
282
+ </a>
283
+ ) : (
284
+ <img src={ad.image} alt={ad.imageAlt} loading="lazy" />
285
+ ))}
286
+ {ad.content && <p>{ad.content}</p>}
287
+ {ad.link.text && (
288
+ <a
289
+ class="btn-ghost"
290
+ href={ad.link.url || '#'}
291
+ target={ad.link.external ? '_blank' : undefined}
292
+ rel={ad.link.external ? 'noopener noreferrer' : undefined}
293
+ >
294
+ {ad.link.text}
295
+ </a>
296
+ )}
297
+ </div>
298
+ </section>
299
+ )}
300
+ </>
301
+ ))}
302
+ </div>
303
+ ))
304
+ }
305
+ </aside>
306
+
307
+ <!-- 广告初始化:置于顶层(条件表达式内无法放脚本体);data-astro-rerun 保证 VT 换页后重绑/重计数。
308
+ displayCount>0 时按 localStorage 计数,超限隐藏当前页的广告卡 -->
309
+ <script is:inline data-astro-rerun define:vars={{ adEnabled: showAd, displayCount: ad.displayCount }}>
310
+ (function () {
311
+ window.__orchidCloseAd = function (btn) {
312
+ var wrapper = btn.closest('.advertisement-widget');
313
+ if (!wrapper) return;
314
+ wrapper.style.transition = 'all .3s ease-out';
315
+ wrapper.style.transform = 'translateX(24px)';
316
+ wrapper.style.opacity = '0';
317
+ setTimeout(function () {
318
+ wrapper.style.display = 'none';
319
+ }, 300);
320
+ };
321
+ if (!adEnabled) return;
322
+ var widget = document.getElementById('advertisement');
323
+ if (!widget) return;
324
+ if (displayCount > 0) {
325
+ var key = 'ad-display-advertisement';
326
+ var n = parseInt(localStorage.getItem(key) || '0', 10);
327
+ if (n >= displayCount) {
328
+ widget.style.display = 'none';
329
+ return;
330
+ }
331
+ localStorage.setItem(key, String(n + 1));
332
+ }
333
+ })();
334
+ </script>
@@ -0,0 +1,27 @@
1
+ ---
2
+ import { t } from "../i18n/index.js";
3
+
4
+ export interface TocHeading {
5
+ depth: number;
6
+ slug: string;
7
+ text: string;
8
+ }
9
+
10
+ interface Props {
11
+ headings: TocHeading[];
12
+ }
13
+
14
+ const { headings } = Astro.props;
15
+ const items = headings.filter((h) => h.depth >= 2 && h.depth <= 4);
16
+ ---
17
+
18
+ {
19
+ items.length > 0 && (
20
+ <aside class="toc" aria-label={t('toc')}>
21
+ <div class="toc-title">{t('tocTitle')}</div>
22
+ {items.map((h) => (
23
+ <a href={`#${h.slug}`}>{h.text}</a>
24
+ ))}
25
+ </aside>
26
+ )
27
+ }
@@ -0,0 +1,22 @@
1
+ ---
2
+ /**
3
+ * Google Analytics(gtag.js)。orchid-blog analytics/GoogleAnalytics.astro 移植,
4
+ * 去掉 swup 专属 data-swup-ignore-script(主题用 ClientRouter,无需该标记)。
5
+ */
6
+ interface Props {
7
+ analyticsId: string;
8
+ }
9
+
10
+ const { analyticsId } = Astro.props;
11
+ ---
12
+
13
+ <!-- Google tag (gtag.js) -->
14
+ <script is:inline async src={`https://www.googletagmanager.com/gtag/js?id=${analyticsId}`}></script>
15
+ <script is:inline define:vars={{ analyticsId }}>
16
+ window.dataLayer = window.dataLayer || [];
17
+ function gtag() {
18
+ dataLayer.push(arguments);
19
+ }
20
+ gtag('js', new Date());
21
+ gtag('config', analyticsId);
22
+ </script>
@@ -0,0 +1,52 @@
1
+ ---
2
+ /**
3
+ * 51.la 统计(js-sdk-pro)。orchid-blog analytics/La51Analytics.astro 移植(去 swup 标记)。
4
+ */
5
+ interface Props {
6
+ analyticsId: string;
7
+ sdkUrl?: string;
8
+ ck?: string;
9
+ autoTrack?: boolean;
10
+ hashMode?: boolean;
11
+ screenRecord?: boolean;
12
+ }
13
+
14
+ const {
15
+ analyticsId,
16
+ sdkUrl,
17
+ ck,
18
+ autoTrack = true,
19
+ hashMode = false,
20
+ screenRecord = true,
21
+ } = Astro.props;
22
+ ---
23
+
24
+ <script is:inline define:vars={{ analyticsId, sdkUrl, ck, autoTrack, hashMode, screenRecord }}>
25
+ !(function (p) {
26
+ 'use strict';
27
+ !(function () {
28
+ var s = window,
29
+ e = document,
30
+ i = p,
31
+ c = sdkUrl || ''.concat(
32
+ 'https:' === e.location.protocol ? 'https://' : 'http://',
33
+ 'sdk.51.la/js-sdk-pro.min.js'
34
+ ),
35
+ n = e.createElement('script'),
36
+ r = e.getElementsByTagName('script')[0];
37
+ (n.type = 'text/javascript'),
38
+ n.setAttribute('charset', 'UTF-8'),
39
+ (n.async = !0),
40
+ (n.src = c),
41
+ (n.id = 'LA_COLLECT'),
42
+ (i.d = n);
43
+ var o = function () {
44
+ s.LA.ids.push(i);
45
+ };
46
+ s.LA
47
+ ? s.LA.ids && o()
48
+ : ((s.LA = p), (s.LA.ids = []), o()),
49
+ r.parentNode.insertBefore(n, r);
50
+ })();
51
+ })({ id: analyticsId, ck: ck || analyticsId, autoTrack: autoTrack, hashMode: hashMode, screenRecord: screenRecord });
52
+ </script>
@@ -0,0 +1,18 @@
1
+ ---
2
+ /**
3
+ * Microsoft Clarity。orchid-blog analytics/MicrosoftClarity.astro 移植(去 swup 标记)。
4
+ */
5
+ interface Props {
6
+ clarityId: string;
7
+ }
8
+
9
+ const { clarityId } = Astro.props;
10
+ ---
11
+
12
+ <script is:inline define:vars={{ clarityId }}>
13
+ (function (c, l, a, r, i, t, y) {
14
+ c[a] = c[a] || function () { (c[a].q = c[a].q || []).push(arguments) };
15
+ t = l.createElement(r); t.async = 1; t.src = 'https://www.clarity.ms/tag/' + i;
16
+ y = l.getElementsByTagName(r)[0]; y.parentNode.insertBefore(t, y);
17
+ })(window, document, 'clarity', 'script', clarityId);
18
+ </script>
@@ -0,0 +1,86 @@
1
+ ---
2
+ /**
3
+ * Umami 统计。orchid-blog analytics/UmamiAnalytics.astro 移植(去 swup 标记)。
4
+ * 注意:所有 is:inline 脚本必须置于模板顶层(JSX 条件表达式内的脚本体
5
+ * 会被模板编译器按表达式解析,{}/箭头函数会编译失败);
6
+ * 开关经 define:vars 传入,脚本内自行短路。
7
+ * 外链追踪用 document 级 astro:page-load 监听,VT 换页后自动重扫新 DOM 外链。
8
+ */
9
+ interface Props {
10
+ websiteId: string;
11
+ scriptUrl?: string;
12
+ replaysScriptUrl?: string;
13
+ trackOutboundLinks?: boolean;
14
+ collectWebVitals?: boolean;
15
+ replays?: {
16
+ enabled?: boolean;
17
+ sampleRate?: number;
18
+ maskLevel?: "moderate" | "strict";
19
+ maxDuration?: number;
20
+ blockSelector?: string;
21
+ };
22
+ }
23
+
24
+ const {
25
+ websiteId,
26
+ scriptUrl,
27
+ replaysScriptUrl,
28
+ trackOutboundLinks = true,
29
+ collectWebVitals = false,
30
+ replays,
31
+ } = Astro.props;
32
+
33
+ const replaysEnabled = replays?.enabled ?? false;
34
+ const replaysSampleRate = replays?.sampleRate ?? 0.15;
35
+ const replaysMaskLevel = replays?.maskLevel ?? "moderate";
36
+ const replaysMaxDuration = replays?.maxDuration ?? 300000;
37
+ const replaysBlockSelector = replays?.blockSelector?.trim();
38
+ ---
39
+
40
+ <script
41
+ is:inline
42
+ defer
43
+ src={scriptUrl}
44
+ data-website-id={websiteId}
45
+ data-performance={collectWebVitals ? 'true' : undefined}
46
+ ></script>
47
+
48
+ {
49
+ replaysEnabled && (
50
+ <script
51
+ is:inline
52
+ defer
53
+ src={replaysScriptUrl}
54
+ data-website-id={websiteId}
55
+ data-sample-rate={String(replaysSampleRate)}
56
+ data-mask-level={replaysMaskLevel}
57
+ data-max-duration={String(replaysMaxDuration)}
58
+ data-block-selector={replaysBlockSelector || undefined}
59
+ />
60
+ )
61
+ }
62
+
63
+ <script is:inline define:vars={{ outboundEnabled: trackOutboundLinks }}>
64
+ (function () {
65
+ if (!outboundEnabled) return;
66
+ var name = 'outbound-link-click';
67
+
68
+ function applyOutboundTracking() {
69
+ document.querySelectorAll('a').forEach(function (a) {
70
+ if (a.host !== window.location.host && !a.getAttribute('data-umami-event')) {
71
+ a.setAttribute('data-umami-event', name);
72
+ a.setAttribute('data-umami-event-url', a.href);
73
+ }
74
+ });
75
+ }
76
+
77
+ if (document.readyState === 'loading') {
78
+ document.addEventListener('DOMContentLoaded', applyOutboundTracking, { once: true });
79
+ } else {
80
+ applyOutboundTracking();
81
+ }
82
+
83
+ // VT 换页后重扫新 DOM 中的外链
84
+ document.addEventListener('astro:page-load', applyOutboundTracking);
85
+ })();
86
+ </script>
@@ -0,0 +1,51 @@
1
+ ---
2
+ /**
3
+ * Artalk 评论实现(orchid-blog components/comment/Artalk.astro 移植)。
4
+ * pageKey 与 orchid-blog 口径一致(/posts/<id> 无尾斜杠),迁移后既有评论数据无缝延续。
5
+ * VT 兼容:identical inline 脚本换页后不会重跑(按内容去重),必须显式
6
+ * data-astro-rerun——否则 guestbook → 其他页 → guestbook(pageKey 相同、脚本内容相同)
7
+ * 二次进入时脚本不重执行,评论区空白。旧 MutationObserver 经 window 句柄清理。
8
+ */
9
+ import config from "virtual:theme/config";
10
+
11
+ interface Props {
12
+ path: string;
13
+ }
14
+
15
+ const { path } = Astro.props;
16
+
17
+ const artalkConfig = {
18
+ el: "#artalk",
19
+ server: config.comment.artalk.server,
20
+ site: config.title,
21
+ pageKey: path,
22
+ pageTitle: "",
23
+ locale: config.comment.artalk.locale,
24
+ dark: "html.dark",
25
+ ...(config.comment.artalk.visitorCount ? { pageview: true } : {}),
26
+ };
27
+ ---
28
+
29
+ <div class="relative w-full">
30
+ <div id="artalk" style="--at-color-main: var(--accent); --at-color-bg: var(--surface); --at-color-border: var(--border);"></div>
31
+
32
+ <link rel="stylesheet" href="https://unpkg.com/artalk/dist/Artalk.css" />
33
+
34
+ <script is:inline type="module" data-astro-rerun define:vars={{ artalkConfig }}>
35
+ const ArtalkModule = await import('https://unpkg.com/artalk/dist/Artalk.mjs');
36
+
37
+ // VT 换页后旧观察器随本次执行清理,避免泄漏
38
+ if (window.__orchidArtalkObserver) window.__orchidArtalkObserver.disconnect();
39
+
40
+ const artalk = ArtalkModule.default.init(artalkConfig);
41
+
42
+ function updateArtalkTheme() {
43
+ artalk.setDarkMode(document.documentElement.classList.contains('dark'));
44
+ }
45
+ updateArtalkTheme();
46
+
47
+ const observer = new MutationObserver(updateArtalkTheme);
48
+ observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
49
+ window.__orchidArtalkObserver = observer;
50
+ </script>
51
+ </div>
@@ -0,0 +1,43 @@
1
+ ---
2
+ /**
3
+ * Disqus 评论实现(orchid-blog components/comment/Disqus.astro 移植)。
4
+ * identifier 口径与 orchid-blog 一致:post id(如 posts/<id>)、guestbook,
5
+ * 迁移后既有评论数据无缝延续;url = 站点域名 + path。
6
+ * VT 兼容:data-astro-rerun 重执行,注入前移除旧 embed 脚本防重复加载。
7
+ */
8
+ import config from "virtual:theme/config";
9
+
10
+ interface Props {
11
+ path: string;
12
+ /** 页面标题(Disqus 后台按 title 展示) */
13
+ title?: string;
14
+ }
15
+
16
+ const { path, title } = Astro.props;
17
+ const shortname = config.comment.disqus.shortname;
18
+ const siteHref = Astro.site?.href ?? "/";
19
+ ---
20
+
21
+ <div id="disqus_thread"></div>
22
+
23
+ <script is:inline data-astro-rerun define:vars={{ shortname, path, title, siteHref }}>
24
+ window.disqus_config = function () {
25
+ this.page.url = siteHref.replace(/\/$/, "") + path;
26
+ // 与 orchid-blog 口径一致:去首尾斜杠的 path(posts/<id>、guestbook)
27
+ this.page.identifier = path.replace(/^\/+|\/+$/g, "") || "index";
28
+ this.page.title = title || path;
29
+ };
30
+
31
+ // 换页重跑防护:移除旧 embed 脚本再注入
32
+ const old = document.getElementById("qy-disqus-embed");
33
+ if (old) old.remove();
34
+ const s = document.createElement("script");
35
+ s.id = "qy-disqus-embed";
36
+ s.src = `https://${shortname}.disqus.com/embed.js`;
37
+ s.setAttribute("data-timestamp", new Date().toString());
38
+ (document.head || document.body).appendChild(s);
39
+ </script>
40
+
41
+ <noscript>
42
+ 请启用 JavaScript 以查看 <a href="https://disqus.com/?ref_noscript">Disqus 评论</a>。
43
+ </noscript>