@xingwangzhe/stalux 1.25.15 → 1.26.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.
Files changed (44) hide show
  1. package/README.md +29 -0
  2. package/package.json +13 -11
  3. package/src/components/stalux/categories/categoriesList.astro +1 -1
  4. package/src/components/stalux/footer/FooterBadges.astro +1 -1
  5. package/src/components/stalux/layout/footer.astro +1 -1
  6. package/src/components/stalux/layout/navs.astro +3 -3
  7. package/src/components/stalux/tags/tagsCloud.astro +1 -1
  8. package/src/index.ts +54 -23
  9. package/src/internal/components-plugin.ts +7 -1
  10. package/src/internal/font-slices.ts +14 -2
  11. package/src/pages/api/posts.json.ts +2 -2
  12. package/src/pages/archives.md.ts +1 -1
  13. package/src/pages/atom.xml.ts +2 -2
  14. package/src/pages/categories/index.md.ts +1 -1
  15. package/src/pages/index.md.ts +1 -1
  16. package/src/pages/llms-full.txt.ts +1 -1
  17. package/src/pages/llms.txt.ts +1 -1
  18. package/src/pages/posts/[post].astro +1 -1
  19. package/src/pages/rss.xml.ts +2 -2
  20. package/src/pages/tags/index.md.ts +1 -1
  21. package/src/plugins/feature-flags.ts +11 -2
  22. package/src/plugins/satteri-temml.ts +10 -3
  23. package/src/schemas/collections.ts +48 -24
  24. package/src/scripts/background.ts +8 -2
  25. package/src/scripts/clarity.ts +11 -2
  26. package/src/scripts/google-analytics.ts +13 -2
  27. package/src/scripts/logger.ts +36 -0
  28. package/src/scripts/navigation.ts +12 -1
  29. package/src/scripts/page-runtime.ts +28 -4
  30. package/src/scripts/photoswipe.ts +12 -1
  31. package/src/scripts/random-posts.ts +7 -1
  32. package/src/scripts/tags-cloud.ts +13 -2
  33. package/src/scripts/vercount.ts +11 -2
  34. package/src/scripts/view-transitions.ts +4 -0
  35. package/src/scripts/waline.ts +8 -2
  36. package/src/scripts/webmcp.ts +25 -14
  37. package/src/utils/ai-discovery.ts +32 -13
  38. package/src/utils/badge-generator.ts +4 -2
  39. package/src/utils/collections-stats.ts +5 -4
  40. package/src/utils/content-index.ts +22 -6
  41. package/src/utils/diagnostics.ts +45 -0
  42. package/src/utils/feed.ts +5 -1
  43. package/src/utils/lucide-icons.ts +7 -3
  44. package/src/utils/word-count-utils.ts +22 -9
