@xingwangzhe/stalux 1.29.5 → 1.29.7

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.
Files changed (40) hide show
  1. package/README.md +23 -3
  2. package/package.json +4 -2
  3. package/src/components/stalux/analytics/clarity.astro +1 -1
  4. package/src/components/stalux/analytics/google.astro +1 -1
  5. package/src/components/stalux/analytics/umami.astro +1 -1
  6. package/src/components/stalux/archives/archivesList.astro +27 -17
  7. package/src/components/stalux/categories/categoriesList.astro +2 -2
  8. package/src/components/stalux/common/verCount.astro +1 -1
  9. package/src/components/stalux/footer/FooterBadges.astro +14 -3
  10. package/src/components/stalux/layout/head.astro +8 -4
  11. package/src/components/stalux/links/linkList.astro +1 -1
  12. package/src/components/stalux/tags/tagsCloud.astro +22 -12
  13. package/src/components/stalux/words/wordList.astro +1 -1
  14. package/src/index.ts +67 -5
  15. package/src/layouts/Stalux.astro +0 -5
  16. package/src/scripts/analytics-runtime.ts +166 -0
  17. package/src/scripts/core-runtime.ts +0 -1
  18. package/src/scripts/date-time.ts +9 -5
  19. package/src/scripts/navigation.ts +48 -25
  20. package/src/scripts/runtime-counter.ts +2 -3
  21. package/src/scripts/search-dialog.ts +24 -0
  22. package/src/scripts/tags-cloud.ts +63 -29
  23. package/src/scripts/visible-clock.ts +24 -0
  24. package/src/scripts/waline.ts +1 -1
  25. package/src/styles/components/archives/archivesList.module.css +15 -87
  26. package/src/styles/components/categories/categoriesCard.module.css +3 -8
  27. package/src/styles/components/categories/categoriesList.module.css +7 -8
  28. package/src/styles/components/links/linkCard.module.css +0 -2
  29. package/src/styles/components/links/linkList.module.css +3 -3
  30. package/src/styles/components/posts/postAuthor.module.css +0 -1
  31. package/src/styles/components/posts/postCard.module.css +3 -12
  32. package/src/styles/components/tags/tagHeader.module.css +31 -31
  33. package/src/styles/components/tags/tagsCloud.module.css +34 -50
  34. package/src/utils/content-index.ts +1 -1
  35. package/src/utils/feed.ts +1 -1
  36. package/src/scripts/after-load.ts +0 -27
  37. package/src/scripts/clarity.ts +0 -48
  38. package/src/scripts/google-analytics.ts +0 -90
  39. package/src/scripts/umami.ts +0 -28
  40. package/src/scripts/vercount.ts +0 -38
package/README.md CHANGED
@@ -26,7 +26,7 @@ bunx stalux init # Generate stalux/ content directory
26
26
  Requires Node.js 26 or later. Then configure `astro.config.ts`. All plugins are bundled into the Stalux integration by default — **no manual configuration is needed**:
27
27
 
28
28
  - **Markdown**: Mermaid (MDAST detection + HAST/SVG rendering), math formulas (Temml → MathML), word count / feature flags, and PhotoSwipe image lightbox are injected into the default `satteri()` processor automatically (math / frontmatter / gfm / smart punctuation are enabled by default).
29
- - **Sitemap**: `@astrojs/sitemap` is bundled (`.md` source endpoints are filtered out by default).
29
+ - **Sitemap**: `@astrojs/sitemap` is bundled (published post `/posts/*.md` endpoints are included when `promote.export_md` is enabled; other Markdown mirrors stay filtered by default).
30
30
  - **Expressive Code**: bundled with line numbers enabled by default.
31
31
 
32
32
  ```ts
@@ -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
- `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 injects the asynchronous tracking code into `<head>` and keeps one loader during Astro View Transitions. Do not install the same project again through `anyhead`, a tag manager, or another plugin.
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, Umami, and Vercount 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 binds its counters again after the incoming page DOM is ready.
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 to `https://www.clarity.ms/collect`.
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.5",
3
+ "version": "1.29.7",
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",
@@ -114,7 +115,8 @@
114
115
  "photoswipe": "^5.4.4",
