@xingwangzhe/stalux 1.19.0 → 1.20.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/README.md CHANGED
@@ -54,7 +54,7 @@ export default defineConfig({
54
54
  Create `src/content.config.ts`:
55
55
 
56
56
  ```ts
57
- import { defineCollections } from "@xingwangzhe/stalux/schemas";
57
+ import { defineCollections } from "@xingwangzhe/stalux/schemas/collections";
58
58
  export const collections = defineCollections({ contentDir: "stalux" });
59
59
  ```
60
60
 
@@ -144,6 +144,21 @@ stalux/
144
144
  └── words/ # Quotes / short notes
145
145
  ```
146
146
 
147
+ ### Analytics configuration
148
+
149
+ Analytics are configured in `stalux/config/head.yml`, so no template changes are needed:
150
+
151
+ ```yaml
152
+ id: head
153
+ bingClarityId: "YOUR_CLARITY_PROJECT_ID"
154
+ ```
155
+
156
+ `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.
157
+
158
+ 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.
159
+
160
+ After deployment, verify the script URL contains the exact Project ID and that the browser sends requests to `https://www.clarity.ms/collect`.
161
+
147
162
  ---
148
163
 
149
164
  ## 🛠️ Development Commands
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xingwangzhe/stalux",
3
- "version": "1.19.0",
3
+ "version": "1.20.1",
4
4
  "description": "A powerful, modern Astro blog theme — use as template or install as plugin",
5
5
  "keywords": [
6
6
  "astro",
package/scripts/cli.mjs CHANGED
@@ -60,9 +60,10 @@ items:
60
60
  `,
61
61
  "head.yml": `id: head
62
62
  # googleAnalyticsId: "G-XXXXXXXXXX"
63
- # clarityId: "xxxxxxxxxx"
64
- # umamiWebsiteId: ""
65
- # umamiSrc: ""
63
+ # bingClarityId: "xxxxxxxxxx"
64
+ # umami:
65
+ # id: ""
66
+ # url: ""
66
67
  # anyhead: ""
67
68
  `,
68
69
  "footer.yml": `id: footer
@@ -91,7 +92,7 @@ badges:
91
92
  custom: |
92
93
  <div id="custom-footer-hook"></div>
93
94
  `,
94
- "media-links.yml": `id: medialinks
95
+ "media-links.yml": `id: media-links
95
96
  items:
96
97
  - icon: "github"
97
98
  link: "https://github.com/yourname"
@@ -102,24 +103,18 @@ description: "Friends & Resources"
102
103
  sites:
103
104
  - name: "Astro"
104
105
  description: "The web framework for content-driven websites"
105
- url: "https://astro.build"
106
+ link: "https://astro.build"
106
107
  icon: "https://astro.build/favicon.svg"
107
108
  `,
108
109
  "comment.yml": `id: comment
109
110
  enabled: false
110
- waline:
111
- serverURL: ""
112
- lang: ""
113
- locale: ""
114
- pageSize: 10
115
- recaptchaV3Key: ""
116
111
  `,
117
112
  "promote.yml": `id: promote
118
113
  export_md: false
119
114
  # llm_promote: |
120
115
  `,
121
116
  "ai-discovery.yml": `id: ai-discovery
122
- conformance: "none"
117
+ conformance: "disabled"
123
118
  `,
124
119
  "typetexts.yml": `id: typetexts
125
120
  items:
@@ -1,14 +1,14 @@
1
1
  ---
2
2
  import { getCollection } from "astro:content";
3
- import { getSiteData, getAuthorData, getHeadData, getNavsData, getLinksData, getCommentData, getFooterData, getPromoteData, getTypetextsData, getMediaLinksData } from "@utils/config-utils";
3
+ import { getHeadData } from "@utils/config-utils";
4
4
  const configEntries = await getCollection("config");
5
5
  const headConfig = getHeadData(configEntries);
6
- const id = headConfig?.bingClarityId | undefined;
6
+ const id = headConfig?.bingClarityId;
7
7
  ---
8
8
 
9
9
  <script is:inline data-astro-rerun define:vars={{ id }}>
10
10
  // 防止重复初始化
11
- if (!window.clarity) {
11
+ if (id && !document.getElementById("stalux-clarity-script") && !window.clarity) {
12
12
  (function (c, l, a, r, i, t, y) {
13
13
  c[a] =
14
14
  c[a] ||
@@ -16,6 +16,7 @@ const id = headConfig?.bingClarityId | undefined;
16
16
  (c[a].q = c[a].q || []).push(arguments);
17
17
  };
18
18
  t = l.createElement(r);
19
+ t.id = "stalux-clarity-script";
19
20
  t.async = 1;
20
21
  t.src = "https://www.clarity.ms/tag/" + i;
21
22
  y = l.getElementsByTagName(r)[0];
@@ -1,9 +1,9 @@
1
1
  ---
2
2
  import { getCollection } from "astro:content";
3
- import { getSiteData, getAuthorData, getHeadData, getNavsData, getLinksData, getCommentData, getFooterData, getPromoteData, getTypetextsData, getMediaLinksData } from "@utils/config-utils";
3
+ import { getHeadData } from "@utils/config-utils";
4
4
  const configEntries = await getCollection("config");
5
5
  const headConfig = getHeadData(configEntries);
6
- const gaID = headConfig?.googleAnalyticsId | undefined;
6
+ const gaID = headConfig?.googleAnalyticsId;
7
7
  ---
8
8
 
9
9
  <script
@@ -74,7 +74,8 @@ interface RouteCharSet {
74
74
  }
75
75
 
76
76
  interface FontManifest {
77
- common: string; // filename of common subset
77
+ common: string; // filename of common subset TTF
78
+ commonCss: string; // filename of common subset CSS (content-addressed)
78
79
  routes: Record<string, string>; // route → subset filename
79
80
  /** Per-post mapping: abbrlink → subset filename */
80
81
  posts: Record<string, string>;
@@ -157,22 +158,19 @@ function scanContent(projectRoot: string): ContentFile[] {
157
158
  for (const f of readdirSync(postsDir)) {
158
159
  if (!f.endsWith(".md") && !f.endsWith(".mdx")) continue;
159
160
  const content = readFileSync(join(postsDir, f), "utf-8");
160
- const isTemplate = f.startsWith("_");
161
161
  const fm = parsePostFrontmatter(content);
162
162
 
163
- // Per-post routes: skip template (_) files
164
- if (!isTemplate) {
165
- const abbrlink = fm.abbrlink ?? basename(f, extname(f));
166
- files.push({
167
- route: `posts/${abbrlink}`,
168
- filePath: join(postsDir, f),
169
- abbrlink,
170
- });
171
- }
172
-
163
+ // Per-post routes:与内容集合 glob(*.{md,mdx},含 _ 前缀文件,
164
+ // 见 schemas/collections.ts)保持一致——这些文件都会生成页面,
165
+ // 字体子集必须覆盖,否则页面 subset css 缺失 → 404。
166
+ const abbrlink = fm.abbrlink ?? basename(f, extname(f));
167
+ files.push({
168
+ route: `posts/${abbrlink}`,
169
+ filePath: join(postsDir, f),
170
+ abbrlink,
171
+ });
173
172
  // Virtual routes (archives/tags/categories): include ALL posts including templates
174
173
  // because content collections load them and they render on those pages
175
- const abbrlink = fm.abbrlink ?? basename(f, extname(f));
176
174
  allPostContents.push({
177
175
  abbrlink,
178
176
  tags: fm.tags,
@@ -423,7 +421,7 @@ export async function runFontSubsetting(
423
421
  const outDir = resolve(projectRoot, FONT_OUT_DIR);
424
422
  if (existsSync(outDir)) {
425
423
  for (const f of readdirSync(outDir)) {
426
- if (f.startsWith("common-") || f.startsWith("subset-")) {
424
+ if (f.startsWith("common-") || f.startsWith("subset-") || f === "common.css") {
427
425
  try {
428
426
  unlinkSync(join(outDir, f));
429
427
  } catch {
@@ -530,7 +528,11 @@ export async function runFontSubsetting(
530
528
  }
531
529
  }
532
530
 
533
- // Generate common CSS
531
+ // Generate common CSS (content-addressed filename: 字符集变化 → hash 变化 →
532
+ // 文件名变化,浏览器必然拉新 CSS,避免缓存旧 CSS 引用已删除的旧 TTF 导致 404)
533
+ // dev 模式(skipPerRoute)用固定名 common.css,由 dev 中间件场景复用;
534
+ // 生产构建用 common-<hash>.css,HTML 从 manifest 读取引用。
535
+ const commonCssName = skipPerRoute ? "common.css" : `common-${commonHash}.css`;
534
536
  const commonCSS = `/* Auto-generated common font subset */
535
537
  @font-face {
536
538
  font-family: "LXGW WenKai Subset";
@@ -538,7 +540,7 @@ export async function runFontSubsetting(
538
540
  font-display: swap;
539
541
  }
540
542
  `;
541
- writeFileSync(join(outDir, "common.css"), commonCSS);
543
+ writeFileSync(join(outDir, commonCssName), commonCSS);
542
544
 
543
545
  const groups = deduplicate(routeSets);
544
546
  const routeMap: Record<string, string> = {};
@@ -570,8 +572,11 @@ export async function runFontSubsetting(
570
572
  }
571
573
 
572
574
  // Generate per-route CSS (each route gets its own CSS file)
575
+ // CSS 文件名内容寻址:内容只引用一个 TTF,文件名带上该 TTF 的 hash,
576
+ // 内容变化 → 文件名变化 → 浏览器必然拉新,避免缓存旧 CSS 引用已删除 TTF。
573
577
  // If no unique chars, reference the common TTF as fallback
574
578
  const refTtf = existsSync(outPath) ? filename : commonFilename;
579
+ const refHash = existsSync(outPath) ? hash : commonHash;
575
580
  for (const route of routes) {
576
581
  const cssContent = `/* Auto-generated font subset for route: ${route} */
577
582
  @font-face {
@@ -581,7 +586,7 @@ export async function runFontSubsetting(
581
586
  }
582
587
  `;
583
588
  const cssName = route.replace("/", "-");
584
- const cssFilename = `subset-${cssName}.css`;
589
+ const cssFilename = `subset-${cssName}-${refHash}.css`;
585
590
  writeFileSync(join(outDir, cssFilename), cssContent);
586
591
  routeMap[route] = cssFilename;
587
592
 
@@ -591,17 +596,24 @@ export async function runFontSubsetting(
591
596
  }
592
597
  }
593
598
  }
594
-
595
- // 6. Generate manifest
596
- const manifest: FontManifest = {
597
- common: commonFilename,
598
- routes: routeMap,
599
- posts: postMap,
600
- };
601
- writeFileSync(join(outDir, "manifest.json"), JSON.stringify(manifest, null, 2));
602
-
603
- logger.info(
604
- `Font subsetting done: ${subsetCount} route subsets + common (${(fontBuffer.length / 1024 / 1024).toFixed(1)} MB → variable)`,
605
- );
606
599
  }
600
+
601
+ // 6. Generate manifest(dev 也生成:routes/posts 为空,供 Stalux.astro import)
602
+ const manifest: FontManifest = {
603
+ common: commonFilename,
604
+ commonCss: commonCssName,
605
+ routes: routeMap,
606
+ posts: postMap,
607
+ };
608
+ writeFileSync(join(outDir, "manifest.json"), JSON.stringify(manifest, null, 2));
609
+
610
+ // 副本写入包内 generated/:Stalux.astro import 它,让 manifest 进入 Astro
611
+ // 依赖图——manifest 变化时页面必然重建,增量构建 restored 的页面不会引用旧资源。
612
+ const genDir = resolve(dirname(fileURLToPath(import.meta.url)), "generated");
613
+ mkdirSync(genDir, { recursive: true });
614
+ writeFileSync(join(genDir, "fonts-manifest.json"), JSON.stringify(manifest));
615
+
616
+ logger.info(
617
+ `Font subsetting done: ${subsetCount} route subsets + common (${(fontBuffer.length / 1024 / 1024).toFixed(1)} MB → variable)`,
618
+ );
607
619
  }
@@ -5,6 +5,9 @@ import { getCollection } from "astro:content";
5
5
  import "@styles/base/init.css";
6
6
  import "@styles/shared/stagger.css";
7
7
  import { getAuthorData, getAiDiscoveryData, getPromoteData, getSiteData } from "@utils/config-utils";
8
+ // font-subset.ts 构建期生成的 manifest 副本(进入 Astro 依赖图,
9
+ // manifest 变化 → 页面重建 → 增量构建 restored 的页面不会引用旧字体资源)
10
+ import fontsManifest from "../internal/generated/fonts-manifest.json";
8
11
  const props = Astro.props as Props;
9
12
 
10
13
  interface Props {
@@ -29,10 +32,38 @@ const promoteConfig = getPromoteData(configEntries);
29
32
  const aiConfig = getAiDiscoveryData(configEntries);
30
33
  const htmlLang = siteConfig.lang || "zh-CN";
31
34
 
32
- // Resolve font subset CSS URL
33
- const fontSubsetUrl = props.fontSubsetId
34
- ? `/fonts/subset-${props.fontSubsetId}.css`
35
- : undefined;
35
+ // Resolve font subset CSS URLs(内容寻址,避免浏览器缓存旧 CSS 引用已删除 TTF → 404)
36
+ // 生产构建:font-subset.ts 生成带 hash 的 CSS 文件名并写入 manifest(副本 import 至此),
37
+ // 这里从 manifest 取文件名,内容变化必然换名、浏览器自动拉新。
38
+ // dev 模式:manifest 的 routes 为空(skipPerRoute),回退固定名 /fonts/common.css 与
39
+ // /fonts/subset-<id>.css,由 dev 按需中间件生成。
40
+ const fontManifest = fontsManifest as
41
+ | {
42
+ commonCss?: string;
43
+ routes?: Record<string, string>;
44
+ }
45
+ | null
46
+ | undefined;
47
+
48
+ let commonFontCssUrl = "/fonts/common.css";
49
+ let fontSubsetUrl: string | undefined;
50
+ if (props.fontSubsetId) {
51
+ fontSubsetUrl = `/fonts/subset-${props.fontSubsetId}.css`;
52
+ }
53
+ if (fontManifest?.commonCss) {
54
+ commonFontCssUrl = `/fonts/${fontManifest.commonCss}`;
55
+ }
56
+ if (props.fontSubsetId && fontManifest?.routes) {
57
+ // route 键可能是 posts/xxx(页面对应 posts-xxx)或含原始 unicode(tag/category),
58
+ // 统一归一化比较:route.replace("/", "-") === decodeURIComponent(fontSubsetId)
59
+ const want = decodeURIComponent(props.fontSubsetId);
60
+ for (const [route, cssFile] of Object.entries(fontManifest.routes)) {
61
+ if (route.replace("/", "-") === want) {
62
+ fontSubsetUrl = `/fonts/${encodeURIComponent(cssFile)}`;
63
+ break;
64
+ }
65
+ }
66
+ }
36
67
 
37
68
  // 背景 SVG 以静态 URL 硬编码在 background.ts(纯客户端),
38
69
  // 集成在 config:setup 时把 SVG 复制到用户项目 public/background/。
@@ -62,7 +93,7 @@ const llmPromote = llmPromoteRaw
62
93
  noindex={props.noindex}
63
94
  />
64
95
  <!-- Per-route font subset (generated at build time) -->
65
- <link rel="stylesheet" href="/fonts/common.css" />
96
+ <link rel="stylesheet" href={commonFontCssUrl} />
66
97
  {fontSubsetUrl && <link rel="stylesheet" href={fontSubsetUrl} />}
67
98
  <script>
68
99
  import "../scripts/background.ts";