package/README.md CHANGED
@@ -226,3 +226,32 @@ jobTitle: Software Engineer # Optional; emitted as JSON-LD Person.jobTitle
226
226
  ```
227
227
 
228
228
  This is a build-time metadata field. It does not create a contact endpoint or expose private information; only publish a role that is already intended to be public.
229
+
230
+
231
+ ## Development diagnostics
232
+
233
+ Stalux routes integration, content-loader and rendering diagnostics through Astro's official logger. Normal mode keeps summaries, warnings and errors. Verbose mode adds configuration/routes, component overrides, font cache, Markdown analysis/cache, content statistics, feeds, asset sync and Pagefind stages.
234
+
235
+ ```bash title="Stalux diagnostics"
236
+ bun run dev:debug
237
+ bun run build:debug
238
+ # In a consuming project:
239
+ STALUX_DEBUG=1 bun run dev --verbose
240
+ STALUX_DEBUG=1 bun run build --verbose
241
+ # Silence Astro; standalone verification output remains separate:
242
+ bun run astro -- build --silent
243
+ ```
244
+
245
+ | Surface | Behavior |
246
+ | --- | --- |
247
+ | Terminal | Integration loggers use `fork("stalux/module")`; rendering uses `Astro.logger`, with explicit context propagation to utilities and loaders. Astro levels, custom destinations and `--silent` remain authoritative. |
248
+ | Browser console | A separate lightweight logger uses `[stalux/module]` prefixes. Detail requires development mode plus `STALUX_DEBUG=1` or `--verbose`; enable Verbose messages in DevTools. Production only emits warnings and errors. |
249
+ | Content diagnostics | `getStaticPaths()` has no runtime logger; pure computation errors are reported by callers or Astro. Math failures use Sätteri diagnostics without duplicate error reporting. |
250
+
251
+ Browser diagnostics cover mount/disposal, navigation, search, PhotoSwipe, Waline, WebMCP, backgrounds and external analytics script loading. Logs stay local: no reporting endpoint or telemetry is added. Search terms, article bodies, complete configuration and credentials are not intentionally logged. Errors retain module, cause and stack; common credentials and URL query parameters are redacted, but review third-party error text before sharing.
252
+
253
+ In Astro 7.3, integration `logger.debug()` uses the official `DEBUG/--verbose` channel, separate from custom JSON destinations. `info/warn/error` and rendering diagnostics use the configured destination. Do not enable `--verbose` or `DEBUG` when requesting a silent run.
254
+
255
+ Timing is emitted only as logs, never included in generated pages or cache keys. Switching between normal and verbose builds may require one rebuild; repeated builds in the same mode remain deterministic. Consumers running `astro dev --background` can read terminal output with `astro dev logs`.
256
+
257
+ Keep the TypeScript 6 alias for `astro check`, upgrade Vitest and its coverage provider together, and run `bun run validate` before releasing.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xingwangzhe/stalux",
3
- "version": "1.25.15",
3
+ "version": "1.26.1",
4
4
  "description": "A powerful, modern Astro blog theme — use as template or install as plugin",
5
5
  "keywords": [
6
6
  "astro",
@@ -82,14 +82,16 @@
82
82
  "knip": "knip",
83
83
  "verify:build": "bun scripts/verify-build.mjs",
84
84
  "validate": "bun run check && bun run astro:check && bun run knip && bun run test:coverage && bun run build",
85
- "prepare": "git config core.hooksPath scripts/git-hooks"
85
+ "prepare": "git config core.hooksPath scripts/git-hooks",
86
+ "dev:debug": "STALUX_DEBUG=1 astro dev --verbose",
87
+ "build:debug": "STALUX_DEBUG=1 astro build --verbose && bun run verify:build"
86
88
  },
87
89
  "dependencies": {
88
- "@astrojs/markdown-satteri": "0.4.0",
90
+ "@astrojs/markdown-satteri": "0.4.1",
89
91
  "@astrojs/rss": "^4.0.19",
90
92
  "@astrojs/sitemap": "^3.7.4",
91
93
  "@expressive-code/plugin-line-numbers": "^0.44.2",
92
- "@lucide/astro": "^1.38.0",
94
+ "@lucide/astro": "^1.42.0",
93
95
  "@mcp-b/webmcp-polyfill": "^5.1.0",
94
96
  "@pagefind/component-ui": "^1.5.2",
95
97
  "@waline/client": "^3.15.2",
@@ -103,21 +105,21 @@
103
105
  "pagefind": "^1.5.2",
104
106
  "photoswipe": "^5.4.4",
105
107
  "satteri": "^0.10.5",
106
- "simple-icons": "^16.29.0",
108
+ "simple-icons": "^16.30.0",
107
109
  "subset-font": "^2.7.0",
108
110
  "temml": "^0.13.5"
109
111
  },
110
112
  "devDependencies": {
111
113
  "@astrojs/check": "0.9.10",
112
- "@biomejs/biome": "2.5.11",
113
- "@vitest/coverage-v8": "4.1.11",
114
- "astro": "^7.2.10",
115
- "knip": "^6.34.0",
114
+ "@biomejs/biome": "2.5.12",
115
+ "@vitest/coverage-v8": "5.0.0",
116
+ "astro": "^7.3.2",
117
+ "knip": "^6.35.1",
116
118
  "typescript": "npm:@typescript/typescript6@6.0.2",
117
119
  "vite": "8.2.2",
118
- "vitest": "4.1.11"
120
+ "vitest": "5.0.0"
119
121
  },
120
122
  "peerDependencies": {
121
- "astro": "7.2.10"
123
+ "astro": "7.3.2"
122
124
  }
123
125
  }
@@ -3,7 +3,7 @@ import CategoriesCard from "@components/stalux/categories/categoriesCard.astro";
3
3
  import styles from "@styles/components/categories/categoriesList.module.css";
4
4
  import { getCategoryCountList } from "@utils/collections-stats";
5
5
 
6
- const categoryList = await getCategoryCountList();
6
+ const categoryList = await getCategoryCountList(Astro.logger);
7
7
  ---
8
8
 
9
9
  <ul class={`${styles.categoriesList} stagger-children`}>
@@ -15,7 +15,7 @@ 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));
18
+ const src = svgToDataUrl(generateBadge(opts, Astro.logger));
19
19
  const alt = opts.alt || `${opts.label}: ${opts.message}`;
20
20
  if (opts.href) {
21
21
  return `<a href="${opts.href}" target="_blank" rel="${opts.rel ?? "noopener"}"><img src="${src}" alt="${alt}" class="badge" /></a>`;
@@ -46,7 +46,7 @@ const badges: BadgeItem[] | undefined = footerConfig?.badges?.map((item) => {
46
46
  };
47
47
  });
48
48
 
49
- const totalWordCount = await getTotalWordCount();
49
+ const totalWordCount = await getTotalWordCount(Astro.logger);
50
50
  const formattedWordCount = formatWordCount(totalWordCount, siteConfig.lang || "zh-CN");
51
51
  ---
52
52
 
@@ -6,8 +6,8 @@ import { getNavsData, getSiteData } from "@utils/config-utils";
6
6
  import { createTranslator } from "@utils/i18n";
7
7
  import { getLucideIcon } from "@utils/lucide-icons";
8
8
 
9
- const MenuIcon = getLucideIcon("menu");
10
- const SearchIcon = getLucideIcon("search");
9
+ const MenuIcon = getLucideIcon("menu", Astro.logger);
10
+ const SearchIcon = getLucideIcon("search", Astro.logger);
11
11
  const configEntries = await getCollection("config");
12
12
  const siteConfig = getSiteData(configEntries);
13
13
  const navsConfig = getNavsData(configEntries);
@@ -45,7 +45,7 @@ const navs = navsConfig?.items ?? [];
45
45
 
46
46
  <ul data-ref="stalux-nav-list" class={styles.navList}>
47
47
  {navs.map((item) => {
48
- const NavIcon = item.icon ? getLucideIcon(item.icon) : null;
48
+ const NavIcon = item.icon ? getLucideIcon(item.icon, Astro.logger) : null;
49
49
  return (
50
50
  <li class={styles.navItem}>
51
51
  <a
@@ -8,7 +8,7 @@ import { createTranslator } from "@utils/i18n";
8
8
  const configEntries = await getCollection("config");
9
9
  const siteConfig = getSiteData(configEntries);
10
10
  const { t } = createTranslator(siteConfig.lang || "zh-CN");
11
- const tagList = await getTagCountList();
11
+ const tagList = await getTagCountList(Astro.logger);
12
12
  const tagLinks = tagList.map((t) => ({
13
13
  type: "link" as const,
14
14
  text: t.name,
package/src/index.ts CHANGED
@@ -20,7 +20,6 @@ import { fontProviders } from "astro/config";
20
20
  // pagefind 是 ESM-only 包,需要在模块顶层导入
21
21
  // 因为 astro:build:done 钩子中 Vite module runner 已关闭,无法动态 import
22
22
  import { createIndex as pagefindCreateIndex } from "pagefind";
23
-
24
23
  import type { StaluxOptions } from "./config";
25
24
  import { expressiveCode } from "./expressive-code";
26
25
  import { staluxComponentsAlias } from "./internal/components-plugin";
@@ -35,6 +34,7 @@ import {
35
34
  import { createViteAliases } from "./internal/vite-aliases";
36
35
  import { featureFlagsHast, featureFlagsMdast } from "./plugins/feature-flags";
37
36
  import { temml } from "./plugins/satteri-temml";
37
+ import { describeError } from "./utils/diagnostics";
38
38
 
39
39
  // 字体配置(Astro Fonts API)通过 updateConfig 注入,两种模式(源码模板/npm 插件)都生效。
40
40
  // 官方 local provider 完全本地读文件(readFile),不联网;
@@ -162,6 +162,13 @@ export function stalux(options: StaluxOptions = {}): AstroIntegration[] {
162
162
  config,
163
163
  logger,
164
164
  }) => {
165
+ const started = performance.now();
166
+ const configLogger = logger.fork("stalux/config");
167
+ const assetsLogger = logger.fork("stalux/assets");
168
+ const routesLogger = logger.fork("stalux/routes");
169
+ const markdownLogger = logger.fork("stalux/markdown");
170
+ const fontsLogger = logger.fork("stalux/fonts");
171
+ configLogger.debug(`setup started; mode=${isPluginMode ? "plugin" : "source"}`);
165
172
  const srcDir = fileURLToPath(new URL(".", import.meta.url));
166
173
  const runtimeCacheKey = createRuntimeCacheKey(
167
174
  path.join(srcDir, "scripts"),
@@ -174,8 +181,14 @@ export function stalux(options: StaluxOptions = {}): AstroIntegration[] {
174
181
  resolve: {
175
182
  alias: createViteAliases(srcDir),
176
183
  },
177
- plugins: [staluxComponentsAlias(opt.components)],
184
+ plugins: [
185
+ staluxComponentsAlias(opt.components, logger.fork("stalux/components")),
186
+ ],
178
187
  define: {
188
+ __STALUX_DEBUG__: JSON.stringify(
189
+ process.env.STALUX_DEBUG === "1" ||
190
+ process.argv.includes("--verbose"),
191
+ ),
179
192
  __VUE_OPTIONS_API__: true,
180
193
  __VUE_PROD_DEVTOOLS__: false,
181
194
  __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: false,
@@ -196,12 +209,12 @@ export function stalux(options: StaluxOptions = {}): AstroIntegration[] {
196
209
  try {
197
210
  const { copied, skipped } = syncBackgroundSvgs(srcDir);
198
211
  if (copied > 0) {
199
- logger.info(`Stalux: 同步背景 SVG ${copied} 个(跳过 ${skipped} 个不变)`);
212
+ assetsLogger.info(`同步背景 SVG ${copied} 个(跳过 ${skipped} 个不变)`);
200
213
  } else if (skipped > 0) {
201
- logger.debug(`Stalux: 背景 SVG 无变化(跳过 ${skipped} 个)`);
214
+ assetsLogger.debug(`背景 SVG 无变化(跳过 ${skipped} 个)`);
202
215
  }
203
216
  } catch (error) {
204
- logger.warn(`Stalux: 同步背景 SVG 失败: ${String(error)}`);
217
+ assetsLogger.warn(`同步背景 SVG 失败: ${describeError(error)}`);
205
218
  }
206
219
 
207
220
  // 3. 注入所有页面路由(仅插件模式,源码模式下使用文件路由)
@@ -210,9 +223,10 @@ export function stalux(options: StaluxOptions = {}): AstroIntegration[] {
210
223
  for (const route of routes) {
211
224
  injectRoute(route);
212
225
  }
213
- logger.debug(`Stalux: injected ${routes.length} routes`);
226
+ routesLogger.debug(`injected ${routes.length} routes`);
227
+ for (const route of routes) routesLogger.debug(`route=${route.pattern}`);
214
228
  } else {
215
- logger.info("Stalux: running in source mode, file-based routing active");
229
+ routesLogger.debug("source mode; file-based routing active");
216
230
  }
217
231
 
218
232
  // 4. 可选:添加 Dev Toolbar 应用
@@ -227,7 +241,9 @@ export function stalux(options: StaluxOptions = {}): AstroIntegration[] {
227
241
  });
228
242
  }
229
243
 
230
- logger.info(`Stalux initialized (contentDir: ${opt.contentDir})`);
244
+ configLogger.debug(
245
+ `contentDir=${opt.contentDir}; toolbar=${opt.devToolbar}; pagefind=${opt.pagefind}`,
246
+ );
231
247
 
232
248
  // 5. 注入 satteri 插件(Mermaid/字数统计/特性标记/数学公式/PhotoSwipe),按插件 name 去重
233
249
  // 两种模式都由集成补齐默认插件。Astro 7 的 markdown.processor 默认为 satteri(),
@@ -241,7 +257,11 @@ export function stalux(options: StaluxOptions = {}): AstroIntegration[] {
241
257
  if (processorOptions) {
242
258
  const seen = collectPluginNames(processorOptions);
243
259
  appendUniquePlugin(processorOptions.mdastPlugins, mermaidMdast(), seen);
244
- appendUniquePlugin(processorOptions.mdastPlugins, temml(), seen);
260
+ appendUniquePlugin(
261
+ processorOptions.mdastPlugins,
262
+ temml(undefined, markdownLogger),
263
+ seen,
264
+ );
245
265
  appendUniquePlugin(processorOptions.mdastPlugins, featureFlagsMdast, seen);
246
266
  appendUniquePlugin(
247
267
  processorOptions.hastPlugins,
@@ -254,17 +274,17 @@ export function stalux(options: StaluxOptions = {}): AstroIntegration[] {
254
274
  );
255
275
  appendUniquePlugin(processorOptions.hastPlugins, photoswipe(), seen);
256
276
  appendUniquePlugin(processorOptions.hastPlugins, featureFlagsHast, seen);
257
- logger.debug(
258
- "Stalux: injected satteri plugins (mermaid/temml/photoswipe/feature-flags)",
277
+ markdownLogger.debug(
278
+ "injected satteri plugins (mermaid/temml/photoswipe/feature-flags)",
259
279
  );
260
280
  } else {
261
- logger.warn(
262
- "Stalux: markdown.processor 不是 satteri,无法注入字数统计/数学公式/PhotoSwipe 插件。" +
281
+ markdownLogger.warn(
282
+ "markdown.processor 不是 satteri,无法注入字数统计/数学公式/PhotoSwipe 插件。" +
263
283
  "请在 astro.config 中配置 `processor: satteri({...})`。",
264
284
  );
265
285
  }
266
286
  } catch (error) {
267
- logger.warn(`Stalux: 注入 markdown 插件失败: ${String(error)}`);
287
+ markdownLogger.warn(`注入 markdown 插件失败: ${describeError(error)}`);
268
288
  }
269
289
 
270
290
  // 6. 站点 URL 单源化:以 stalux/config/site.yml 的 url 为准
@@ -302,14 +322,18 @@ export function stalux(options: StaluxOptions = {}): AstroIntegration[] {
302
322
  );
303
323
  }
304
324
  }
305
- } catch {
306
- logger.debug("Stalux: site.yml not found, skip site sync");
325
+ } catch (error) {
326
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") {
327
+ configLogger.debug("site.yml not found; site sync skipped");
328
+ } else {
329
+ configLogger.warn(`site sync failed: ${describeError(error)}`);
330
+ }
307
331
  }
308
332
 
309
333
  // 7. 字体:构建期把正文切成 unicode-range 分片,通过官方 Fonts API 注入
310
334
  // (config:setup 阶段执行,保证 dev/build 都能生成;local provider 纯本地读文件)
311
335
  try {
312
- const sliced = await runFontSlicing(process.cwd(), logger);
336
+ const sliced = await runFontSlicing(process.cwd(), fontsLogger);
313
337
  if (sliced) {
314
338
  updateConfig({
315
339
  fonts: buildFontsConfig(
@@ -318,16 +342,19 @@ export function stalux(options: StaluxOptions = {}): AstroIntegration[] {
318
342
  sliced.codeItalic,
319
343
  ),
320
344
  });
321
- logger.info(
322
- `Stalux: injected ${sliced.body.length} body font chunks + code font via Fonts API`,
345
+ fontsLogger.debug(
346
+ `injected ${sliced.body.length} body font chunks + code font via Fonts API`,
323
347
  );
324
348
  }
325
349
  } catch (error) {
326
- logger.warn(`Stalux: font injection failed: ${String(error)}`);
350
+ fontsLogger.warn(`font injection failed: ${describeError(error)}`);
327
351
  }
352
+ configLogger.info(`initialized in ${(performance.now() - started).toFixed(1)}ms`);
328
353
  },
329
354
 
330
- "astro:build:done": async ({ dir, logger }) => {
355
+ "astro:build:done": async ({ dir, logger: rootLogger }) => {
356
+ const logger = rootLogger.fork("stalux/pagefind");
357
+ const started = performance.now();
331
358
  const outDir = fileURLToPath(dir);
332
359
 
333
360
  // 后处理:Pagefind 搜索索引。启用时任何失败都必须让构建失败,
@@ -353,11 +380,15 @@ export function stalux(options: StaluxOptions = {}): AstroIntegration[] {
353
380
  outputPath: path.join(outDir, "pagefind"),
354
381
  });
355
382
 
356
- logger.info(`Pagefind indexed ${page_count} pages → ${outputPath}`);
383
+ logger.info(
384
+ `indexed ${page_count} pages → ${outputPath} (${(performance.now() - started).toFixed(1)}ms)`,
385
+ );
357
386
  } catch (error) {
358
- logger.error(`Pagefind indexing failed: ${String(error)}`);
387
+ logger.error(`Pagefind indexing failed: ${describeError(error)}`);
359
388
  throw error;
360
389
  }
390
+ } else {
391
+ logger.debug("disabled; indexing skipped");
361
392
  }
362
393
  },
363
394
  },
@@ -18,6 +18,7 @@
18
18
 
19
19
  import { fileURLToPath } from "node:url";
20
20
 
21
+ import type { AstroIntegrationLogger } from "astro";
21
22
  import type { Plugin } from "vite";
22
23
 
23
24
  import type { ComponentOverrideMap } from "../config";
@@ -28,7 +29,10 @@ const VIRTUAL_PREFIX = "@stalux/component/";
28
29
  /**
29
30
  * 创建 Vite 别名插件,处理 `@stalux/component/*` → 实际组件路径
30
31
  */