115
116
  "satteri": "^0.10.5",
116
117
  "simple-icons": "^16.32.0",
117
- "temml": "^0.13.5"
118
+ "temml": "^0.13.5",
119
+ "yaml": "^2.9.1"
118
120
  },
119
121
  "devDependencies": {
120
122
  "@astrojs/check": "0.9.10",
@@ -1,4 +1,4 @@
1
1
  ---
2
2
  ---
3
3
 
4
- <script src="../../../scripts/clarity.ts"></script>
4
+ <script src="../../../scripts/analytics-runtime.ts"></script>
@@ -1,4 +1,4 @@
1
1
  ---
2
2
  ---
3
3
 
4
- <script src="../../../scripts/google-analytics.ts"></script>
4
+ <script src="../../../scripts/analytics-runtime.ts"></script>
@@ -1 +1 @@
1
- <script src="../../../scripts/umami.ts"></script>
1
+ <script src="../../../scripts/analytics-runtime.ts"></script>
@@ -65,24 +65,34 @@ const archiveYears = [...archivesByYearMonth.entries()]
65
65
  </div>
66
66
  </div>
67
67
  {archiveYear.months.map(({ month, posts: monthPosts }) => (
68
- <div class={styles.monthSection}>
68
+ <div
69
+ class={styles.monthSection}
70
+ style={{ "--archive-estimate": `${Math.ceil(monthPosts.length / 12) * 12 * 48 + 40}px` }}
71
+ >
69
72
  <h3 class={styles.monthTitle}>{t("archives.month", { n: month })}</h3>
70
- <ul class={styles.postList}>
71
- {monthPosts.map((post) => (
72
- <li class={styles.postItem}>
73
- <time class={styles.postDate} datetime={post.data.date}>
74
- {format(parseDate(post.data.date), "yyyy-MM-dd")}
75
- </time>
76
- <a
77
- href={`/posts/${post.data.abbrlink}/`}
78
- class={styles.postLink}
79
- data-astro-prefetch="viewport"
80
- >
81
- {post.data.title}
82
- </a>
83
- </li>
84
- ))}
85
- </ul>
73
+ {Array.from({ length: Math.ceil(monthPosts.length / 12) }, (_, index) =>
74
+ monthPosts.slice(index * 12, (index + 1) * 12),
75
+ ).map((group) => (
76
+ <ul
77
+ class={styles.postList}
78
+ style={{ "--archive-group-estimate": `${group.length * 48}px` }}
79
+ >
80
+ {group.map((post) => (
81
+ <li class={styles.postItem}>
82
+ <time class={styles.postDate} datetime={post.data.date}>
83
+ {format(parseDate(post.data.date), "yyyy-MM-dd")}
84
+ </time>
85
+ <a
86
+ href={`/posts/${post.data.abbrlink}/`}
87
+ class={styles.postLink}
88
+ data-astro-prefetch="viewport"
89
+ >
90
+ {post.data.title}
91
+ </a>
92
+ </li>
93
+ ))}
94
+ </ul>
95
+ ))}
86
96
  </div>
87
97
  ))}
88
98
  </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={`${styles.categoriesList} stagger-children`}>
9
+ <ul class={styles.categoriesList}>
10
10
  {categoryList.map((category) => (
11
- <li class={styles.item}>
11
+ <li>
12
12
  <CategoriesCard category={category} />
13
13
  </li>
14
14
  ))}
@@ -1,4 +1,4 @@
1
1
  ---
2
2
  ---
3
3
 
4
- <Fragment />
4
+ <script src="../../../scripts/analytics-runtime.ts"></script>
@@ -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 src = svgToDataUrl(generateBadge(opts, Astro.logger));
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"}"><img src="${src}" alt="${alt}" class="badge" /></a>`;
27
+ return `<a href="${opts.href}" target="_blank" rel="${opts.rel ?? "noopener"}">${img}</a>`;
22
28
  }
23
- return `<img src="${src}" alt="${alt}" class="badge" />`;
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,7 +9,7 @@ const linksConfig = getLinksData(configEntries);
9
9
  const links = linksConfig?.sites ?? [];
10
10
  ---
11
11
 
12
- <ul class={`${styles.list} stagger-children`}>
12
+ <ul class={styles.list}>
13
13
  {links.map((link) => (
14
14
  <li class={styles.item}>
15
15
  <LinkCard link={link} />
@@ -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
- tagList.map(({ name, count }) => (
23
- <a
24
- href={`/tags/${name}/`}
25
- class={styles.tagCard}
26
- data-stalux-tag-card
27
- data-count={count}
28
- data-astro-prefetch
29
- >
30
- <span class={styles.tagName}>{name}</span>
31
- <span class={styles.tagCount}>{count}</span>
32
- </a>
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.canvas} id="tags-canvas" data-tags={JSON.stringify(tagLinks)}></div>
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={`${styles.list} stagger-children`}>
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";
@@ -27,6 +27,7 @@ import { fontProviders } from "astro/config";
27
27
  // pagefind 是 ESM-only 包,需要在模块顶层导入
28
28
  // 因为 astro:build:done 钩子中 Vite module runner 已关闭,无法动态 import
29
29
  import { createIndex as pagefindCreateIndex } from "pagefind";
30
+ import { parse as parseYaml } from "yaml";
30
31
  import type { StaluxOptions } from "./config";
31
32
  import { expressiveCode } from "./expressive-code";
32
33
  import { staluxComponentsAlias } from "./internal/components-plugin";
@@ -178,7 +179,14 @@ export function stalux(options: StaluxOptions = {}): AstroIntegration[] {
178
179
 
179
180
  // 1. 注入 Vite 别名 + 组件覆盖插件 + Vue 特性标记(Waline 依赖)
180
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
+ },
181
188
  vite: {
189
+ build: { target: config.vite.build?.target ?? "esnext" },
182
190
  resolve: {
183
191
  alias: createViteAliases(srcDir),
184
192
  },
@@ -457,20 +465,74 @@ export function stalux(options: StaluxOptions = {}): AstroIntegration[] {
457
465
  };
458
466
 
459
467
  // 打包内置集成:Astro 的 integrations 配置支持嵌套数组并自动展平(schema 里 val.flat(Infinity)),
460
- // 因此这里返回 [coreIntegration, sitemap, expressiveCode],让消费方一行 `integrations: [stalux()]` 即可完成配置。
461
- const bundled: AstroIntegration[] = [coreIntegration];
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
+ ];
462
475
 
463
476
  if (opt.sitemap !== false) {
464
477
  const userSitemap = opt.sitemap === true ? undefined : opt.sitemap;
465
- // 默认过滤:不把 Markdown 源码端点(/posts/*.md)写入 sitemap;用户自定义 filter 与之叠加
466
- const defaultFilter = (page: string) => !page.endsWith(".md");
478
+ const contentDir = opt.contentDir ?? "stalux";
479
+ const promotePath = path.join(process.cwd(), contentDir, "config", "promote.yml");
480
+ const promoteConfig = existsSync(promotePath)
481
+ ? (parseYaml(readFileSync(promotePath, "utf8")) as { export_md?: boolean } | undefined)
482
+ : undefined;
483
+ const exportMarkdown = promoteConfig?.export_md === true;
484
+ const sitePath = path.join(process.cwd(), contentDir, "config", "site.yml");
485
+ const siteConfig = existsSync(sitePath)
486
+ ? (parseYaml(readFileSync(sitePath, "utf8")) as { url?: string } | undefined)
487
+ : undefined;
488
+ const sitemapSite = opt.site ?? siteConfig?.url;
489
+ const defaultFilter = (page: string) =>
490
+ exportMarkdown || !new URL(page).pathname.endsWith(".md");
467
491
  const userFilter = userSitemap?.filter;
468
492
  const filter = userFilter
469
493
  ? (page: string) => defaultFilter(page) && userFilter(page)
470
494
  : defaultFilter;
495
+ const customPages = [...(userSitemap?.customPages ?? [])];
496
+ if (exportMarkdown) {
497
+ const postsDir = path.resolve(process.cwd(), contentDir, "posts");
498
+ if (existsSync(postsDir)) {
499
+ for (const filename of readdirSync(postsDir)) {
500
+ if (!filename.endsWith(".md") && !filename.endsWith(".mdx")) {
501
+ continue;
502
+ }
503
+ const source = readFileSync(path.join(postsDir, filename), "utf8");
504
+ const opening = source.startsWith("---\r\n")
505
+ ? 5
506
+ : source.startsWith("---\n")
507
+ ? 4
508
+ : 0;
509
+ if (!opening) continue;
510
+ const closing = source.indexOf("\n---", opening);
511
+ if (closing < 0) continue;
512
+ const frontmatterText = source.slice(opening, closing);
513
+ const frontmatter = frontmatterText.endsWith("\r")
514
+ ? frontmatterText.slice(0, -1)
515
+ : frontmatterText;
516
+ const postData = parseYaml(frontmatter) as
517
+ | { abbrlink?: string | number; draft?: boolean }
518
+ | undefined;
519
+ if (postData?.draft || postData?.abbrlink == null) continue;
520
+ if (!sitemapSite) continue;
521
+ const normalizedSite = sitemapSite.endsWith("/")
522
+ ? sitemapSite.slice(0, -1)
523
+ : sitemapSite;
524
+ const url = new URL(
525
+ `posts/${encodeURIComponent(String(postData.abbrlink))}.md`,
526
+ `${normalizedSite}/`,
527
+ );
528
+ customPages.push(url.href);
529
+ }
530
+ }
531
+ }
471
532
  bundled.push(
472
533
  sitemap({
473
534
  ...userSitemap,
535
+ customPages,
474
536
  filter,
475
537
  }),
476
538
  );
@@ -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,166 @@
1
+ /**
2
+ * Keep third-party code 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
+ // Vercount captures its display elements at startup. A new Astro body needs
120
+ // a new worker execution so the counters refer to the incoming DOM.
121
+ document.querySelector("script[data-stalux-vercount]")?.remove();
122
+ appendWorkerScript("https://events.vercount.one/js", {
123
+ "data-stalux-vercount": "true",
124
+ });
125
+ }
126
+
127
+ if (!state.installed) {
128
+ state.installed = true;
129
+ let sandbox: HTMLIFrameElement | undefined;
130
+ // Moving an iframe with append() reloads its browsing context. Park it outside
131
+ // the replaced body with moveBefore(), which preserves the running worker.
132
+ // Capture runs before @astrojs/partytown's iframe recreation handler.
133
+ document.addEventListener(
134
+ "astro:before-swap",
135
+ () => {
136
+ const frame = document.body.querySelector<HTMLIFrameElement>(
137
+ 'iframe[src*="~partytown/"]',
138
+ );
139
+ if (frame && typeof document.documentElement.moveBefore === "function") {
140
+ sandbox = frame;
141
+ document.documentElement.moveBefore(frame, null);
142
+ } else if (!window.crossOriginIsolated) {
143
+ // Atomics mode has no sandbox iframe and keeps its worker alive.
144
+ state.googleInitialized = false;
145
+ state.clarityId = undefined;
146
+ state.umamiKey = undefined;
147
+ }
148
+ },
149
+ true,
150
+ );
151
+ document.addEventListener("astro:after-swap", () => {
152
+ if (sandbox) {
153
+ document.body.moveBefore(sandbox, null);
154
+ sandbox = undefined;
155
+ }
156
+ });
157
+ document.addEventListener("astro:page-load", () => {
158
+ initializeTrackers();
159
+ trackGooglePage();
160
+ trackUmamiPage();
161
+ trackVercountPage();
162
+ state.previousPage = window.location.href;
163
+ });
164
+ }
165
+
166
+ export {};
@@ -4,4 +4,3 @@ import "./date-time";
4
4
  import "./footer-badges";
5
5
  import "./navigation";
6
6
  import "./runtime-counter";
7
- import "./vercount";
@@ -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
- dateElement.textContent = toDisplayDate(now, lang);
33
- dateElement.dateTime = toMachineDate(now);
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
  });