@xingwangzhe/stalux 1.28.4 → 1.29.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/.release/cli.mjs +1 -0
- package/README.md +6 -0
- package/package.json +1 -1
- package/src/components/stalux/analytics/umami.astro +1 -19
- package/src/components/stalux/archives/archivesList.astro +3 -3
- package/src/components/stalux/categories/categoriesList.astro +1 -1
- package/src/components/stalux/common/date.astro +21 -0
- package/src/components/stalux/posts/PostTaxonomy.astro +1 -6
- package/src/components/stalux/posts/postCard.astro +5 -1
- package/src/components/stalux/posts/postContent.astro +3 -4
- package/src/components/stalux/posts/postToc.astro +1 -6
- package/src/index.ts +6 -1
- package/src/internal/image-dimensions.ts +122 -0
- package/src/layouts/PostLayout.astro +3 -0
- package/src/layouts/Stalux.astro +4 -1
- package/src/pages/posts/[post].astro +1 -0
- package/src/schemas/config.ts +5 -0
- package/src/scripts/after-load.ts +27 -0
- package/src/scripts/clarity.ts +23 -16
- package/src/scripts/umami.ts +28 -0
- package/src/scripts/vercount.ts +20 -13
- package/src/scripts/waline-client.ts +23 -0
- package/src/scripts/waline.ts +29 -25
- package/src/styles/base/init.css +30 -17
- package/src/styles/components/archives/archivesList.module.css +11 -112
- package/src/styles/components/categories/categoriesCard.module.css +13 -7
- package/src/styles/components/categories/categoriesList.module.css +2 -5
- package/src/styles/components/layout/navs.module.css +4 -3
- package/src/styles/components/links/linkCard.module.css +13 -12
- package/src/styles/components/links/linkList.module.css +0 -13
- package/src/styles/components/posts/postCard.module.css +19 -21
- package/src/styles/components/posts/postContent.module.css +19 -11
- package/src/styles/components/posts/postLayout.module.css +5 -5
- package/src/styles/components/posts/postList.module.css +2 -1
- package/src/styles/components/posts/postNavigation.module.css +2 -3
- package/src/styles/components/posts/postSidebar.module.css +6 -4
- package/src/styles/components/posts/waline.css +3 -4
- package/src/styles/components/words/wordCard.module.css +9 -3
- package/src/styles/components/words/wordList.module.css +0 -13
- package/src/styles/shared/view-transitions.css +54 -234
- package/src/utils/accent-color.ts +24 -0
- package/src/utils/view-transitions.ts +30 -20
package/.release/cli.mjs
CHANGED
package/README.md
CHANGED
|
@@ -40,6 +40,12 @@ export default defineConfig({
|
|
|
40
40
|
});
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
+
Set the optional `accentColor` in `stalux/config/site.yml` to customize the shared theme and Waline accent. It accepts a six-digit HEX color and defaults to `#EAB308`; choose a color with sufficient contrast against your background.
|
|
44
|
+
|
|
45
|
+
```yaml title="stalux/config/site.yml"
|
|
46
|
+
accentColor: "#EAB308"
|
|
47
|
+
```
|
|
48
|
+
|
|
43
49
|
To customize the bundled integrations, pass options (or disable them with `false`):
|
|
44
50
|
|
|
45
51
|
```ts
|
package/package.json
CHANGED
|
@@ -1,19 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { getCollection } from "astro:content";
|
|
3
|
-
import { getHeadData } from "@utils/config-utils";
|
|
4
|
-
|
|
5
|
-
const configEntries = await getCollection("config");
|
|
6
|
-
const headConfig = getHeadData(configEntries);
|
|
7
|
-
const umamiConfig = headConfig?.umami;
|
|
8
|
-
const umamiID = umamiConfig?.id;
|
|
9
|
-
const umamiURL = umamiConfig?.url;
|
|
10
|
-
---
|
|
11
|
-
|
|
12
|
-
<script
|
|
13
|
-
is:inline
|
|
14
|
-
defer
|
|
15
|
-
src={umamiURL}
|
|
16
|
-
data-website-id={umamiID}
|
|
17
|
-
type="text/javascript"
|
|
18
|
-
transition:persist="stalux-umami"
|
|
19
|
-
></script>
|
|
1
|
+
<script src="../../../scripts/umami.ts"></script>
|
|
@@ -58,18 +58,18 @@ const archiveYears = [...archivesByYearMonth.entries()]
|
|
|
58
58
|
{archiveYears.length > 0 ? (
|
|
59
59
|
archiveYears.map((archiveYear) => (
|
|
60
60
|
<div class={styles.yearSection} data-year={String(archiveYear.year)}>
|
|
61
|
-
<div class={styles.yearHeader}
|
|
61
|
+
<div class={styles.yearHeader}>
|
|
62
62
|
<h2 class={styles.yearTitle}>{archiveYear.year}</h2>
|
|
63
63
|
<div class={styles.yearCount}>
|
|
64
64
|
{t("archives.postCount", { n: archiveYear.count })}
|
|
65
65
|
</div>
|
|
66
66
|
</div>
|
|
67
67
|
{archiveYear.months.map(({ month, posts: monthPosts }) => (
|
|
68
|
-
<div class={styles.monthSection}
|
|
68
|
+
<div class={styles.monthSection}>
|
|
69
69
|
<h3 class={styles.monthTitle}>{t("archives.month", { n: month })}</h3>
|
|
70
70
|
<ul class={styles.postList}>
|
|
71
71
|
{monthPosts.map((post) => (
|
|
72
|
-
<li class={styles.postItem}
|
|
72
|
+
<li class={styles.postItem}>
|
|
73
73
|
<time class={styles.postDate} datetime={post.data.date}>
|
|
74
74
|
{format(parseDate(post.data.date), "yyyy-MM-dd")}
|
|
75
75
|
</time>
|
|
@@ -8,7 +8,7 @@ const categoryList = await getCategoryCountList(Astro.logger);
|
|
|
8
8
|
|
|
9
9
|
<ul class={`${styles.categoriesList} stagger-children`}>
|
|
10
10
|
{categoryList.map((category) => (
|
|
11
|
-
<li class={styles.item}
|
|
11
|
+
<li class={styles.item}>
|
|
12
12
|
<CategoriesCard category={category} />
|
|
13
13
|
</li>
|
|
14
14
|
))}
|
|
@@ -6,3 +6,24 @@ import styles from "@styles/components/common/date.module.css";
|
|
|
6
6
|
<time id={styles.date} data-ref="date" datetime=""></time>
|
|
7
7
|
<time id={styles.time} data-ref="time" datetime=""></time>
|
|
8
8
|
</div>
|
|
9
|
+
|
|
10
|
+
<script is:inline>
|
|
11
|
+
(() => {
|
|
12
|
+
const dateElement = document.querySelector('[data-ref="date"]');
|
|
13
|
+
const timeElement = document.querySelector('[data-ref="time"]');
|
|
14
|
+
if (!dateElement || !timeElement) return;
|
|
15
|
+
const now = new Date();
|
|
16
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
17
|
+
const lang = document.documentElement.lang || "zh-CN";
|
|
18
|
+
dateElement.textContent = lang.startsWith("zh")
|
|
19
|
+
? `${now.getFullYear()}年${pad(now.getMonth() + 1)}月${pad(now.getDate())}日`
|
|
20
|
+
: new Intl.DateTimeFormat("en-US", {
|
|
21
|
+
year: "numeric",
|
|
22
|
+
month: "short",
|
|
23
|
+
day: "2-digit",
|
|
24
|
+
}).format(now);
|
|
25
|
+
dateElement.dateTime = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`;
|
|
26
|
+
timeElement.textContent = `${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`;
|
|
27
|
+
timeElement.dateTime = timeElement.textContent;
|
|
28
|
+
})();
|
|
29
|
+
</script>
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
---
|
|
2
2
|
import { getCollection } from "astro:content";
|
|
3
|
-
import { fade } from "astro:transitions";
|
|
4
3
|
import style from "@styles/components/posts/postTaxonomy.module.css";
|
|
5
4
|
import { getSiteData } from "@utils/config-utils";
|
|
6
5
|
import { createTranslator } from "@utils/i18n";
|
|
@@ -46,11 +45,7 @@ const cfg = {
|
|
|
46
45
|
<nav class={style.container} transition:name={cfg.containerName} aria-label={title ?? cfg.heading}>
|
|
47
46
|
{items.length > 0 && <h3 class={style.title}>{title ?? cfg.heading}</h3>}
|
|
48
47
|
{items.length > 0 ? (
|
|
49
|
-
<div
|
|
50
|
-
class={cfg.listClass}
|
|
51
|
-
transition:name={cfg.contentName}
|
|
52
|
-
transition:animate={fade({ duration: "0.3s" })}
|
|
53
|
-
>
|
|
48
|
+
<div class={cfg.listClass} transition:name={cfg.contentName}>
|
|
54
49
|
{items.map((item) => (
|
|
55
50
|
<a href={`${cfg.prefix}${item}/`} class={`${cfg.itemClass} a-none`} data-astro-prefetch>
|
|
56
51
|
{item}
|
|
@@ -4,6 +4,7 @@ import { getCollection } from "astro:content";
|
|
|
4
4
|
import style from "@styles/components/posts/postCard.module.css";
|
|
5
5
|
import { getSiteData } from "@utils/config-utils";
|
|
6
6
|
import { toMachineDateTime } from "@utils/semantic-time";
|
|
7
|
+
import { getPostTitleTransitionName } from "@utils/view-transitions";
|
|
7
8
|
|
|
8
9
|
interface Props {
|
|
9
10
|
post: CollectionEntry<"posts">;
|
|
@@ -41,7 +42,10 @@ const description = post.data.desc || "";
|
|
|
41
42
|
</div>
|
|
42
43
|
</a>
|
|
43
44
|
)}
|
|
44
|
-
<h2
|
|
45
|
+
<h2
|
|
46
|
+
class={style.postTitle}
|
|
47
|
+
transition:name={priority ? getPostTitleTransitionName(post.data.abbrlink) : undefined}
|
|
48
|
+
>
|
|
45
49
|
<a href={`/posts/${post.data.abbrlink}/`} class="a-none" data-astro-prefetch="viewport">
|
|
46
50
|
{post.data.title}
|
|
47
51
|
</a>
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
---
|
|
2
|
-
import { fade } from "astro:transitions";
|
|
3
2
|
import "photoswipe/dist/photoswipe.css";
|
|
4
3
|
import style from "@styles/components/posts/postContent.module.css";
|
|
5
4
|
import "@styles/components/markdown.css";
|
|
@@ -11,6 +10,7 @@ import { toMachineDateTime } from "@utils/semantic-time";
|
|
|
11
10
|
interface Props {
|
|
12
11
|
title: string;
|
|
13
12
|
transitionName?: string;
|
|
13
|
+
titleTransitionName?: string;
|
|
14
14
|
date?: string;
|
|
15
15
|
updated?: string;
|
|
16
16
|
readingMinutes?: number;
|
|
@@ -35,12 +35,11 @@ const mdSourceUrl = exportMd ? `${Astro.url.pathname.replace(/\/$/, "")}.md` : "
|
|
|
35
35
|
<article
|
|
36
36
|
class={style.articleContent}
|
|
37
37
|
data-stalux-gallery={props.hasImage ? "true" : undefined}
|
|
38
|
-
transition:name={props.transitionName
|
|
39
|
-
transition:animate={fade({ duration: "0.45s" })}
|
|
38
|
+
transition:name={props.transitionName}
|
|
40
39
|
>
|
|
41
40
|
<!-- Article Header -->
|
|
42
41
|
<header class={style.articleHeader}>
|
|
43
|
-
<h1 class={style.title}>{props.title}</h1>
|
|
42
|
+
<h1 class={style.title} transition:name={props.titleTransitionName}>{props.title}</h1>
|
|
44
43
|
<div class={style.metadata}>
|
|
45
44
|
{props.date && (
|
|
46
45
|
<time
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
---
|
|
2
2
|
import { getCollection } from "astro:content";
|
|
3
|
-
import { fade } from "astro:transitions";
|
|
4
3
|
import style from "@styles/components/posts/postToc.module.css";
|
|
5
4
|
import { getSiteData } from "@utils/config-utils";
|
|
6
5
|
import { createTranslator } from "@utils/i18n";
|
|
@@ -36,11 +35,7 @@ const tocItems = headings
|
|
|
36
35
|
>
|
|
37
36
|
<h3 class={style.title}>{title}</h3>
|
|
38
37
|
{tocItems.length > 0 ? (
|
|
39
|
-
<ul
|
|
40
|
-
class={style.list}
|
|
41
|
-
transition:name="toc-content"
|
|
42
|
-
transition:animate={fade({ duration: "0.3s" })}
|
|
43
|
-
>
|
|
38
|
+
<ul class={style.list} transition:name="toc-content">
|
|
44
39
|
{tocItems.map((heading) => (
|
|
45
40
|
<li>
|
|
46
41
|
<a
|
package/src/index.ts
CHANGED
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
resolveFontInputs,
|
|
37
37
|
writePageFontSubset,
|
|
38
38
|
} from "./internal/font-slices";
|
|
39
|
+
import { addLocalImageDimensions } from "./internal/image-dimensions";
|
|
39
40
|
import { createInjectedRoutes } from "./internal/injected-routes";
|
|
40
41
|
import { createRuntimeCacheKey } from "./internal/runtime-cache-key";
|
|
41
42
|
import {
|
|
@@ -376,7 +377,11 @@ export function stalux(options: StaluxOptions = {}): AstroIntegration[] {
|
|
|
376
377
|
/<style data-stalux-page-font>[\s\S]*?<\/style>/gi,
|
|
377
378
|
"",
|
|
378
379
|
);
|
|
379
|
-
|
|
380
|
+
const withImageDimensions = await addLocalImageDimensions(
|
|
381
|
+
withoutPageFont,
|
|
382
|
+
outDir,
|
|
383
|
+
);
|
|
384
|
+
let after = applyHtmlImageLoadingPolicy(withImageDimensions);
|
|
380
385
|
if (pageFontData) {
|
|
381
386
|
const linkedStylesheetText = readLinkedStylesheetText(
|
|
382
387
|
after,
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
export type ImageDimensions = { width: number; height: number };
|
|
5
|
+
|
|
6
|
+
/** Read intrinsic dimensions for common public image formats without loading an image library. */
|
|
7
|
+
export function readImageDimensions(data: Buffer): ImageDimensions | undefined {
|
|
8
|
+
if (data.length >= 24 && data.toString("hex", 0, 8) === "89504e470d0a1a0a") {
|
|
9
|
+
return { width: data.readUInt32BE(16), height: data.readUInt32BE(20) };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (
|
|
13
|
+
data.length >= 10 &&
|
|
14
|
+
(data.toString("ascii", 0, 6) === "GIF87a" || data.toString("ascii", 0, 6) === "GIF89a")
|
|
15
|
+
) {
|
|
16
|
+
return { width: data.readUInt16LE(6), height: data.readUInt16LE(8) };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (
|
|
20
|
+
data.length >= 30 &&
|
|
21
|
+
data.toString("ascii", 0, 4) === "RIFF" &&
|
|
22
|
+
data.toString("ascii", 8, 12) === "WEBP"
|
|
23
|
+
) {
|
|
24
|
+
const chunk = data.toString("ascii", 12, 16);
|
|
25
|
+
if (chunk === "VP8X") {
|
|
26
|
+
return {
|
|
27
|
+
width: 1 + data.readUIntLE(24, 3),
|
|
28
|
+
height: 1 + data.readUIntLE(27, 3),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
if (chunk === "VP8 " && data[23] === 0x9d && data[24] === 0x01 && data[25] === 0x2a) {
|
|
32
|
+
return {
|
|
33
|
+
width: data.readUInt16LE(26) & 0x3fff,
|
|
34
|
+
height: data.readUInt16LE(28) & 0x3fff,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
if (chunk === "VP8L" && data[20] === 0x2f) {
|
|
38
|
+
const b1 = data[21] ?? 0;
|
|
39
|
+
const b2 = data[22] ?? 0;
|
|
40
|
+
const b3 = data[23] ?? 0;
|
|
41
|
+
const b4 = data[24] ?? 0;
|
|
42
|
+
return {
|
|
43
|
+
width: 1 + b1 + ((b2 & 0x3f) << 8),
|
|
44
|
+
height: 1 + (b2 >> 6) + (b3 << 2) + ((b4 & 0x0f) << 10),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (data.length >= 4 && data[0] === 0xff && data[1] === 0xd8) {
|
|
50
|
+
let offset = 2;
|
|
51
|
+
while (offset + 4 <= data.length) {
|
|
52
|
+
if (data[offset] !== 0xff) break;
|
|
53
|
+
const marker = data[offset + 1] ?? 0;
|
|
54
|
+
const size = data.readUInt16BE(offset + 2);
|
|
55
|
+
if (size < 7 || offset + 2 + size > data.length) break;
|
|
56
|
+
if (
|
|
57
|
+
[
|
|
58
|
+
0xc0, 0xc1, 0xc2, 0xc3, 0xc5, 0xc6, 0xc7, 0xc9, 0xca, 0xcb, 0xcd, 0xce, 0xcf,
|
|
59
|
+
].includes(marker)
|
|
60
|
+
) {
|
|
61
|
+
return {
|
|
62
|
+
width: data.readUInt16BE(offset + 7),
|
|
63
|
+
height: data.readUInt16BE(offset + 5),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
offset += size + 2;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Add width/height to same-origin emitted images so their space is reserved before download. */
|
|
74
|
+
export async function addLocalImageDimensions(html: string, outputDir: string): Promise<string> {
|
|
75
|
+
const dimensionsCache = new Map<string, Promise<ImageDimensions | undefined>>();
|
|
76
|
+
const root = path.resolve(outputDir);
|
|
77
|
+
const tags = [...html.matchAll(/<img\b[^>]*>/gi)];
|
|
78
|
+
|
|
79
|
+
for (const match of tags) {
|
|
80
|
+
const tag = match[0];
|
|
81
|
+
if (!tag || (/\swidth=/i.test(tag) && /\sheight=/i.test(tag))) continue;
|
|
82
|
+
const src = tag.match(/\ssrc="([^"]+)"/i)?.[1];
|
|
83
|
+
if (!src) continue;
|
|
84
|
+
|
|
85
|
+
let filePath: string;
|
|
86
|
+
try {
|
|
87
|
+
const url = new URL(src, "https://stalux.invalid");
|
|
88
|
+
if (url.origin !== "https://stalux.invalid") continue;
|
|
89
|
+
filePath = path.resolve(root, `.${decodeURIComponent(url.pathname)}`);
|
|
90
|
+
} catch {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (filePath !== root && !filePath.startsWith(`${root}${path.sep}`)) continue;
|
|
94
|
+
|
|
95
|
+
let dimensions = dimensionsCache.get(filePath);
|
|
96
|
+
if (!dimensions) {
|
|
97
|
+
dimensions = readFile(filePath)
|
|
98
|
+
.then(readImageDimensions)
|
|
99
|
+
.catch(() => undefined);
|
|
100
|
+
dimensionsCache.set(filePath, dimensions);
|
|
101
|
+
}
|
|
102
|
+
const size = await dimensions;
|
|
103
|
+
if (!size || size.width <= 0 || size.height <= 0) continue;
|
|
104
|
+
|
|
105
|
+
const hasWidth = /\swidth=/i.test(tag);
|
|
106
|
+
const hasHeight = /\sheight=/i.test(tag);
|
|
107
|
+
const updated =
|
|
108
|
+
hasWidth && !hasHeight
|
|
109
|
+
? tag.replace(/\s*\/?\s*>$/, (ending) => ` height="${size.height}"${ending}`)
|
|
110
|
+
: hasHeight && !hasWidth
|
|
111
|
+
? tag.replace(
|
|
112
|
+
/\sheight="[^"]*"/i,
|
|
113
|
+
` width="${size.width}" height="${size.height}"`,
|
|
114
|
+
)
|
|
115
|
+
: tag.replace(
|
|
116
|
+
/\s*\/?\s*>$/,
|
|
117
|
+
(ending) => ` width="${size.width}" height="${size.height}"${ending}`,
|
|
118
|
+
);
|
|
119
|
+
html = html.replace(tag, updated);
|
|
120
|
+
}
|
|
121
|
+
return html;
|
|
122
|
+
}
|
|
@@ -7,12 +7,14 @@ import PostNavigation from "@components/stalux/posts/postNavigation.astro";
|
|
|
7
7
|
import PostRight from "@components/stalux/posts/postRight.astro";
|
|
8
8
|
import WalineComment from "@components/stalux/posts/WalineComment.astro";
|
|
9
9
|
import { getCommentData } from "@utils/config-utils";
|
|
10
|
+
import { getPostTitleTransitionName } from "@utils/view-transitions";
|
|
10
11
|
import { buildWalineProps } from "@utils/waline";
|
|
11
12
|
import "@styles/base/math.css";
|
|
12
13
|
import layoutStyle from "@styles/components/posts/postLayout.module.css";
|
|
13
14
|
|
|
14
15
|
interface Props {
|
|
15
16
|
title: string;
|
|
17
|
+
postId?: string | number;
|
|
16
18
|
date?: string;
|
|
17
19
|
updated?: string;
|
|
18
20
|
readingMinutes?: number;
|
|
@@ -57,6 +59,7 @@ const walineConfig = walineSection?.serverURL
|
|
|
57
59
|
<!-- Main Content -->
|
|
58
60
|
<PostContent
|
|
59
61
|
title={props.title}
|
|
62
|
+
titleTransitionName={props.postId === undefined ? undefined : getPostTitleTransitionName(props.postId)}
|
|
60
63
|
date={props.date}
|
|
61
64
|
updated={props.updated}
|
|
62
65
|
readingMinutes={props.readingMinutes}
|
package/src/layouts/Stalux.astro
CHANGED
|
@@ -8,6 +8,7 @@ import "@pagefind/component-ui/css";
|
|
|
8
8
|
import "@styles/base/init.css";
|
|
9
9
|
import "@styles/shared/stagger.css";
|
|
10
10
|
import "@styles/shared/view-transitions.css";
|
|
11
|
+
import { getAccentColorStyle } from "@utils/accent-color";
|
|
11
12
|
import {
|
|
12
13
|
getAiDiscoveryData,
|
|
13
14
|
getAuthorData,
|
|
@@ -56,7 +57,7 @@ const routeAnimation = getRouteAnimation(routeKind);
|
|
|
56
57
|
---
|
|
57
58
|
|
|
58
59
|
<!doctype html>
|
|
59
|
-
<html lang={htmlLang}>
|
|
60
|
+
<html lang={htmlLang} style={getAccentColorStyle(siteConfig.accentColor)}>
|
|
60
61
|
<Head
|
|
61
62
|
title={props.title}
|
|
62
63
|
description={props.description}
|
|
@@ -84,6 +85,8 @@ const routeAnimation = getRouteAnimation(routeKind);
|
|
|
84
85
|
data-stalux-site-lang={siteConfig.lang}
|
|
85
86
|
data-stalux-ga-id={headConfig?.googleAnalyticsId}
|
|
86
87
|
data-stalux-clarity-id={headConfig?.bingClarityId}
|
|
88
|
+
data-stalux-umami-id={headConfig?.umami?.id}
|
|
89
|
+
data-stalux-umami-url={headConfig?.umami?.url}
|
|
87
90
|
>
|
|
88
91
|
{llmPromote && (
|
|
89
92
|
<section class="stalux-ai-prompt" data-stalux-ai-prompt aria-label="AI site context">
|
|
@@ -69,6 +69,7 @@ export async function getStaticPaths() {
|
|
|
69
69
|
<div style="height:3em"></div>
|
|
70
70
|
<PostLayout
|
|
71
71
|
title={props.post.data.title}
|
|
72
|
+
postId={props.post.data.abbrlink}
|
|
72
73
|
date={props.post.data.date}
|
|
73
74
|
updated={props.post.data.updated}
|
|
74
75
|
readingMinutes={flags.readingMinutes}
|
package/src/schemas/config.ts
CHANGED
|
@@ -16,6 +16,11 @@ export const siteSchema = z.object({
|
|
|
16
16
|
title: z.string().min(1, "site.title is required"),
|
|
17
17
|
url: z.url("site.url must be a valid URL"),
|
|
18
18
|
description: z.string().min(1, "site.description is required"),
|
|
19
|
+
accentColor: z
|
|
20
|
+
.string()
|
|
21
|
+
.regex(/^#[\da-fA-F]{6}$/, "site.accentColor must be a six-digit HEX color such as #EAB308")
|
|
22
|
+
.optional()
|
|
23
|
+
.default("#EAB308"),
|
|
19
24
|
seoTitle: z.string().min(1).optional(),
|
|
20
25
|
timezone: z.string().optional().default("Asia/Shanghai"),
|
|
21
26
|
canonical: z.url("site.canonical must be a valid URL").optional(),
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
type IdleWindow = Window & {
|
|
2
|
+
requestIdleCallback?: (callback: () => void, options?: { timeout: number }) => number;
|
|
3
|
+
};
|
|
4
|
+
|
|
5
|
+
/** Run non-essential work after the initial page load and during an idle period. */
|
|
6
|
+
export function afterLoadAndIdle(callback: () => void): void {
|
|
7
|
+
let scheduled = false;
|
|
8
|
+
const schedule = () => {
|
|
9
|
+
if (scheduled) return;
|
|
10
|
+
scheduled = true;
|
|
11
|
+
const idleWindow = window as IdleWindow;
|
|
12
|
+
if (idleWindow.requestIdleCallback) {
|
|
13
|
+
idleWindow.requestIdleCallback(callback, { timeout: 2_000 });
|
|
14
|
+
} else {
|
|
15
|
+
window.setTimeout(callback, 0);
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
if (document.readyState === "complete") {
|
|
20
|
+
schedule();
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
window.addEventListener("load", schedule, { once: true });
|
|
25
|
+
// Still load analytics if a third-party resource prevents the load event.
|
|
26
|
+
window.setTimeout(schedule, 5_000);
|
|
27
|
+
}
|
package/src/scripts/clarity.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { afterLoadAndIdle } from "./after-load";
|
|
1
2
|
import { createClientLogger } from "./logger";
|
|
2
3
|
|
|
3
4
|
const logger = createClientLogger("clarity");
|
|
@@ -14,22 +15,28 @@ function load() {
|
|
|
14
15
|
const id = document.body?.dataset.staluxClarityId;
|
|
15
16
|
if (!id || window.__staluxClarityLoaded || document.getElementById("stalux-clarity-script"))
|
|
16
17
|
return;
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
() => logger.
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
18
|
+
afterLoadAndIdle(() => {
|
|
19
|
+
if (window.__staluxClarityLoaded || document.getElementById("stalux-clarity-script"))
|
|
20
|
+
return;
|
|
21
|
+
window.__staluxClarityLoaded = true;
|
|
22
|
+
window.clarity =
|
|
23
|
+
window.clarity ||
|
|
24
|
+
Object.assign((...args: unknown[]) => window.clarity?.q?.push(args), { q: [] });
|
|
25
|
+
const script = document.createElement("script");
|
|
26
|
+
script.id = "stalux-clarity-script";
|
|
27
|
+
script.async = true;
|
|
28
|
+
script.src = `https://www.clarity.ms/tag/${encodeURIComponent(id)}`;
|
|
29
|
+
script.addEventListener("load", () => logger.debug("external script loaded"), {
|
|
30
|
+
once: true,
|
|
31
|
+
});
|
|
32
|
+
script.addEventListener(
|
|
33
|
+
"error",
|
|
34
|
+
() => logger.warn("external script failed to load (network or content blocker)"),
|
|
35
|
+
{ once: true },
|
|
36
|
+
);
|
|
37
|
+
logger.debug("loading external script after page load and idle time");
|
|
38
|
+
document.head.appendChild(script);
|
|
39
|
+
});
|
|
33
40
|
}
|
|
34
41
|
|
|
35
42
|
if (!window.__staluxClarityPageLoadListener) {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { afterLoadAndIdle } from "./after-load";
|
|
2
|
+
import { createClientLogger } from "./logger";
|
|
3
|
+
|
|
4
|
+
const logger = createClientLogger("umami");
|
|
5
|
+
|
|
6
|
+
const id = document.body?.dataset.staluxUmamiId;
|
|
7
|
+
const url = document.body?.dataset.staluxUmamiUrl;
|
|
8
|
+
|
|
9
|
+
if (id && url) {
|
|
10
|
+
afterLoadAndIdle(() => {
|
|
11
|
+
if (document.querySelector("script[data-stalux-umami]")) return;
|
|
12
|
+
const script = document.createElement("script");
|
|
13
|
+
script.async = true;
|
|
14
|
+
script.dataset.staluxUmami = "true";
|
|
15
|
+
script.dataset.websiteId = id;
|
|
16
|
+
script.src = url;
|
|
17
|
+
script.addEventListener("load", () => logger.debug("external script loaded"), {
|
|
18
|
+
once: true,
|
|
19
|
+
});
|
|
20
|
+
script.addEventListener(
|
|
21
|
+
"error",
|
|
22
|
+
() => logger.warn("external script failed to load (network or content blocker)"),
|
|
23
|
+
{ once: true },
|
|
24
|
+
);
|
|
25
|
+
logger.debug("loading external script after page load and idle time");
|
|
26
|
+
document.head.appendChild(script);
|
|
27
|
+
});
|
|
28
|
+
}
|
package/src/scripts/vercount.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { afterLoadAndIdle } from "./after-load";
|
|
1
2
|
import { createClientLogger } from "./logger";
|
|
2
3
|
|
|
3
4
|
const logger = createClientLogger("vercount");
|
|
@@ -10,19 +11,25 @@ declare global {
|
|
|
10
11
|
|
|
11
12
|
function load() {
|
|
12
13
|
if (document.querySelector("script[data-stalux-vercount]")) return;
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
"
|
|
21
|
-
() => logger.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
14
|
+
afterLoadAndIdle(() => {
|
|
15
|
+
if (window.__staluxVerCountLoaded || document.querySelector("script[data-stalux-vercount]"))
|
|
16
|
+
return;
|
|
17
|
+
window.__staluxVerCountLoaded = true;
|
|
18
|
+
const script = document.createElement("script");
|
|
19
|
+
script.async = true;
|
|
20
|
+
script.dataset.staluxVercount = "true";
|
|
21
|
+
script.src = "https://events.vercount.one/js";
|
|
22
|
+
script.addEventListener("load", () => logger.debug("external script loaded"), {
|
|
23
|
+
once: true,
|
|
24
|
+
});
|
|
25
|
+
script.addEventListener(
|
|
26
|
+
"error",
|
|
27
|
+
() => logger.warn("external script failed to load (network or content blocker)"),
|
|
28
|
+
{ once: true },
|
|
29
|
+
);
|
|
30
|
+
logger.debug("loading external script after page load and idle time");
|
|
31
|
+
document.head.appendChild(script);
|
|
32
|
+
});
|
|
26
33
|
}
|
|
27
34
|
|
|
28
35
|
document.addEventListener("astro:page-load", load);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { init, type WalineInitOptions } from "@waline/client";
|
|
2
|
+
import walineCss from "@waline/client/style?inline";
|
|
3
|
+
import staluxWalineCss from "../styles/components/posts/waline.css?inline";
|
|
4
|
+
|
|
5
|
+
export function mountWaline(config: Record<string, unknown>, element: HTMLElement): () => void {
|
|
6
|
+
const style = document.createElement("style");
|
|
7
|
+
style.dataset.staluxWaline = "true";
|
|
8
|
+
style.textContent = `${walineCss}\n${staluxWalineCss}`;
|
|
9
|
+
document.head.append(style);
|
|
10
|
+
|
|
11
|
+
const options = {
|
|
12
|
+
...config,
|
|
13
|
+
el: element,
|
|
14
|
+
serverURL: config.serverURL,
|
|
15
|
+
path: typeof config.path === "string" ? config.path : window.location.pathname,
|
|
16
|
+
} as WalineInitOptions;
|
|
17
|
+
const instance = init(options);
|
|
18
|
+
|
|
19
|
+
return () => {
|
|
20
|
+
instance?.destroy();
|
|
21
|
+
style.remove();
|
|
22
|
+
};
|
|
23
|
+
}
|