@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 +1 -1
- package/src/components/stalux/analytics/clarity.astro +1 -22
- package/src/components/stalux/analytics/google.astro +1 -31
- package/src/components/stalux/common/verCount.astro +1 -12
- package/src/components/stalux/footer/FooterStats.astro +2 -36
- package/src/components/stalux/layout/head.astro +1 -1
- package/src/components/stalux/posts/randomPost.astro +9 -118
- package/src/index.ts +40 -2
- package/src/layouts/Stalux.astro +27 -17
- package/src/pages/404.astro +2 -32
- package/src/scripts/clarity.ts +26 -0
- package/src/scripts/countdown.ts +27 -0
- package/src/scripts/google-analytics.ts +47 -0
- package/src/scripts/random-posts.ts +58 -0
- package/src/scripts/runtime-counter.ts +32 -0
- package/src/scripts/vercount.ts +21 -0
- package/src/scripts/webmcp.ts +12 -4
package/package.json
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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 {
|
|
@@ -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(
|
|
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
|
-
|
|
22
|
-
|
|
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
|
-
|
|
46
|
-
|
|
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 {
|
|
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 {
|
package/src/layouts/Stalux.astro
CHANGED
|
@@ -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 &&
|
|
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>
|
package/src/pages/404.astro
CHANGED
|
@@ -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
|
|
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 {};
|
package/src/scripts/webmcp.ts
CHANGED
|
@@ -56,9 +56,11 @@ interface DocumentWithModelContext extends Document {
|
|
|
56
56
|
// 语言选择(跟随站点 site.yml 的 lang)
|
|
57
57
|
// ---------------------------------------------------------------------------
|
|
58
58
|
|
|
59
|
-
const SITE_LANG =
|
|
60
|
-
typeof
|
|
61
|
-
|
|
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
|
|
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 ?? "",
|