@xingwangzhe/stalux 1.24.7 → 1.25.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xingwangzhe/stalux",
3
- "version": "1.24.7",
3
+ "version": "1.25.0",
4
4
  "description": "A powerful, modern Astro blog theme — use as template or install as plugin",
5
5
  "keywords": [
6
6
  "astro",
@@ -1,25 +1,4 @@
1
1
  ---
2
- import { getCollection } from "astro:content";
3
- import { getHeadData } from "@utils/config-utils";
4
- const configEntries = await getCollection("config");
5
- const headConfig = getHeadData(configEntries);
6
- const id = headConfig?.bingClarityId;
7
2
  ---
8
3
 
9
- <script is:inline data-astro-rerun define:vars={{ id }}>
10
- // 防止重复初始化
11
- const load = () => {
12
- if (!id || document.getElementById("stalux-clarity-script") || window.clarity) return;
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);
16
- t.id = "stalux-clarity-script";
17
- t.async = 1;
18
- t.src = "https://www.clarity.ms/tag/" + i;
19
- y = l.getElementsByTagName(r)[0];
20
- y.parentNode.insertBefore(t, y);
21
- })(window, document, "clarity", "script", id);
22
- };
23
- if ("requestIdleCallback" in window) requestIdleCallback(load, { timeout: 4000 });
24
- else window.addEventListener("load", () => setTimeout(load, 0), { once: true });
25
- </script>
4
+ <script src="../../../scripts/clarity.ts"></script>
@@ -1,34 +1,4 @@
1
1
  ---
2
- import { getCollection } from "astro:content";
3
- import { getHeadData } from "@utils/config-utils";
4
- const configEntries = await getCollection("config");
5
- const headConfig = getHeadData(configEntries);
6
- const gaID = headConfig?.googleAnalyticsId;
7
2
  ---
8
3
 
9
- <script is:inline define:vars={{ gaID }}>
10
- (() => {
11
- const load = () => {
12
- if (window.__staluxAnalyticsLoaded) return;
13
- window.__staluxAnalyticsLoaded = true;
14
- const script = document.createElement("script");
15
- script.async = true;
16
- script.src = `https://www.googletagmanager.com/gtag/js?id=${gaID}`;
17
- document.head.appendChild(script);
18
- window.dataLayer = window.dataLayer || [];
19
- window.gtag = window.gtag || function () {
20
- window.dataLayer.push(arguments);
21
- };
22
- window.gtag("js", new Date());
23
- window.gtag("config", gaID);
24
- if (!window.__gtagListenerAdded) {
25
- document.addEventListener("astro:page-load", () => {
26
- window.gtag("event", "page_view", { page_path: window.location.pathname });
27
- });
28
- window.__gtagListenerAdded = true;
29
- }
30
- };
31
- if ("requestIdleCallback" in window) requestIdleCallback(load, { timeout: 3000 });
32
- else window.addEventListener("load", () => setTimeout(load, 0), { once: true });
33
- })();
34
- </script>
4
+ <script src="../../../scripts/google-analytics.ts"></script>
@@ -2,15 +2,4 @@
2
2
 
3
3
  ---
4
4
 
5
- <script is:inline data-astro-rerun>
6
- (() => {
7
- const load = () => {
8
- const script = document.createElement("script");
9
- script.defer = true;
10
- script.src = "https://events.vercount.one/js";
11
- document.head.appendChild(script);
12
- };
13
- if ("requestIdleCallback" in window) requestIdleCallback(load, { timeout: 5000 });
14
- else window.addEventListener("load", () => setTimeout(load, 0), { once: true });
15
- })();
16
- </script>
5
+ <script src="../../../scripts/vercount.ts"></script>
@@ -41,7 +41,7 @@ const { showStats = true, wordCount, buildtime } = Astro.props;
41
41
 
