@xingwangzhe/stalux 1.29.6 → 1.29.8
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/README.md +22 -2
- package/package.json +2 -1
- package/src/components/stalux/analytics/clarity.astro +1 -1
- package/src/components/stalux/analytics/google.astro +1 -1
- package/src/components/stalux/analytics/umami.astro +1 -1
- package/src/components/stalux/archives/archivesList.astro +29 -17
- package/src/components/stalux/categories/categoriesList.astro +2 -2
- package/src/components/stalux/common/verCount.astro +1 -1
- package/src/components/stalux/footer/FooterBadges.astro +14 -3
- package/src/components/stalux/layout/head.astro +8 -4
- package/src/components/stalux/links/linkList.astro +1 -1
- package/src/components/stalux/tags/tagsCloud.astro +22 -12
- package/src/components/stalux/words/wordList.astro +1 -1
- package/src/index.ts +15 -3
- package/src/layouts/Stalux.astro +0 -5
- package/src/scripts/analytics-runtime.ts +169 -0
- package/src/scripts/core-runtime.ts +0 -1
- package/src/scripts/date-time.ts +9 -5
- package/src/scripts/navigation.ts +48 -25
- package/src/scripts/runtime-counter.ts +2 -3
- package/src/scripts/search-dialog.ts +24 -0
- package/src/scripts/tags-cloud.ts +63 -29
- package/src/scripts/visible-clock.ts +24 -0
- package/src/scripts/waline.ts +1 -1
- package/src/styles/components/archives/archivesList.module.css +15 -87
- package/src/styles/components/categories/categoriesCard.module.css +3 -8
- package/src/styles/components/categories/categoriesList.module.css +7 -8
- package/src/styles/components/links/linkCard.module.css +0 -2
- package/src/styles/components/links/linkList.module.css +3 -3
- package/src/styles/components/posts/postAuthor.module.css +0 -1
- package/src/styles/components/posts/postCard.module.css +3 -12
- package/src/styles/components/tags/tagHeader.module.css +31 -31
- package/src/styles/components/tags/tagsCloud.module.css +34 -50
- package/src/utils/content-index.ts +1 -1
- package/src/utils/feed.ts +1 -1
- package/src/scripts/after-load.ts +0 -27
- package/src/scripts/clarity.ts +0 -48
- package/src/scripts/google-analytics.ts +0 -90
- package/src/scripts/umami.ts +0 -28
- package/src/scripts/vercount.ts +0 -38
package/README.md
CHANGED
|
@@ -109,6 +109,20 @@ The subset CSS uses an exact per-character `unicode-range` and `font-display: sw
|
|
|
109
109
|
|
|
110
110
|
The persistent BLAKE3 cache key includes font bytes, face index, normalized character set, and algorithm version. Cache files live under `node_modules/.astro/` and are reused across pages and incremental builds. Font inputs are read locally; builds do not fetch fonts from the network.
|
|
111
111
|
|
|
112
|
+
### Visibility-aware loading and updates
|
|
113
|
+
|
|
114
|
+
The desktop tag cloud loads dynamically near the viewport. Switching to mobile, leaving the viewport or hiding the document destroys it and stops animation scheduling; becoming visible recreates it. Tag clicks use Astro client navigation. Clocks and the footer runtime update only while visible, and the date changes only on a new day. Comments load within half a viewport (capped at 400px). Tag groups, archive months and category cards retain static HTML while deferring offscreen rendering.
|
|
115
|
+
|
|
116
|
+
### Modern JavaScript
|
|
117
|
+
|
|
118
|
+
TypeScript uses ESNext. The theme defaults `vite.build.target` to `"esnext"` to retain modern syntax with minimal compatibility transforms; an explicit consumer target takes precedence. This targets modern browsers, does not polyfill missing Web APIs, and does not move ordinary scripts onto the GPU. Biome enforces `noVar` and `useConst` as errors in maintained source; dependencies and generated bundles are outside this source policy.
|
|
119
|
+
|
|
120
|
+
### Initial CSS and search styles
|
|
121
|
+
|
|
122
|
+
The theme defaults to Astro's `build.inlineStylesheets: "always"`, placing route styles in the HTML to reduce stylesheet requests before the first paint. An explicit consumer setting of `build.inlineStylesheets: "never"` keeps external stylesheets. Inlining increases each HTML response in exchange for a shorter stylesheet request chain on a first visit.
|
|
123
|
+
|
|
124
|
+
Pagefind dialog CSS is emitted as a separate hashed static file with a `prefetch` hint. Opening search activates the stylesheet and waits for it before displaying the dialog. It does not block the initial content or depend on JavaScript generating a CSS asset. A `<noscript>` stylesheet fallback is included.
|
|
125
|
+
|
|
112
126
|
---
|
|
113
127
|
|
|
114
128
|
## 🎨 Component Override
|
|
@@ -156,11 +170,17 @@ id: head
|
|
|
156
170
|
bingClarityId: "YOUR_CLARITY_PROJECT_ID"
|
|
157
171
|
```
|
|
158
172
|
|
|
159
|
-
|
|
173
|
+
> The Partytown migration in this branch is an unpublished candidate. Representative route measurements show lower TBT, but LCP regresses on the homepage and category page, so performance validation is incomplete. The following describes the candidate implementation.
|
|
174
|
+
|
|
175
|
+
`stalux()` includes Partytown. The Google Analytics, Clarity, and Umami scripts run in a Web Worker without an additional consumer integration. Page tracking follows `astro:page-load`; browsers with `Element.moveBefore()` preserve the worker during navigation. Vercount loads asynchronously in the main window, avoiding worker startup and DOM bridge delays for visible counters. It runs once after each `astro:page-load` to bind incoming page counters and use the current URL and first-party visitor cookie.
|
|
176
|
+
|
|
177
|
+
For Google Analytics, turn off **Page changes based on browser history events** under **Admin → Data streams → Enhanced measurement → Page views → Advanced settings**. Stalux sets `send_page_view: false` and sends the URL and title for each navigation explicitly. GA's history tracking must also be disabled in the property to prevent duplicate soft-navigation pageviews. Other enhanced measurement options can remain enabled. See [Google's pageview documentation](https://developers.google.com/analytics/devguides/collection/ga4/views).
|
|
178
|
+
|
|
179
|
+
`bingClarityId` is the historical Stalux field name for a Microsoft Clarity Project ID. Get the ID from the Clarity project under **Settings → Setup → Get tracking code**. Stalux loads the tracking code through Partytown and keeps one loader in browsers that support preserving the worker. Do not install the same project again through `anyhead`, a tag manager, or another plugin.
|
|
160
180
|
|
|
161
181
|
The Project ID is a public browser identifier. Never put a Clarity Data Export API token in this YAML or in client-side code. If the site uses a strict CSP, consent banner, or CMP, configure those host-site policies and signals separately; this theme does not make legal compliance decisions for the site.
|
|
162
182
|
|
|
163
|
-
After deployment, verify the script URL contains the exact Project ID and that the browser sends requests
|
|
183
|
+
After deployment, verify the script URL contains the exact Project ID and that the browser sends Clarity `/collect` requests (the collection host may be a regional endpoint such as `a.clarity.ms`).
|
|
164
184
|
|
|
165
185
|
---
|
|
166
186
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xingwangzhe/stalux",
|
|
3
|
-
"version": "1.29.
|
|
3
|
+
"version": "1.29.8",
|
|
4
4
|
"description": "A powerful, modern Astro blog theme — use as template or install as plugin",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"astro",
|
|
@@ -94,6 +94,7 @@
|
|
|
94
94
|
},
|
|
95
95
|
"dependencies": {
|
|
96
96
|
"@astrojs/markdown-satteri": "0.4.2",
|
|
97
|
+
"@astrojs/partytown": "2.1.8",
|
|
97
98
|
"@astrojs/rss": "^4.0.19",
|
|
98
99
|
"@astrojs/sitemap": "^3.7.4",
|
|
99
100
|
"@expressive-code/plugin-line-numbers": "^0.44.2",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<script src="../../../scripts/
|
|
1
|
+
<script src="../../../scripts/analytics-runtime.ts"></script>
|
|
@@ -65,24 +65,36 @@ const archiveYears = [...archivesByYearMonth.entries()]
|
|
|
65
65
|
</div>
|
|
66
66
|
</div>
|
|
67
67
|
{archiveYear.months.map(({ month, posts: monthPosts }) => (
|
|
68
|
-
<div
|
|
68
|
+
<div
|
|
69
|
+
class={styles.monthSection}
|
|
70
|
+
style={{
|
|
71
|
+
"--archive-estimate": `${Math.ceil(monthPosts.length / 12) * 12 * 48 + 40}px`,
|
|
72
|
+
}}
|
|
73
|
+
>
|
|
69
74
|
<h3 class={styles.monthTitle}>{t("archives.month", { n: month })}</h3>
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
data
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
75
|
+
{Array.from({ length: Math.ceil(monthPosts.length / 12) }, (_, index) =>
|
|
76
|
+
monthPosts.slice(index * 12, (index + 1) * 12),
|
|
77
|
+
).map((group) => (
|
|
78
|
+
<ul
|
|
79
|
+
class={styles.postList}
|
|
80
|
+
style={{ "--archive-group-estimate": `${group.length * 48}px` }}
|
|
81
|
+
>
|
|
82
|
+
{group.map((post) => (
|
|
83
|
+
<li class={styles.postItem}>
|
|
84
|
+
<time class={styles.postDate} datetime={post.data.date}>
|
|
85
|
+
{format(parseDate(post.data.date), "yyyy-MM-dd")}
|
|
86
|
+
</time>
|
|
87
|
+
<a
|
|
88
|
+
href={`/posts/${post.data.abbrlink}/`}
|
|
89
|
+
class={styles.postLink}
|
|
90
|
+
data-astro-prefetch="viewport"
|
|
91
|
+
>
|
|
92
|
+
{post.data.title}
|
|
93
|
+
</a>
|
|
94
|
+
</li>
|
|
95
|
+
))}
|
|
96
|
+
</ul>
|
|
97
|
+
))}
|
|
86
98
|
</div>
|
|
87
99
|
))}
|
|
88
100
|
</div>
|
|
@@ -6,9 +6,9 @@ import { getCategoryCountList } from "@utils/collections-stats";
|
|
|
6
6
|
const categoryList = await getCategoryCountList(Astro.logger);
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
-
<ul class={
|
|
9
|
+
<ul class={styles.categoriesList}>
|
|
10
10
|
{categoryList.map((category) => (
|
|
11
|
-
<li
|
|
11
|
+
<li>
|
|
12
12
|
<CategoriesCard category={category} />
|
|
13
13
|
</li>
|
|
14
14
|
))}
|
|
@@ -15,12 +15,18 @@ const siteConfig = getSiteData(configEntries);
|
|
|
15
15
|
const { t } = createTranslator(siteConfig.lang || "zh-CN");
|
|
16
16
|
|
|
17
17
|
function badgeImg(opts: BadgeOptions) {
|
|
18
|
-
const
|
|
18
|
+
const svg = generateBadge(opts, Astro.logger);
|
|
19
|
+
const src = svgToDataUrl(svg);
|
|
19
20
|
const alt = opts.alt || `${opts.label}: ${opts.message}`;
|
|
21
|
+
const width = svg.match(/<svg\b[^>]*\bwidth="(\d+)"/)?.[1];
|
|
22
|
+
const height = svg.match(/<svg\b[^>]*\bheight="(\d+)"/)?.[1];
|
|
23
|
+
const dimensions = width && height ? ` width="${width}" height="${height}"` : "";
|
|
24
|
+
// Closed badge groups should not initialize every SVG document during first paint.
|
|
25
|
+
const img = `<img src="${src}" alt="${alt}" class="badge" loading="lazy" decoding="async"${dimensions} />`;
|
|
20
26
|
if (opts.href) {
|
|
21
|
-
return `<a href="${opts.href}" target="_blank" rel="${opts.rel ?? "noopener"}"
|
|
27
|
+
return `<a href="${opts.href}" target="_blank" rel="${opts.rel ?? "noopener"}">${img}</a>`;
|
|
22
28
|
}
|
|
23
|
-
return
|
|
29
|
+
return img;
|
|
24
30
|
}
|
|
25
31
|
---
|
|
26
32
|
|
|
@@ -63,6 +69,11 @@ function badgeImg(opts: BadgeOptions) {
|
|
|
63
69
|
gap: 0.5rem;
|
|
64
70
|
}
|
|
65
71
|
|
|
72
|
+
/* Badge HTML is emitted by set:html, so target it through the scoped wrapper. */
|
|
73
|
+
.badges-wrapper :global(.badge) {
|
|
74
|
+
width: auto;
|
|
75
|
+
}
|
|
76
|
+
|
|
66
77
|
.badge-group {
|
|
67
78
|
width: 100%;
|
|
68
79
|
border: none;
|
|
@@ -4,11 +4,11 @@ import { getCollection } from "astro:content";
|
|
|
4
4
|
import { ClientRouter } from "astro:transitions";
|
|
5
5
|
import BingClarity from "@components/stalux/analytics/clarity.astro";
|
|
6
6
|
import GoogleAnalytics from "@components/stalux/analytics/google.astro";
|
|
7
|
-
import VerCount from "@components/stalux/common/verCount.astro";
|
|
8
|
-
import { SEO } from "astro-seo";
|
|
9
|
-
import packageJson from "../../../../package.json";
|
|
10
|
-
import "@styles/base/init.css";
|
|
11
7
|
import Umami from "@components/stalux/analytics/umami.astro";
|
|
8
|
+
import VerCount from "@components/stalux/common/verCount.astro";
|
|
9
|
+
// Emit a stable asset even while the search component JavaScript stays lazy.
|
|
10
|
+
// The closed search dialog does not need to block the page's first paint.
|
|
11
|
+
import pagefindStyles from "@pagefind/component-ui/css?url";
|
|
12
12
|
import {
|
|
13
13
|
getAuthorData,
|
|
14
14
|
getHeadData,
|
|
@@ -17,6 +17,8 @@ import {
|
|
|
17
17
|
getSiteData,
|
|
18
18
|
} from "@utils/config-utils";
|
|
19
19
|
import { buildSeoDocument, type SeoPageInput } from "@utils/seo-document";
|
|
20
|
+
import { SEO } from "astro-seo";
|
|
21
|
+
import packageJson from "../../../../package.json";
|
|
20
22
|
|
|
21
23
|
const configEntries = await getCollection("config");
|
|
22
24
|
const siteConfig = getSiteData(configEntries);
|
|
@@ -56,6 +58,8 @@ const ldJson = JSON.stringify(seo.schema).replace(/<\//g, "<\\/");
|
|
|
56
58
|
/>
|
|
57
59
|
<!-- 代码字体由 Astro Fonts API 注入;正文按页面生成 native CJK 子集。 -->
|
|
58
60
|
<Font cssVariable="--font-code" />
|
|
61
|
+
<link rel="prefetch" as="style" href={pagefindStyles} data-stalux-search-style>
|
|
62
|
+
<noscript><link rel="stylesheet" href={pagefindStyles}></noscript>
|
|
59
63
|
{headConfig?.anyhead && <Fragment set:html={headConfig.anyhead} />}
|
|
60
64
|
{headConfig?.googleAnalyticsId && <GoogleAnalytics />}
|
|
61
65
|
{headConfig?.bingClarityId && <BingClarity />}
|
|
@@ -9,6 +9,10 @@ const configEntries = await getCollection("config");
|
|
|
9
9
|
const siteConfig = getSiteData(configEntries);
|
|
10
10
|
const { t } = createTranslator(siteConfig.lang || "zh-CN");
|
|
11
11
|
const tagList = await getTagCountList(Astro.logger);
|
|
12
|
+
const tagGroups = Array.from({ length: Math.ceil(tagList.length / 32) }, (_, index) =>
|
|
13
|
+
tagList.slice(index * 32, (index + 1) * 32),
|
|
14
|
+
);
|
|
15
|
+
const cloudDensity = Math.sqrt(Math.max(tagList.length, 100) / 100);
|
|
12
16
|
const tagLinks = tagList.map((t) => ({
|
|
13
17
|
type: "link" as const,
|
|
14
18
|
text: t.name,
|
|
@@ -19,17 +23,21 @@ const tagLinks = tagList.map((t) => ({
|
|
|
19
23
|
<!-- 移动端:SSR 卡片 -->
|
|
20
24
|
<div class={styles.cards} id="tags-cards">
|
|
21
25
|
{tagList.length > 0 ? (
|
|
22
|
-
|
|
23
|
-
<
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
26
|
+
tagGroups.map((group) => (
|
|
27
|
+
<div class={styles.tagGroup}>
|
|
28
|
+
{group.map(({ name, count }) => (
|
|
29
|
+
<a
|
|
30
|
+
href={`/tags/${name}/`}
|
|
31
|
+
class={styles.tagCard}
|
|
32
|
+
data-stalux-tag-card
|
|
33
|
+
data-count={count}
|
|
34
|
+
data-astro-prefetch
|
|
35
|
+
>
|
|
36
|
+
<span class={styles.tagName}>{name}</span>
|
|
37
|
+
<span class={styles.tagCount}>{count}</span>
|
|
38
|
+
</a>
|
|
39
|
+
))}
|
|
40
|
+
</div>
|
|
33
41
|
))
|
|
34
42
|
) : (
|
|
35
43
|
<div class={styles.noTags}>{t("tags.noTags")}</div>
|
|
@@ -37,7 +45,9 @@ const tagLinks = tagList.map((t) => ({
|
|
|
37
45
|
</div>
|
|
38
46
|
|
|
39
47
|
<!-- 桌面端:Canvas 标签云 -->
|
|
40
|
-
<div class={styles.
|
|
48
|
+
<div class={styles.canvasFrame} style={{ "--cloud-density": cloudDensity }}>
|
|
49
|
+
<div class={styles.canvas} id="tags-canvas" data-tags={JSON.stringify(tagLinks)}></div>
|
|
50
|
+
</div>
|
|
41
51
|
|
|
42
52
|
<script>
|
|
43
53
|
import "../../../scripts/tags-cloud";
|
|
@@ -18,7 +18,7 @@ const sorted = [...entries].sort((a, b) => {
|
|
|
18
18
|
});
|
|
19
19
|
---
|
|
20
20
|
|
|
21
|
-
<ul class={
|
|
21
|
+
<ul class={styles.list}>
|
|
22
22
|
{sorted.map((entry) => {
|
|
23
23
|
const hash = entry.data.abbrlink ? `#word-${entry.data.abbrlink}` : undefined;
|
|
24
24
|
return (
|
package/src/index.ts
CHANGED
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
} from "node:fs";
|
|
19
19
|
import path from "node:path";
|
|
20
20
|
import { fileURLToPath } from "node:url";
|
|
21
|
-
|
|
21
|
+
import partytown from "@astrojs/partytown";
|
|
22
22
|
import sitemap from "@astrojs/sitemap";
|
|
23
23
|
import { mermaidHast, mermaidMdast } from "@xingwangzhe/satteri-mermaid";
|
|
24
24
|
import { photoswipe } from "@xingwangzhe/satteri-photoswipe";
|
|
@@ -179,7 +179,14 @@ export function stalux(options: StaluxOptions = {}): AstroIntegration[] {
|
|
|
179
179
|
|
|
180
180
|
// 1. 注入 Vite 别名 + 组件覆盖插件 + Vue 特性标记(Waline 依赖)
|
|
181
181
|
updateConfig({
|
|
182
|
+
// Keep route CSS on the first-response path. Search UI CSS is
|
|
183
|
+
// a separate prefetched asset; respect explicit external CSS.
|
|
184
|
+
build: {
|
|
185
|
+
inlineStylesheets:
|
|
186
|
+
config.build.inlineStylesheets === "never" ? "never" : "always",
|
|
187
|
+
},
|
|
182
188
|
vite: {
|
|
189
|
+
build: { target: config.vite.build?.target ?? "esnext" },
|
|
183
190
|
resolve: {
|
|
184
191
|
alias: createViteAliases(srcDir),
|
|
185
192
|
},
|
|
@@ -458,8 +465,13 @@ export function stalux(options: StaluxOptions = {}): AstroIntegration[] {
|
|
|
458
465
|
};
|
|
459
466
|
|
|
460
467
|
// 打包内置集成:Astro 的 integrations 配置支持嵌套数组并自动展平(schema 里 val.flat(Infinity)),
|
|
461
|
-
// 因此这里返回 [coreIntegration, sitemap, expressiveCode],让消费方一行 `integrations: [stalux()]` 即可完成配置。
|
|
462
|
-
const bundled: AstroIntegration[] = [
|
|
468
|
+
// 因此这里返回 [coreIntegration, partytown, sitemap, expressiveCode],让消费方一行 `integrations: [stalux()]` 即可完成配置。
|
|
469
|
+
const bundled: AstroIntegration[] = [
|
|
470
|
+
coreIntegration,
|
|
471
|
+
partytown({
|
|
472
|
+
config: { forward: ["dataLayer.push", "gtag"], mainWindowAccessors: ["CSS"] },
|
|
473
|
+
}),
|
|
474
|
+
];
|
|
463
475
|
|
|
464
476
|
if (opt.sitemap !== false) {
|
|
465
477
|
const userSitemap = opt.sitemap === true ? undefined : opt.sitemap;
|
package/src/layouts/Stalux.astro
CHANGED
|
@@ -2,11 +2,6 @@
|
|
|
2
2
|
import { getCollection } from "astro:content";
|
|
3
3
|
import Footer from "@components/stalux/layout/footer.astro";
|
|
4
4
|
import Head from "@components/stalux/layout/head.astro";
|
|
5
|
-
// Keep Pagefind styles on the layout's static CSS graph. The component JavaScript
|
|
6
|
-
// remains lazy-loaded when search opens, but preview must always have stable CSS.
|
|
7
|
-
import "@pagefind/component-ui/css";
|
|
8
|
-
import "@styles/base/init.css";
|
|
9
|
-
import "@styles/shared/stagger.css";
|
|
10
5
|
import "@styles/shared/view-transitions.css";
|
|
11
6
|
import { getAccentColorStyle } from "@utils/accent-color";
|
|
12
7
|
import {
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keep analytics trackers in Partytown. This small main-thread bridge owns the
|
|
3
|
+
* Astro navigation lifecycle and tells the worker about each completed page.
|
|
4
|
+
*/
|
|
5
|
+
declare global {
|
|
6
|
+
interface Window {
|
|
7
|
+
__staluxAnalyticsRuntime?: {
|
|
8
|
+
installed?: boolean;
|
|
9
|
+
googleId?: string;
|
|
10
|
+
googleInitialized?: boolean;
|
|
11
|
+
clarityId?: string;
|
|
12
|
+
umamiKey?: string;
|
|
13
|
+
previousPage?: string;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
window.__staluxAnalyticsRuntime ??= {};
|
|
19
|
+
const state = window.__staluxAnalyticsRuntime;
|
|
20
|
+
|
|
21
|
+
function appendWorkerInline(code: string): void {
|
|
22
|
+
const script = document.createElement("script");
|
|
23
|
+
script.type = "text/partytown";
|
|
24
|
+
script.textContent = code;
|
|
25
|
+
document.head.appendChild(script);
|
|
26
|
+
window.dispatchEvent(new CustomEvent("ptupdate"));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function appendWorkerScript(src: string, attributes: Record<string, string> = {}): void {
|
|
30
|
+
const script = document.createElement("script");
|
|
31
|
+
script.type = "text/partytown";
|
|
32
|
+
script.src = src;
|
|
33
|
+
for (const [name, value] of Object.entries(attributes)) script.setAttribute(name, value);
|
|
34
|
+
document.head.appendChild(script);
|
|
35
|
+
window.dispatchEvent(new CustomEvent("ptupdate"));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function initializeTrackers(): void {
|
|
39
|
+
const config = document.body?.dataset;
|
|
40
|
+
if (!config) return;
|
|
41
|
+
|
|
42
|
+
if (config.staluxGaId) {
|
|
43
|
+
state.googleId = config.staluxGaId;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (config.staluxClarityId && state.clarityId !== config.staluxClarityId) {
|
|
47
|
+
state.clarityId = config.staluxClarityId;
|
|
48
|
+
appendWorkerInline(`
|
|
49
|
+
window.clarity = window.clarity || function () {
|
|
50
|
+
(window.clarity.q = window.clarity.q || []).push(arguments);
|
|
51
|
+
};
|
|
52
|
+
window.clarity.q = window.clarity.q || [];
|
|
53
|
+
`);
|
|
54
|
+
appendWorkerScript(`https://www.clarity.ms/tag/${encodeURIComponent(state.clarityId)}`, {
|
|
55
|
+
"data-stalux-clarity": "true",
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (
|
|
60
|
+
config.staluxUmamiId &&
|
|
61
|
+
config.staluxUmamiUrl &&
|
|
62
|
+
state.umamiKey !== `${config.staluxUmamiId}\n${config.staluxUmamiUrl}`
|
|
63
|
+
) {
|
|
64
|
+
state.umamiKey = `${config.staluxUmamiId}\n${config.staluxUmamiUrl}`;
|
|
65
|
+
appendWorkerScript(config.staluxUmamiUrl, {
|
|
66
|
+
"data-stalux-umami": "true",
|
|
67
|
+
"data-website-id": config.staluxUmamiId,
|
|
68
|
+
"data-auto-pageview": "false",
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function trackGooglePage(): void {
|
|
74
|
+
if (!state.googleId) return;
|
|
75
|
+
const page = {
|
|
76
|
+
page_location: window.location.href,
|
|
77
|
+
page_path: window.location.pathname + window.location.search,
|
|
78
|
+
page_title: document.title,
|
|
79
|
+
};
|
|
80
|
+
const initialize = !state.googleInitialized;
|
|
81
|
+
state.googleInitialized = true;
|
|
82
|
+
appendWorkerInline(`
|
|
83
|
+
// Partytown 0.14 serializes HTMLCollection as its worker NodeList.
|
|
84
|
+
// GA user-data detection checks instanceof HTMLCollection before sending page_view.
|
|
85
|
+
window.HTMLCollection = window.HTMLCollection || window.NodeList;
|
|
86
|
+
window.dataLayer = window.dataLayer || [];
|
|
87
|
+
window.gtag = function () { window.dataLayer.push(arguments); };
|
|
88
|
+
${initialize ? `window.gtag("js", new Date());window.gtag("config", ${JSON.stringify(state.googleId)}, { send_page_view: false });` : ""}
|
|
89
|
+
window.gtag("event", "page_view", ${JSON.stringify(page)});
|
|
90
|
+
`);
|
|
91
|
+
if (initialize)
|
|
92
|
+
appendWorkerScript(
|
|
93
|
+
`https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(state.googleId)}`,
|
|
94
|
+
{ "data-stalux-google": "true" },
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function trackUmamiPage(): void {
|
|
99
|
+
const id = document.body?.dataset.staluxUmamiId;
|
|
100
|
+
if (!id || !state.umamiKey) return;
|
|
101
|
+
const payload = JSON.stringify({
|
|
102
|
+
website: id,
|
|
103
|
+
url: window.location.href,
|
|
104
|
+
title: document.title,
|
|
105
|
+
hostname: window.location.hostname,
|
|
106
|
+
screen: `${window.screen.width}x${window.screen.height}`,
|
|
107
|
+
language: navigator.language,
|
|
108
|
+
referrer: state.previousPage ?? document.referrer,
|
|
109
|
+
});
|
|
110
|
+
appendWorkerInline(`
|
|
111
|
+
(function send(attempt) {
|
|
112
|
+
if (window.umami?.track) window.umami.track(${payload});
|
|
113
|
+
else if (attempt < 50) setTimeout(() => send(attempt + 1), 100);
|
|
114
|
+
})(0);
|
|
115
|
+
`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function trackVercountPage(): void {
|
|
119
|
+
// Visible counters need the current window's URL, cookie and DOM nodes.
|
|
120
|
+
// Avoid worker startup and synchronous DOM bridges; bind fresh nodes once
|
|
121
|
+
// per completed Astro navigation using the small upstream async script.
|
|
122
|
+
document.querySelector("script[data-stalux-vercount]")?.remove();
|
|
123
|
+
const script = document.createElement("script");
|
|
124
|
+
script.async = true;
|
|
125
|
+
script.src = "https://events.vercount.one/js";
|
|
126
|
+
script.dataset.staluxVercount = "true";
|
|
127
|
+
document.head.appendChild(script);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (!state.installed) {
|
|
131
|
+
state.installed = true;
|
|
132
|
+
let sandbox: HTMLIFrameElement | undefined;
|
|
133
|
+
// Moving an iframe with append() reloads its browsing context. Park it outside
|
|
134
|
+
// the replaced body with moveBefore(), which preserves the running worker.
|
|
135
|
+
// Capture runs before @astrojs/partytown's iframe recreation handler.
|
|
136
|
+
document.addEventListener(
|
|
137
|
+
"astro:before-swap",
|
|
138
|
+
() => {
|
|
139
|
+
const frame = document.body.querySelector<HTMLIFrameElement>(
|
|
140
|
+
'iframe[src*="~partytown/"]',
|
|
141
|
+
);
|
|
142
|
+
if (frame && typeof document.documentElement.moveBefore === "function") {
|
|
143
|
+
sandbox = frame;
|
|
144
|
+
document.documentElement.moveBefore(frame, null);
|
|
145
|
+
} else if (!window.crossOriginIsolated) {
|
|
146
|
+
// Atomics mode has no sandbox iframe and keeps its worker alive.
|
|
147
|
+
state.googleInitialized = false;
|
|
148
|
+
state.clarityId = undefined;
|
|
149
|
+
state.umamiKey = undefined;
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
true,
|
|
153
|
+
);
|
|
154
|
+
document.addEventListener("astro:after-swap", () => {
|
|
155
|
+
if (sandbox) {
|
|
156
|
+
document.body.moveBefore(sandbox, null);
|
|
157
|
+
sandbox = undefined;
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
document.addEventListener("astro:page-load", () => {
|
|
161
|
+
initializeTrackers();
|
|
162
|
+
trackGooglePage();
|
|
163
|
+
trackUmamiPage();
|
|
164
|
+
trackVercountPage();
|
|
165
|
+
state.previousPage = window.location.href;
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export {};
|
package/src/scripts/date-time.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { registerPageLifecycle } from "./page-runtime";
|
|
2
|
+
import { observeVisibleClock } from "./visible-clock";
|
|
2
3
|
|
|
3
4
|
const pad2 = (value: number) => String(value).padStart(2, "0");
|
|
4
5
|
|
|
@@ -26,15 +27,18 @@ registerPageLifecycle("date-time", () => {
|
|
|
26
27
|
const timeElement = document.querySelector<HTMLTimeElement>('[data-ref="time"]');
|
|
27
28
|
if (!dateElement || !timeElement) return;
|
|
28
29
|
const lang = document.documentElement.lang || "zh-CN";
|
|
30
|
+
let lastDate = "";
|
|
29
31
|
const update = () => {
|
|
30
32
|
const now = new Date();
|
|
31
33
|
const time = toMachineTime(now);
|
|
32
|
-
|
|
33
|
-
|
|
34
|
+
const date = toMachineDate(now);
|
|
35
|
+
if (date !== lastDate) {
|
|
36
|
+
dateElement.textContent = toDisplayDate(now, lang);
|
|
37
|
+
dateElement.dateTime = date;
|
|
38
|
+
lastDate = date;
|
|
39
|
+
}
|
|
34
40
|
timeElement.textContent = time;
|
|
35
41
|
timeElement.dateTime = time;
|
|
36
42
|
};
|
|
37
|
-
update
|
|
38
|
-
const timer = window.setInterval(update, 1_000);
|
|
39
|
-
return () => window.clearInterval(timer);
|
|
43
|
+
return observeVisibleClock(timeElement, update);
|
|
40
44
|
});
|