@xingwangzhe/stalux 1.24.7 → 1.25.1
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 +41 -2
- package/src/layouts/Stalux.astro +27 -17
- package/src/pages/404.astro +2 -32
- package/src/pages/about.md.ts +4 -1
- package/src/pages/archives.md.ts +4 -1
- package/src/pages/categories/[category].md.ts +4 -1
- package/src/pages/categories/index.md.ts +4 -1
- package/src/pages/index.md.ts +4 -1
- package/src/pages/links.md.ts +4 -1
- package/src/pages/llms-full.txt.ts +1 -0
- package/src/pages/llms.txt.ts +1 -0
- package/src/pages/openapi.json.ts +77 -0
- package/src/pages/posts/[post].md.ts +1 -0
- package/src/pages/tags/[tag].md.ts +4 -1
- package/src/pages/tags/index.md.ts +4 -1
- package/src/pages/words.md.ts +4 -1
- 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/src/utils/ai-discovery.ts +32 -0
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)。
|
|
@@ -112,6 +147,7 @@ function getRoutes(baseDir: URL): RouteEntry[] {
|
|
|
112
147
|
{ pattern: "/atom.xml", entrypoint: resolvePage("./pages/atom.xml.ts") },
|
|
113
148
|
{ pattern: "/llms.txt", entrypoint: resolvePage("./pages/llms.txt.ts") },
|
|
114
149
|
{ pattern: "/llms-full.txt", entrypoint: resolvePage("./pages/llms-full.txt.ts") },
|
|
150
|
+
{ pattern: "/openapi.json", entrypoint: resolvePage("./pages/openapi.json.ts") },
|
|
115
151
|
{
|
|
116
152
|
pattern: "/api/post.abbrlink.json",
|
|
117
153
|
entrypoint: resolvePage("./pages/api/post.abbrlink.json.ts"),
|
|
@@ -438,9 +474,12 @@ export function stalux(options: StaluxOptions = {}): AstroIntegration[] {
|
|
|
438
474
|
},
|
|
439
475
|
|
|
440
476
|
"astro:build:done": async ({ dir, logger }) => {
|
|
477
|
+
const outDir = fileURLToPath(dir);
|
|
478
|
+
stripGeneratedHtmlComments(outDir);
|
|
479
|
+
logger.info("Removed generated HTML comments (preserved pre/code/textarea blocks)");
|
|
480
|
+
|
|
441
481
|
// 6. 后处理:Pagefind 搜索索引
|
|
442
482
|
if (opt.pagefind) {
|
|
443
|
-
const outDir = fileURLToPath(dir);
|
|
444
483
|
logger.info("Running Pagefind indexer...");
|
|
445
484
|
|
|
446
485
|
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>
|
package/src/pages/about.md.ts
CHANGED
|
@@ -10,6 +10,9 @@ export const GET: APIRoute = async (context) => {
|
|
|
10
10
|
const site = getSite(config, context.site?.toString());
|
|
11
11
|
const text = await renderAboutMd(site);
|
|
12
12
|
return new Response(text, {
|
|
13
|
-
headers: {
|
|
13
|
+
headers: {
|
|
14
|
+
"Content-Type": "text/markdown; charset=utf-8",
|
|
15
|
+
Vary: "Accept, Accept-Encoding",
|
|
16
|
+
},
|
|
14
17
|
});
|
|
15
18
|
};
|
package/src/pages/archives.md.ts
CHANGED
|
@@ -19,6 +19,9 @@ export const GET: APIRoute = async (context) => {
|
|
|
19
19
|
const { t } = createTranslator(lang);
|
|
20
20
|
const text = await renderArchivesMd(site, exportMd, t);
|
|
21
21
|
return new Response(text, {
|
|
22
|
-
headers: {
|
|
22
|
+
headers: {
|
|
23
|
+
"Content-Type": "text/markdown; charset=utf-8",
|
|
24
|
+
Vary: "Accept, Accept-Encoding",
|
|
25
|
+
},
|
|
23
26
|
});
|
|
24
27
|
};
|
|
@@ -29,6 +29,9 @@ export const GET: APIRoute = async (context) => {
|
|
|
29
29
|
const site = getSite(config, context.site?.toString());
|
|
30
30
|
const text = renderTaxonomyPageMd(category, posts, site, "categories", exportMd);
|
|
31
31
|
return new Response(text, {
|
|
32
|
-
headers: {
|
|
32
|
+
headers: {
|
|
33
|
+
"Content-Type": "text/markdown; charset=utf-8",
|
|
34
|
+
Vary: "Accept, Accept-Encoding",
|
|
35
|
+
},
|
|
33
36
|
});
|
|
34
37
|
};
|
|
@@ -22,6 +22,9 @@ export const GET: APIRoute = async (context) => {
|
|
|
22
22
|
const map = buildTaxonomyMap(posts, "categories");
|
|
23
23
|
const text = renderTaxonomyListMd(map, site, "categories", t("ai.allCategories"));
|
|
24
24
|
return new Response(text, {
|
|
25
|
-
headers: {
|
|
25
|
+
headers: {
|
|
26
|
+
"Content-Type": "text/markdown; charset=utf-8",
|
|
27
|
+
Vary: "Accept, Accept-Encoding",
|
|
28
|
+
},
|
|
26
29
|
});
|
|
27
30
|
};
|
package/src/pages/index.md.ts
CHANGED
|
@@ -13,6 +13,9 @@ export const GET: APIRoute = async (context) => {
|
|
|
13
13
|
const { t } = createTranslator(lang);
|
|
14
14
|
const text = await renderIndexMd(config, site, t);
|
|
15
15
|
return new Response(text, {
|
|
16
|
-
headers: {
|
|
16
|
+
headers: {
|
|
17
|
+
"Content-Type": "text/markdown; charset=utf-8",
|
|
18
|
+
Vary: "Accept, Accept-Encoding",
|
|
19
|
+
},
|
|
17
20
|
});
|
|
18
21
|
};
|
package/src/pages/links.md.ts
CHANGED
|
@@ -13,6 +13,9 @@ export const GET: APIRoute = async (context) => {
|
|
|
13
13
|
const { t } = createTranslator(lang);
|
|
14
14
|
const text = renderLinksMd(config, site, t);
|
|
15
15
|
return new Response(text, {
|
|
16
|
-
headers: {
|
|
16
|
+
headers: {
|
|
17
|
+
"Content-Type": "text/markdown; charset=utf-8",
|
|
18
|
+
Vary: "Accept, Accept-Encoding",
|
|
19
|
+
},
|
|
17
20
|
});
|
|
18
21
|
};
|
package/src/pages/llms.txt.ts
CHANGED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { APIRoute } from "astro";
|
|
2
|
+
|
|
3
|
+
export const prerender = true;
|
|
4
|
+
|
|
5
|
+
const responseSchema = {
|
|
6
|
+
type: "array",
|
|
7
|
+
items: { type: "object", additionalProperties: true },
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const GET: APIRoute = async ({ site }) => {
|
|
11
|
+
const server = site?.toString().replace(/\/$/, "") || "";
|
|
12
|
+
const document = {
|
|
13
|
+
openapi: "3.1.0",
|
|
14
|
+
info: {
|
|
15
|
+
title: "Stalux Blog Read API",
|
|
16
|
+
version: "1.0.0",
|
|
17
|
+
description:
|
|
18
|
+
"Read-only machine-readable endpoints for discovering and retrieving published Stalux blog content.",
|
|
19
|
+
},
|
|
20
|
+
servers: server ? [{ url: server }] : undefined,
|
|
21
|
+
paths: {
|
|
22
|
+
"/api/posts.json": {
|
|
23
|
+
get: {
|
|
24
|
+
operationId: "listPublishedPosts",
|
|
25
|
+
summary: "List published posts",
|
|
26
|
+
description: "Returns metadata for every published post, newest first.",
|
|
27
|
+
responses: {
|
|
28
|
+
"200": {
|
|
29
|
+
description: "Published post metadata",
|
|
30
|
+
content: { "application/json": { schema: responseSchema } },
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
"/api/post.abbrlink.json": {
|
|
36
|
+
get: {
|
|
37
|
+
operationId: "listPostIdentifiers",
|
|
38
|
+
summary: "List post identifiers",
|
|
39
|
+
description: "Returns the title and stable abbrlink for every published post.",
|
|
40
|
+
responses: {
|
|
41
|
+
"200": {
|
|
42
|
+
description: "Published post identifiers",
|
|
43
|
+
content: { "application/json": { schema: responseSchema } },
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
"/posts/{abbrlink}.md": {
|
|
49
|
+
get: {
|
|
50
|
+
operationId: "readPostMarkdown",
|
|
51
|
+
summary: "Read one post as Markdown",
|
|
52
|
+
description: "Returns the normalized Markdown source for a published post.",
|
|
53
|
+
parameters: [
|
|
54
|
+
{
|
|
55
|
+
name: "abbrlink",
|
|
56
|
+
in: "path",
|
|
57
|
+
required: true,
|
|
58
|
+
description: "Stable post identifier returned by the post index.",
|
|
59
|
+
schema: { type: "string" },
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
responses: {
|
|
63
|
+
"200": {
|
|
64
|
+
description: "Markdown post content",
|
|
65
|
+
content: { "text/markdown": { schema: { type: "string" } } },
|
|
66
|
+
},
|
|
67
|
+
"404": { description: "Post not found" },
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
return new Response(JSON.stringify(document, null, 2), {
|
|
75
|
+
headers: { "Content-Type": "application/json; charset=utf-8" },
|
|
76
|
+
});
|
|
77
|
+
};
|
|
@@ -29,6 +29,9 @@ export const GET: APIRoute = async (context) => {
|
|
|
29
29
|
const site = getSite(config, context.site?.toString());
|
|
30
30
|
const text = renderTaxonomyPageMd(tag, posts, site, "tags", exportMd);
|
|
31
31
|
return new Response(text, {
|
|
32
|
-
headers: {
|
|
32
|
+
headers: {
|
|
33
|
+
"Content-Type": "text/markdown; charset=utf-8",
|
|
34
|
+
Vary: "Accept, Accept-Encoding",
|
|
35
|
+
},
|
|
33
36
|
});
|
|
34
37
|
};
|
|
@@ -22,6 +22,9 @@ export const GET: APIRoute = async (context) => {
|
|
|
22
22
|
const map = buildTaxonomyMap(posts, "tags");
|
|
23
23
|
const text = renderTaxonomyListMd(map, site, "tags", t("ai.allTags"));
|
|
24
24
|
return new Response(text, {
|
|
25
|
-
headers: {
|
|
25
|
+
headers: {
|
|
26
|
+
"Content-Type": "text/markdown; charset=utf-8",
|
|
27
|
+
Vary: "Accept, Accept-Encoding",
|
|
28
|
+
},
|
|
26
29
|
});
|
|
27
30
|
};
|
package/src/pages/words.md.ts
CHANGED
|
@@ -9,6 +9,9 @@ export const GET: APIRoute = async () => {
|
|
|
9
9
|
|
|
10
10
|
const text = await renderWordsMd();
|
|
11
11
|
return new Response(text, {
|
|
12
|
-
headers: {
|
|
12
|
+
headers: {
|
|
13
|
+
"Content-Type": "text/markdown; charset=utf-8",
|
|
14
|
+
Vary: "Accept, Accept-Encoding",
|
|
15
|
+
},
|
|
13
16
|
});
|
|
14
17
|
};
|
|
@@ -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 ?? "",
|
|
@@ -468,6 +468,38 @@ export async function renderLlmsTxt(config: ConfigMap, site: string): Promise<st
|
|
|
468
468
|
lines.push(buildMdLink("llms-full.txt", `${site}/llms-full.txt`, t("ai.llmsFullTxtDesc")));
|
|
469
469
|
lines.push("");
|
|
470
470
|
|
|
471
|
+
// Developer resources and agent guidance. Keep these links predictable so an
|
|
472
|
+
// agent can discover the read API without scraping the HTML application shell.
|
|
473
|
+
lines.push("## Developer resources");
|
|
474
|
+
lines.push(
|
|
475
|
+
buildMdLink(
|
|
476
|
+
"OpenAPI specification",
|
|
477
|
+
`${site}/openapi.json`,
|
|
478
|
+
"Machine-readable description of the read-only blog API.",
|
|
479
|
+
),
|
|
480
|
+
);
|
|
481
|
+
lines.push(
|
|
482
|
+
buildMdLink(
|
|
483
|
+
"Published post index",
|
|
484
|
+
`${site}/api/posts.json`,
|
|
485
|
+
"JSON metadata for published posts, including stable abbrlink identifiers.",
|
|
486
|
+
),
|
|
487
|
+
);
|
|
488
|
+
lines.push(
|
|
489
|
+
buildMdLink(
|
|
490
|
+
"Post identifier index",
|
|
491
|
+
`${site}/api/post.abbrlink.json`,
|
|
492
|
+
"JSON mapping of published post titles to stable identifiers.",
|
|
493
|
+
),
|
|
494
|
+
);
|
|
495
|
+
lines.push("");
|
|
496
|
+
|
|
497
|
+
lines.push("## When to use this site");
|
|
498
|
+
lines.push(
|
|
499
|
+
"Use the read API or Markdown endpoints when you need to search, cite, summarize, or retrieve the author's published technical articles. Start with `/openapi.json` or `/api/posts.json`, then fetch a specific `/posts/{abbrlink}.md` resource. The endpoints are read-only and do not require authentication.",
|
|
500
|
+
);
|
|
501
|
+
lines.push("");
|
|
502
|
+
|
|
471
503
|
// Optional
|
|
472
504
|
lines.push(`## ${t("ai.optional")}`);
|
|
473
505
|
lines.push(buildMdLink(t("ai.rss"), `${site}/rss.xml`));
|