42
42
  {
43
43
  buildtime && (
44
- <div class="site-build-time">
44
+ <div class="site-build-time" data-stalux-buildtime={buildtime} data-stalux-runtime-format={t("footer.runtimeFormat")}>
45
45
  <span>
46
46
  {t("footer.runningTime")}
47
47
  <span id="runtime-counter" />
@@ -50,41 +50,7 @@ const { showStats = true, wordCount, buildtime } = Astro.props;
50
50
  )
51
51
  }
52
52
 
53
- <script define:vars={{ buildtime, runtimeFormat: t("footer.runtimeFormat") }}>
54
- document.addEventListener("astro:page-load", () => {
55
- if (!buildtime) return;
56
- const runtimeCounter = document.getElementById("runtime-counter");
57
- if (!runtimeCounter) return;
58
-
59
- const updateRuntime = () => {
60
- const now = new Date();
61
- const start = new Date(buildtime);
62
- const diffMs = now.getTime() - start.getTime();
63
- if (Number.isNaN(diffMs) || diffMs < 0) return;
64
-
65
- const totalSeconds = Math.floor(diffMs / 1000);
66
- const days = Math.floor(totalSeconds / 86400);
67
- const hours = Math.floor((totalSeconds % 86400) / 3600);
68
- const minutes = Math.floor((totalSeconds % 3600) / 60);
69
- const seconds = totalSeconds % 60;
70
-
71
- runtimeCounter.textContent = runtimeFormat
72
- .replace("{days}", String(days))
73
- .replace("{hours}", String(hours))
74
- .replace("{minutes}", String(minutes))
75
- .replace("{seconds}", String(seconds));
76
- };
77
-
78
- updateRuntime();
79
- // define:vars 内联脚本会在每次视图切换时重新执行,
80
- // 因此用 window 句柄记录定时器,创建新定时器前先清掉旧的,避免累加 1Hz 定时器。
81
- const w = window;
82
- if (w.__staluxRuntimeTimer != null) {
83
- clearInterval(w.__staluxRuntimeTimer);
84
- }
85
- w.__staluxRuntimeTimer = setInterval(updateRuntime, 1000);
86
- });
87
- </script>
53
+ <script src="../../../scripts/runtime-counter.ts"></script>
88
54
 
89
55
  <style>
90
56
  .site-stats {
@@ -269,7 +269,7 @@ if (isArticle) {
269
269
  ],
270
270
  };
271
271
  }