31
- export function staluxComponentsAlias(overrides: ComponentOverrideMap = {}): Plugin {
32
+ export function staluxComponentsAlias(
33
+ overrides: ComponentOverrideMap = {},
34
+ logger?: AstroIntegrationLogger,
35
+ ): Plugin {
32
36
  // 包内 components 目录的绝对路径
33
37
  const componentsDir = fileURLToPath(new URL("../components", import.meta.url));
34
38
 
@@ -42,6 +46,8 @@ export function staluxComponentsAlias(overrides: ComponentOverrideMap = {}): Plu
42
46
  // 使用 resolveComponentPath 获取实际路径
43
47
  const resolvedPath = resolveComponentPath(componentName, overrides, componentsDir);
44
48
 
49
+ logger?.debug(`${componentName}: ${overrides[componentName] ? "override" : "default"}`);
50
+
45
51
  // 如果是用户提供的相对路径,相对于项目根解析
46
52
  if (overrides[componentName]) {
47
53
  return this.resolve(resolvedPath, importer, { skipSelf: true });
@@ -1,3 +1,4 @@
1
+ import { describeError } from "../utils/diagnostics";
1
2
  /**
2
3
  * Build-time CJK font slicing engine (Astro Fonts API variant).
3
4
  *
@@ -148,6 +149,10 @@ export async function runFontSlicing(
148
149
  projectRoot: string,
149
150
  logger: AstroIntegrationLogger,
150
151
  ): Promise<SlicedFonts | null> {
152
+ const started = performance.now();
153
+ let cached = 0;
154
+ let generated = 0;
155
+ logger.debug("locating font inputs");
151
156
  // 1. Locate fonts (check project root first, then stalux package dir)
152
157
  const fontPath = findFont(projectRoot, FONT_INPUT);
153
158
  if (!fontPath) {
@@ -177,17 +182,21 @@ export async function runFontSlicing(
177
182
  const filename = `lxgw-wenkai-slice-${def.id}-${hash}.woff2`;
178
183
  const outPath = join(outDir, filename);
179
184
 
180
- if (!existsSync(outPath)) {
185
+ if (existsSync(outPath)) {
186
+ cached++;
187
+ logger.debug(`body slice ${def.id}: cache hit`);
188
+ } else {
181
189
  try {
182
190
  const data = await subsetFont(fontBuffer, chars, { targetFormat: "woff2" });
183
191
  if (data.length > 0) {
184
192
  writeFileSync(outPath, data);
193
+ generated++;
185
194
  logger.debug(
186
195
  ` body slice ${def.id}: ${(data.length / 1024).toFixed(1)} KB → ${filename}`,
187
196
  );
188
197
  }
189
198
  } catch (error) {
190
- logger.warn(` body slice ${def.id} failed: ${String(error)}`);
199
+ logger.warn(` body slice ${def.id} failed: ${describeError(error)}`);
191
200
  }
192
201
  }
193
202
 
@@ -201,6 +210,9 @@ export async function runFontSlicing(
201
210
  return null;
202
211
  }
203
212
 
213
+ logger.debug(
214
+ `generated=${generated}; cached=${cached}; elapsed=${(performance.now() - started).toFixed(1)}ms`,
215
+ );
204
216
  logger.info(
205
217
  `Font slicing done: ${body.length} body chunks + code font ` +
206
218
  `(source ${(fontBuffer.length / 1024 / 1024).toFixed(1)} MB → woff2 chunks)`,
@@ -10,7 +10,7 @@ export const prerender = true;
10
10
  * 相比 /api/post.abbrlink.json,包含完整元信息(日期/分类/标签/摘要/字数),
11
11
  * 是 stalux_get_post / stalux_random_post 等 WebMCP 工具的廉价数据源。
12
12
  */
13
- export const GET: APIRoute = async () => {
13
+ export const GET: APIRoute = async ({ logger }) => {
14
14
  const posts = await getCollection("posts", ({ data }) => !data.draft);
15
15
 
16
16
  const payload = await Promise.all(
@@ -22,7 +22,7 @@ export const GET: APIRoute = async () => {
22
22
  tags: post.data.tags ?? [],
23
23
  categories: post.data.categories ?? [],
24
24
  desc: post.data.desc ?? "",
25
- wordCount: (await analyzeFeatureFlags(post.body)).wordCount,
25
+ wordCount: (await analyzeFeatureFlags(post.body, logger)).wordCount,
26
26
  url: `/posts/${post.data.abbrlink}/`,
27
27
  })),
28
28
  );
@@ -17,7 +17,7 @@ export const GET: APIRoute = async (context) => {
17
17
  const site = getSite(config, context.site?.toString());
18
18
  const lang = (config.get("site")?.lang as string) || "zh-CN";
19
19
  const { t } = createTranslator(lang);
20
- const text = await renderArchivesMd(site, exportMd, t);
20
+ const text = await renderArchivesMd(site, exportMd, t, context.logger);
21
21
  return new Response(text, {
22
22
  headers: {
23
23
  "Content-Type": "text/markdown; charset=utf-8",
@@ -5,12 +5,12 @@ import { buildFeedItems } from "@utils/feed";
5
5
  import { createTranslator, langToFeedLanguage } from "@utils/i18n";
6
6
  import type { APIRoute } from "astro";
7
7
 
8
- export const GET: APIRoute = async () => {
8
+ export const GET: APIRoute = async ({ logger }) => {
9
9
  const configCollection = await getCollection("config");
10
10
  const stalux = getSiteData(configCollection);
11
11
 
12
12
  const posts = await getCollection("posts", ({ data }) => !data.draft);
13
- const items = await buildFeedItems(stalux, posts, "updated");
13
+ const items = await buildFeedItems(stalux, posts, "updated", logger);
14
14
 
15
15
  const lang = stalux?.lang || "zh-CN";
16
16
  const { t } = createTranslator(lang);
@@ -18,7 +18,7 @@ export const GET: APIRoute = async (context) => {
18
18
  const site = getSite(config, context.site?.toString());
19
19
  const lang = (config.get("site")?.lang as string) || "zh-CN";
20
20
  const { t } = createTranslator(lang);
21
- const posts = await getPublishedPosts();
21
+ const posts = await getPublishedPosts(context.logger);
22
22
  const map = buildTaxonomyMap(posts, "categories");
23
23
  const text = renderTaxonomyListMd(map, site, "categories", t("ai.allCategories"));
24
24
  return new Response(text, {
@@ -11,7 +11,7 @@ export const GET: APIRoute = async (context) => {
11
11
  const site = getSite(config, context.site?.toString());
12
12
  const lang = (config.get("site")?.lang as string) || "zh-CN";
13
13
  const { t } = createTranslator(lang);
14
- const text = await renderIndexMd(config, site, t);
14
+ const text = await renderIndexMd(config, site, t, context.logger);
15
15
  return new Response(text, {
16
16
  headers: {
17
17
  "Content-Type": "text/markdown; charset=utf-8",
@@ -15,7 +15,7 @@ export const GET: APIRoute = async (context) => {
15
15
  }
16
16
  const config = await loadConfig();
17
17
  const site = getSite(config, context.site?.toString());
18
- const text = await renderLlmsFullTxt(config, site);
18
+ const text = await renderLlmsFullTxt(config, site, context.logger);
19
19
 
20
20
  return new Response(text, {
21
21
  headers: {
@@ -15,7 +15,7 @@ export const GET: APIRoute = async (context) => {
15
15
  }
16
16
  const config = await loadConfig();
17
17
  const site = getSite(config, context.site?.toString());
18
- const text = await renderLlmsTxt(config, site);
18
+ const text = await renderLlmsTxt(config, site, context.logger);
19
19
 
20
20
  return new Response(text, {
21
21
  headers: {
@@ -20,7 +20,7 @@ const props = Astro.props as Props;
20
20
  const { Content, headings } = await render(props.post);
21
21
 
22
22
  // 文章统计完全由 Sätteri AST 分析,不读取 render metadata。
23
- const flags = await analyzeFeatureFlags(props.post.body);
23
+ const flags = await analyzeFeatureFlags(props.post.body, Astro.logger);
24
24
  const autoDesc = props.post.data.desc || "";
25
25
 
26
26
  export async function getStaticPaths() {
@@ -5,12 +5,12 @@ import { buildFeedItems } from "@utils/feed";
5
5
  import { createTranslator, langToFeedLanguage } from "@utils/i18n";
6
6
  import type { APIRoute } from "astro";
7
7
 
8
- export const GET: APIRoute = async () => {
8
+ export const GET: APIRoute = async ({ logger }) => {
9
9
  const configCollection = await getCollection("config");
10
10
  const stalux = getSiteData(configCollection);
11
11
 
12
12
  const posts = await getCollection("posts", ({ data }) => !data.draft);
13
- const items = await buildFeedItems(stalux, posts, "atom:updated");
13
+ const items = await buildFeedItems(stalux, posts, "atom:updated", logger);
14
14
 
15
15
  const lang = stalux?.lang || "zh-CN";
16
16
  const { t } = createTranslator(lang);
@@ -18,7 +18,7 @@ export const GET: APIRoute = async (context) => {
18
18
  const site = getSite(config, context.site?.toString());
19
19
  const lang = (config.get("site")?.lang as string) || "zh-CN";
20
20
  const { t } = createTranslator(lang);
21
- const posts = await getPublishedPosts();
21
+ const posts = await getPublishedPosts(context.logger);
22
22
  const map = buildTaxonomyMap(posts, "tags");
23
23
  const text = renderTaxonomyListMd(map, site, "tags", t("ai.allTags"));
24
24
  return new Response(text, {
@@ -1,4 +1,5 @@
1
1
  import { createSatteriMarkdownProcessor } from "@astrojs/markdown-satteri";
2
+ import type { AstroRuntimeLogger } from "astro";
2
3
  import type { HastVisitorContext, MdastNode, MdastVisitorContext } from "satteri";
3
4
  /**
4
5
  * Sätteri 插件:在构建时完成字数统计和特性标记,
@@ -15,6 +16,7 @@ import type { HastVisitorContext, MdastNode, MdastVisitorContext } from "satteri
15
16
  * - 数学公式由 satteri-temml 插件直接输出 MathML,无需额外标记
16
17
  */
17
18
  import { defineHastPlugin, defineMdastPlugin } from "satteri";
19
+ import { logDetail } from "../utils/diagnostics";
18
20
 
19
21
  declare module "satteri" {
20
22
  interface DataMap {
@@ -254,10 +256,17 @@ function getFeatureFlagsProcessor() {
254
256
  * 直接用 Sätteri AST 分析一篇文章,供文章页、全局统计和 API 复用。
255
257
  * 这里不复用内容集合的 data,也不读取 Astro 的渲染 metadata。
256
258
  */
257
- export function analyzeFeatureFlags(body: string | undefined): Promise<FeatureFlagsResult> {
259
+ export function analyzeFeatureFlags(
260
+ body: string | undefined,
261
+ logger?: AstroRuntimeLogger,
262
+ ): Promise<FeatureFlagsResult> {
258
263
  const content = body ?? "";
259
264
  const cached = analysisCache.get(content);
260
- if (cached) return cached;
265
+ if (cached) {
266
+ logDetail(logger, "markdown-analysis", "cache hit");
267
+ return cached;
268
+ }
269
+ logDetail(logger, "markdown-analysis", "cache miss; analyzing feature flags");
261
270
 
262
271
  const resultPromise = analyzeFeatureFlagsUncached(content);
263
272
  analysisCache.set(content, resultPromise);
@@ -7,6 +7,8 @@
7
7
  *
8
8
  * 输出纯 MathML,浏览器原生渲染,无需额外的 CSS/字体。
9
9
  */
10
+ import type { AstroIntegrationLogger } from "astro";
11
+
10
12
  import { defineMdastPlugin, type MdastNode, type MdastVisitorContext } from "satteri";
11
13
  import temmlLib, { type Options as TemmlOptions } from "temml";
12
14
 
@@ -34,6 +36,7 @@ function renderMath(
34
36
  displayMode: boolean,
35
37
  options: TemmlOptions,
36
38
  ctx: MdastVisitorContext,
39
+ logger?: AstroIntegrationLogger,
37
40
  ): string {
38
41
  const value = node.value;
39
42
  try {
@@ -43,6 +46,9 @@ function renderMath(
43
46
  throwOnError: true,
44
47
  });
45
48
  } catch (error) {
49
+ logger?.debug(
50
+ "Temml render failed; reporting diagnostic through Markdown processor and trying tolerant rendering",
51
+ );
46
52
  const cause = error instanceof Error ? error : new Error(String(error));
47
53
  ctx.report({
48
54
  message: `Could not render math with Temml: ${cause.message}`,
@@ -57,6 +63,7 @@ function renderMath(
57
63
  throwOnError: false,
58
64
  });
59
65
  } catch {
66
+ logger?.debug("Temml tolerant rendering failed; using error markup");
60
67
  return renderTemmlError(value, error, options);
61
68
  }
62
69
  }
@@ -66,17 +73,17 @@ function renderMath(
66
73
  * 创建一个 Sätteri temml mdast 插件。
67
74
  * @param options - 透传给 temml.renderToString 的选项(displayMode 除外)
68
75
  */
69
- export function temml(options?: TemmlOptions) {
76
+ export function temml(options?: TemmlOptions, logger?: AstroIntegrationLogger) {
70
77
  const settings = options ?? emptyOptions;
71
78
  return defineMdastPlugin({
72
79
  name: "temml",
73
80
  math(node, ctx) {
74
- return { rawHtml: renderMath(node, true, settings, ctx) };
81
+ return { rawHtml: renderMath(node, true, settings, ctx, logger) };
75
82
  },
76
83
  inlineMath(node, ctx) {
77
84
  return {
78
85
  type: "html",
79
- value: renderMath(node, false, settings, ctx),
86
+ value: renderMath(node, false, settings, ctx, logger),
80
87
  };
81
88
  },
82
89
  });