272
- const ldJson = JSON.stringify(schema, null, 2).replace(/<\//g, "<\\/");
272
+ const ldJson = JSON.stringify(schema).replace(/<\//g, "<\\/");
273
273
  ---
274
274
 
275
275
  <head>
@@ -3,132 +3,23 @@ import style from "@styles/components/posts/randomPost.module.css";
3
3
  import { getCollection } from "astro:content";
4
4
  import { createTranslator } from "@utils/i18n";
5
5
 
6
- interface Props {
7
- count?: number;
8
- title?: string;
9
- }
10
-
6
+ interface Props { count?: number; title?: string; }
11
7
  const configEntries = await getCollection("config");
12
- const siteConfig = (configEntries.find(e => e.id === "site")?.data ?? {}) as Record<string, unknown>;
8
+ const siteConfig = (configEntries.find((entry) => entry.id === "site")?.data ?? {}) as Record<string, unknown>;
13
9
  const { t } = createTranslator((siteConfig.lang as string) || "zh-CN");
14
10
  const { count = 5, title = t("randomPost.title") } = Astro.props;
15
11
  ---
16
-
17
- <aside class={style.container} transition:persist-props aria-label={t("randomPost.title")}>
12
+ <aside class={style.container} transition:persist-props aria-label={t("randomPost.title")} data-stalux-random-posts data-count={count} data-no-posts={t("randomPost.noPosts")} data-load-failed={t("randomPost.loadFailed")} data-load-error={t("randomPost.loadError")}>
18
13
  <div class={style.header}>
19
14
  <h3 class={style.title}>{title}</h3>
20
- <button
21
- id="refresh-random-posts"
22
- class={style.refreshButton}
23
- title={t("randomPost.refresh")}
24
- aria-label={t("randomPost.refresh")}
25
- >
26
- <svg
27
- xmlns="http://www.w3.org/2000/svg"
28
- width="16"
29
- height="16"
30
- viewBox="0 0 24 24"
31
- fill="none"
32
- stroke="currentColor"
33
- stroke-width="2"
34
- stroke-linecap="round"
35
- stroke-linejoin="round"
36
- >
37
- <path
38
- d="M21.5 2v6h-6M2.5 22v-6h6M2 11.5a10 10 0 0 1 18.8-4.3M22 12.5a10 10 0 0 1-18.8 4.2"
39
- ></path>
15
+ <button id="refresh-random-posts" class={style.refreshButton} title={t("randomPost.refresh")} aria-label={t("randomPost.refresh")}>
16
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
17
+ <path d="M21.5 2v6h-6M2.5 22v-6h6M2 11.5a10 10 0 0 1 18.8-4.3M22 12.5a10 10 0 0 1-18.8 4.2"></path>
40
18
  </svg>
41
19
  </button>
42
20
  </div>
43
21
  <ul class={style.list} id="random-posts-list">
44
22
  <li class={style.loading}>{t("randomPost.loading")}</li>
45
- </ul>
46
- </aside>
47
-
48
- <script define:vars={{ count, labels: { noPosts: t("randomPost.noPosts"), loadFailed: t("randomPost.loadFailed"), loadError: t("randomPost.loadError") } }}>
49
- const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
50
-
51
- // 费雪-耶茨洗牌(crypto 优先,回退 Math.random)
52
- const shuffleArray = (array) => {
53
- const shuffled = [...array];
54
- const randomIndex = (max) => {
55
- if (globalThis.crypto?.getRandomValues) {
56
- const buf = new Uint32Array(1);
57
- crypto.getRandomValues(buf);
58
- return buf[0] % max;
59
- }
60
- return Math.floor(Math.random() * max);
61
- };
62
- for (let i = shuffled.length - 1; i > 0; i--) {
63
- const j = randomIndex(i + 1);
64
- [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
65
- }
66
- return shuffled;
67
- };
68
-
69
- const fetchRandomPosts = async () => {
70
- try {
71
- const response = await fetch("/api/post.abbrlink.json");
72
- if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
73
- return (await response.json()) ?? [];
74
- } catch (error) {
75
- console.error("Failed to fetch posts:", error);
76
- return [];
77
- }
78
- };
79
-
80
- const generatePostsHTML = (posts) => {
81
- if (posts.length === 0) {
82
- return `<li class="no-posts">${labels.noPosts}</li>`;
83
- }
84
- return posts
85
- .map(
86
- (post) => `
87
- <li class="random-post-item">
88
- <a href="/posts/${post.abbrlink}/" class="random-post-link a-none">
89
- ${post.title}
90
- </a>
91
- </li>
92
- `,
93
- )
94
- .join("");
95
- };
96
-
97
- const renderRandomPosts = async (listEl, emptyMsg, errorMsg) => {
98
- try {
99
- const allPosts = await fetchRandomPosts();
100
- if (allPosts.length === 0) {
101
- listEl.innerHTML = `<li class="no-posts">${emptyMsg}</li>`;
102
- return;
103
- }
104
- listEl.innerHTML = generatePostsHTML(shuffleArray(allPosts).slice(0, count));
105
- } catch (error) {
106
- console.error("Error rendering random posts:", error);
107
- listEl.innerHTML = `<li class="error">${errorMsg}</li>`;
108
- }
109
- };
110
-
111
- const refreshRandomPosts = async () => {
112
- const randomPostsList = document.getElementById("random-posts-list");
113
- if (!randomPostsList) return;
114
-
115
- randomPostsList.style.opacity = "0.5";
116
- await sleep(300);
117
- await renderRandomPosts(randomPostsList, labels.noPosts, labels.loadFailed);
118
- randomPostsList.style.opacity = "1";
119
- };
120
-
121
- document.addEventListener("astro:page-load", () => {
122
- const refreshButton = document.getElementById("refresh-random-posts");
123
- refreshButton?.addEventListener("click", async (e) => {
124
- e.preventDefault();
125
- refreshButton.classList.add("rotating");
126
- await refreshRandomPosts();
127
- await sleep(600);
128
- refreshButton.classList.remove("rotating");
129
- });
130
-
131
- const list = document.getElementById("random-posts-list");
132
- if (list) void renderRandomPosts(list, labels.noPosts, labels.loadError);
133
- });
134
- </script>
23
+ </ul>
24
+ </aside>
25
+ <script src="../../../scripts/random-posts.ts"></script>
package/src/index.ts CHANGED
@@ -8,7 +8,14 @@
8
8
  * 基于 Astro 7.1.3 Integration API(astro:config:setup / injectRoute / injectScript)
9
9
  */
10
10
 
11
- import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync } from "node:fs";
11
+ import {
12
+ copyFileSync,
13
+ existsSync,
14
+ mkdirSync,
15
+ readFileSync,
16
+ readdirSync,
17
+ writeFileSync,
18
+ } from "node:fs";
12
19
  import path from "node:path";
13
20
  import { fileURLToPath } from "node:url";
14
21
 
@@ -28,6 +35,34 @@ import { runFontSlicing, type FontSlice } from "./internal/font-slices";
28
35
  import { featureFlagsHast, featureFlagsMdast } from "./plugins/feature-flags";
29
36
  import { temml } from "./plugins/satteri-temml";
30
37
 
38
+ function stripGeneratedHtmlComments(outDir: string): void {
39
+ const walk = (dir: string) => {
40
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
41
+ const target = path.join(dir, entry.name);
42
+ if (entry.isDirectory()) walk(target);
43
+ else if (entry.isFile() && entry.name.endsWith(".html")) {
44
+ const source = readFileSync(target, "utf8");
45
+ const protectedBlocks: string[] = [];
46
+ const protectedSource = source.replace(
47
+ /<(pre|code|textarea)\b[\s\S]*?<\/\1>/gi,
48
+ (block) => {
49
+ protectedBlocks.push(block);
50
+ return `__STALUX_PROTECTED_HTML_${protectedBlocks.length - 1}__`;
51
+ },
52
+ );
53
+ const stripped = protectedSource
54
+ .replace(/<!--(?!\[if)[\s\S]*?-->/g, "")
55
+ .replace(
56
+ /__STALUX_PROTECTED_HTML_(\d+)__/g,
57
+ (_, index: string) => protectedBlocks[Number(index)] ?? "",
58
+ );
59
+ if (stripped !== source) writeFileSync(target, stripped);
60
+ }
61
+ }
62
+ };
63
+ walk(outDir);
64
+ }
65
+
31
66
  // 字体配置(Astro Fonts API)通过 updateConfig 注入,两种模式(源码模板/npm 插件)都生效。
32
67
  // 官方 local provider 完全本地读文件(readFile),不联网;
33
68
  // 7.2.2 已修复 Fonts API 随机端口污染 incrementalBuild dependencyHash 的问题(PR #17659)。
@@ -438,9 +473,12 @@ export function stalux(options: StaluxOptions = {}): AstroIntegration[] {
438
473
  },
439
474
 
440
475
  "astro:build:done": async ({ dir, logger }) => {
476
+ const outDir = fileURLToPath(dir);
477
+ stripGeneratedHtmlComments(outDir);
478
+ logger.info("Removed generated HTML comments (preserved pre/code/textarea blocks)");
479
+
441
480
  // 6. 后处理:Pagefind 搜索索引
442
481
  if (opt.pagefind) {
443
- const outDir = fileURLToPath(dir);
444
482
  logger.info("Running Pagefind indexer...");
445
483
 
446
484
  try {
@@ -5,7 +5,7 @@ import { getCollection } from "astro:content";
5
5
  import "@styles/base/init.css";
6
6
  import "@styles/shared/stagger.css";
7
7
  import "@styles/shared/view-transitions.css";
8
- import { getAuthorData, getAiDiscoveryData, getPromoteData, getSiteData } from "@utils/config-utils";
8
+ import { getAuthorData, getAiDiscoveryData, getHeadData, getPromoteData, getSiteData } from "@utils/config-utils";
9
9
  import { getRouteAnimation, getRouteKind } from "@utils/view-transitions";
10
10
  const props = Astro.props as Props;
11
11
 
@@ -25,6 +25,7 @@ interface Props {
25
25
  const configEntries = await getCollection("config");
26
26
  const siteConfig = getSiteData(configEntries);
27
27
  const authorConfig = getAuthorData(configEntries);
28
+ const headConfig = getHeadData(configEntries);
28
29
  const promoteConfig = getPromoteData(configEntries);
29
30
  const aiConfig = getAiDiscoveryData(configEntries);
30
31
  const htmlLang = siteConfig.lang || "zh-CN";
@@ -65,21 +66,6 @@ const routeAnimation = getRouteAnimation(routeKind);
65
66
  {
66
67
  aiConfig?.conformance !== "disabled" && (
67
68
  <>
68
- <script
69
- define:vars={{
70
- siteTitle: siteConfig.title,
71
- siteUrl: siteConfig.url,
72
- siteDesc: siteConfig.description,
73
- siteLang: siteConfig.lang,
74
- }}
75
- >
76
- window.__STALUX_SITE_INFO__ = {
77
- title: siteTitle,
78
- url: siteUrl,
79
- description: siteDesc,
80
- lang: siteLang,
81
- };
82
- </script>
83
69
  <script>
84
70
  import "../scripts/webmcp.ts";
85
71
  </script>
@@ -90,8 +76,18 @@ const routeAnimation = getRouteAnimation(routeKind);
90
76
  class="stalux-root dark"
91
77
  data-stalux-bg-layer="a"
92
78
  data-stalux-bg-index="0"
79
+ data-stalux-site-title={siteConfig.title}
80
+ data-stalux-site-url={siteConfig.url}
81
+ data-stalux-site-description={siteConfig.description}
82
+ data-stalux-site-lang={siteConfig.lang}
83
+ data-stalux-ga-id={headConfig?.googleAnalyticsId}
84
+ data-stalux-clarity-id={headConfig?.bingClarityId}
93
85
  >
94
- {llmPromote && <div style="display:none">{llmPromote}</div>}
86
+ {llmPromote && (
87
+ <section class="stalux-ai-prompt" data-stalux-ai-prompt aria-label="AI site context">
88
+ <p>{llmPromote}</p>
89
+ </section>
90
+ )}
95
91
  <div
96
92
  class="bg-layer bg-a"
97
93
  style="opacity: 1;"
@@ -116,3 +112,17 @@ const routeAnimation = getRouteAnimation(routeKind);
116
112
  </html>
117
113
 
118
114
  <style></style>
115
+
116
+ <style>
117
+ .stalux-ai-prompt {
118
+ position: absolute;
119
+ width: 1px;
120
+ height: 1px;
121
+ padding: 0;
122
+ margin: -1px;
123
+ overflow: hidden;
124
+ clip: rect(0, 0, 0, 0);
125
+ white-space: pre-wrap;
126
+ border: 0;
127
+ }
128
+ </style>
@@ -21,42 +21,12 @@ const { t } = createTranslator(siteConfig.lang || "zh-CN");
21
21
  <div class="error-content">
22
22
  <h1>404</h1>
23
23
  <p>{t("page404.pageNotFound")}</p>
24
- <p id="countdown" class="countdown-text">{t("page404.countdown", { n: 5 })}</p>
24
+ <p id="countdown" class="countdown-text" data-countdown-template={t("page404.countdown", { n: "{n}" })}>{t("page404.countdown", { n: 5 })}</p>
25
25
  <a href="/" class="home-btn" data-astro-prefetch>{t("page404.backHome")}</a>
26
26
  </div>
27
27
  </div>
28
28
 
29
- <script define:vars={{ countdownText: t("page404.countdown", { n: "{n}" }) }} is:inline>
30
- let timer;
31
-
32
- function startCountdown() {
33
- if (timer !== undefined) window.clearInterval(timer);
34
- let seconds = 5;
35
- const el = document.getElementById("countdown");
36
- if (!el) return;
37
-
38
- timer = window.setInterval(() => {
39
- seconds--;
40
- if (seconds <= 0) {
41
- if (timer !== undefined) window.clearInterval(timer);
42
- timer = undefined;
43
- const home = document.querySelector(".home-btn");
44
- home?.click();
45
- } else {
46
- el.textContent = countdownText.replace("{n}", String(seconds));
47
- }
48
- }, 1000);
49
- }
50
-
51
- function stopCountdown() {
52
- if (timer !== undefined) window.clearInterval(timer);
53
- timer = undefined;
54
- }
55
-
56
- document.addEventListener("astro:before-swap", stopCountdown);
57
- document.addEventListener("astro:page-load", startCountdown);
58
- startCountdown();
59
- </script>
29
+ <script src="../scripts/countdown.ts"></script>
60
30
  </Stalux>
61
31
 
62
32
  <style>
@@ -0,0 +1,26 @@
1
+ declare global {
2
+ interface Window {
3
+ clarity?: ((...args: unknown[]) => void) & { q?: unknown[][] };
4
+ __staluxClarityLoaded?: boolean;
5
+ }
6
+ }
7
+
8
+ function load() {
9
+ const id = document.body?.dataset.staluxClarityId;
10
+ if (!id || document.getElementById("stalux-clarity-script")) return;
11
+ window.__staluxClarityLoaded = true;
12
+ window.clarity =
13
+ window.clarity ||
14
+ Object.assign((...args: unknown[]) => window.clarity?.q?.push(args), { q: [] });
15
+ const script = document.createElement("script");
16
+ script.id = "stalux-clarity-script";
17
+ script.async = true;
18
+ script.src = `https://www.clarity.ms/tag/${encodeURIComponent(id)}`;
19
+ document.head.appendChild(script);
20
+ }
21
+
22
+ document.addEventListener("astro:page-load", load);
23
+ if (document.readyState === "loading")
24
+ document.addEventListener("DOMContentLoaded", load, { once: true });
25
+ else load();
26
+ export {};
@@ -0,0 +1,27 @@
1
+ let timer: number | undefined;
2
+
3
+ function stop() {
4
+ if (timer !== undefined) window.clearInterval(timer);
5
+ timer = undefined;
6
+ }
7
+
8
+ function start() {
9
+ stop();
10
+ const el = document.getElementById("countdown");
11
+ const template = el?.dataset.countdownTemplate;
12
+ if (!el || !template) return;
13
+ let seconds = 5;
14
+ timer = window.setInterval(() => {
15
+ seconds -= 1;
16
+ if (seconds <= 0) {
17
+ stop();
18
+ document.querySelector<HTMLAnchorElement>(".home-btn")?.click();
19
+ } else {
20
+ el.textContent = template.replace("{n}", String(seconds));
21
+ }
22
+ }, 1000);
23
+ }
24
+
25
+ document.addEventListener("astro:before-swap", stop);
26
+ document.addEventListener("astro:page-load", start);
27
+ start();
@@ -0,0 +1,47 @@
1
+ declare global {
2
+ interface Window {
3
+ dataLayer?: unknown[];
4
+ gtag?: (...args: unknown[]) => void;
5
+ __staluxAnalyticsLoaded?: boolean;
6
+ __staluxAnalyticsPageLoadListener?: boolean;
7
+ }
8
+ }
9
+
10
+ function load() {
11
+ const gaID = document.body?.dataset.staluxGaId;
12
+ if (!gaID) return;
13
+ const hasScript = document.querySelector('script[data-stalux-google="true"]');
14
+ window.dataLayer = window.dataLayer || [];
15
+ window.gtag = window.gtag || ((...args: unknown[]) => window.dataLayer?.push(args));
16
+ if (!hasScript) {
17
+ const script = document.createElement("script");
18
+ script.async = true;
19
+ script.dataset.staluxGoogle = "true";
20
+ script.src = `https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(gaID)}`;
21
+ document.head.appendChild(script);
22
+ }
23
+ if (!window.__staluxAnalyticsLoaded) {
24
+ window.__staluxAnalyticsLoaded = true;
25
+ window.gtag("js", new Date());
26
+ window.gtag("config", gaID);
27
+ }
28
+ }
29
+
30
+ if (!window.__staluxAnalyticsPageLoadListener) {
31
+ window.__staluxAnalyticsPageLoadListener = true;
32
+ let firstPageLoad = true;
33
+ document.addEventListener("astro:page-load", () => {
34
+ load();
35
+ if (firstPageLoad) {
36
+ firstPageLoad = false;
37
+ return;
38
+ }
39
+ window.gtag?.("event", "page_view", { page_path: window.location.pathname });
40
+ });
41
+ }
42
+
43
+ if (document.readyState === "loading")
44
+ document.addEventListener("DOMContentLoaded", load, { once: true });
45
+ else load();
46
+
47
+ export {};
@@ -0,0 +1,58 @@
1
+ const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
2
+
3
+ function shuffle<T>(items: T[]): T[] {
4
+ const result = [...items];
5
+ for (let i = result.length - 1; i > 0; i -= 1) {
6
+ const bytes = new Uint32Array(1);
7
+ const j = globalThis.crypto?.getRandomValues
8
+ ? (crypto.getRandomValues(bytes)[0] ?? 0) % (i + 1)
9
+ : Math.floor(Math.random() * (i + 1));
10
+ [result[i], result[j]] = [result[j], result[i]];
11
+ }
12
+ return result;
13
+ }
14
+
15
+ async function fetchPosts() {
16
+ const response = await fetch("/api/post.abbrlink.json");
17
+ if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
18
+ return (await response.json()) as { abbrlink: string; title: string }[];
19
+ }
20
+
21
+ function render(posts: { abbrlink: string; title: string }[]) {
22
+ return posts
23
+ .map(
24
+ (post) =>
25
+ `<li class="random-post-item"><a href="/posts/${post.abbrlink}/" class="random-post-link a-none">${post.title}</a></li>`,
26
+ )
27
+ .join("");
28
+ }
29
+
30
+ async function refresh(container: HTMLElement, fallback: string) {
31
+ const list = container.querySelector<HTMLElement>("#random-posts-list");
32
+ if (!list) return;
33
+ list.style.opacity = "0.5";
34
+ await sleep(300);
35
+ try {
36
+ const posts = shuffle(await fetchPosts()).slice(0, Number(container.dataset.count ?? 5));
37
+ list.innerHTML = posts.length
38
+ ? render(posts)
39
+ : `<li class="no-posts">${container.dataset.noPosts ?? fallback}</li>`;
40
+ } catch {
41
+ list.innerHTML = `<li class="error">${container.dataset.loadFailed ?? fallback}</li>`;
42
+ } finally {
43
+ list.style.opacity = "1";
44
+ }
45
+ }
46
+
47
+ document.addEventListener("astro:page-load", () => {
48
+ const container = document.querySelector<HTMLElement>("[data-stalux-random-posts]");
49
+ if (!container || container.dataset.staluxBound === "true") return;
50
+ container.dataset.staluxBound = "true";
51
+ container.querySelector("#refresh-random-posts")?.addEventListener("click", async (event) => {
52
+ event.preventDefault();
53
+ await refresh(container, container.dataset.loadError ?? "Failed to load posts");
54
+ });
55
+ void refresh(container, container.dataset.loadError ?? "Failed to load posts");
56
+ });
57
+
58
+ export {};
@@ -0,0 +1,32 @@
1
+ declare global {
2
+ interface Window {
3
+ __staluxRuntimeTimer?: ReturnType<typeof setInterval>;
4
+ }
5
+ }
6
+
7
+ function update() {
8
+ const container = document.querySelector<HTMLElement>("[data-stalux-buildtime]");
9
+ const counter = document.getElementById("runtime-counter");
10
+ if (!container || !counter) return;
11
+ const start = new Date(container.dataset.staluxBuildtime ?? "");
12
+ const diffMs = Date.now() - start.getTime();
13
+ if (Number.isNaN(diffMs) || diffMs < 0) return;
14
+ const total = Math.floor(diffMs / 1000);
15
+ const format =
16
+ container.dataset.staluxRuntimeFormat ?? "{days}d {hours}h {minutes}m {seconds}s";
17
+ counter.textContent = format
18
+ .replace("{days}", String(Math.floor(total / 86400)))
19
+ .replace("{hours}", String(Math.floor((total % 86400) / 3600)))
20
+ .replace("{minutes}", String(Math.floor((total % 3600) / 60)))
21
+ .replace("{seconds}", String(total % 60));
22
+ }
23
+
24
+ function start() {
25
+ if (window.__staluxRuntimeTimer) clearInterval(window.__staluxRuntimeTimer);
26
+ update();
27
+ window.__staluxRuntimeTimer = setInterval(update, 1000);
28
+ }
29
+
30
+ document.addEventListener("astro:page-load", start);
31
+ start();
32
+ export {};
@@ -0,0 +1,21 @@
1
+ declare global {
2
+ interface Window {
3
+ __staluxVerCountLoaded?: boolean;
4
+ }
5
+ }
6
+
7
+ function load() {
8
+ if (document.querySelector("script[data-stalux-vercount]")) return;
9
+ window.__staluxVerCountLoaded = true;
10
+ const script = document.createElement("script");
11
+ script.defer = true;
12
+ script.dataset.staluxVercount = "true";
13
+ script.src = "https://events.vercount.one/js";
14
+ document.head.appendChild(script);
15
+ }
16
+
17
+ document.addEventListener("astro:page-load", load);
18
+ if (document.readyState === "loading")
19
+ document.addEventListener("DOMContentLoaded", load, { once: true });
20
+ else load();
21
+ export {};
@@ -56,9 +56,11 @@ interface DocumentWithModelContext extends Document {
56
56
  // 语言选择(跟随站点 site.yml 的 lang)
57
57
  // ---------------------------------------------------------------------------
58
58
 
59
- const SITE_LANG = (
60
- typeof window !== "undefined" ? (window.__STALUX_SITE_INFO__?.lang ?? "zh-CN") : "zh-CN"
61
- ).toLowerCase();
59
+ const SITE_LANG =
60
+ (typeof document !== "undefined"
61
+ ? document.body?.dataset.staluxSiteLang
62
+ : "zh-CN"
63
+ )?.toLowerCase() ?? "zh-cn";
62
64
 
63
65
  /** 站点为英文站时为 true */
64
66
  const IS_EN = SITE_LANG === "en" || SITE_LANG === "en-us" || SITE_LANG === "en-gb";
@@ -492,7 +494,13 @@ function siteInfoTool(): WebMCPTool {
492
494
  annotations: { readOnlyHint: true, untrustedContentHint: false },
493
495
  inputSchema: { type: "object", properties: {}, additionalProperties: false },
494
496
  execute: async () => {
495
- const info = window.__STALUX_SITE_INFO__ ?? {};
497
+ const body = document.body;
498
+ const info = window.__STALUX_SITE_INFO__ ?? {
499
+ title: body?.dataset.staluxSiteTitle,
500
+ url: body?.dataset.staluxSiteUrl,
501
+ description: body?.dataset.staluxSiteDescription,
502
+ lang: body?.dataset.staluxSiteLang,
503
+ };
496
504
  return {
497
505
  ok: true,
498
506
  title: info.title ?? "",