@xingwangzhe/stalux 1.41.2 → 1.41.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xingwangzhe/stalux",
3
- "version": "1.41.2",
3
+ "version": "1.41.4",
4
4
  "description": "A powerful, modern Astro blog theme — use as template or install as plugin",
5
5
  "keywords": [
6
6
  "astro",
@@ -5,6 +5,7 @@ import { type CollectionEntry, getCollection } from "astro:content";
5
5
  import { getSiteData } from "@utils/config-utils";
6
6
  import { formatInTimeZone, parseDate, toTimestamp, zonedCalendarParts } from "@utils/dayjs";
7
7
  import { createTranslator } from "@utils/i18n";
8
+ import { toMachineDateTime } from "@utils/semantic-time";
8
9
 
9
10
  interface Props {
10
11
  posts?: CollectionEntry<"posts">[];
@@ -81,7 +82,10 @@ const archiveYears = [...archivesByYearMonth.entries()]
81
82
  <li class="stalux-archive-post-item">
82
83
  <time
83
84
  class="stalux-archive-post-date"
84
- datetime={post.data.date}
85
+ datetime={toMachineDateTime(
86
+ post.data.date,
87
+ timezone,
88
+ )}
85
89
  >
86
90
  {formatInTimeZone(
87
91
  parseDate(post.data.date, timezone),
@@ -2,6 +2,7 @@
2
2
  import { getCollection } from "astro:content";
3
3
  import { getSiteData } from "@utils/config-utils";
4
4
  import { createTranslator } from "@utils/i18n";
5
+ import { toMachineDateTime } from "@utils/semantic-time";
5
6
 
6
7
  interface Props {
7
8
  showStats?: boolean;
@@ -13,6 +14,11 @@ const configEntries = await getCollection("config");
13
14
  const siteConfig = getSiteData(configEntries);
14
15
  const { t } = createTranslator(siteConfig.lang || "zh-CN");
15
16
  const { showStats = true, wordCount, buildtime } = Astro.props;
17
+ // buildtime 会被浏览器端 new Date(dataset) 解析:裸串会按访客本地时区解释,
18
+ // 不同时区访客算出的运行时长不同。这里统一转成带站点时区偏移的机器时间。
19
+ const machineBuildtime = buildtime
20
+ ? toMachineDateTime(buildtime, siteConfig.timezone || "Asia/Shanghai")
21
+ : undefined;
16
22
  ---
17
23
 
18
24
  {showStats && (
@@ -34,10 +40,10 @@ const { showStats = true, wordCount, buildtime } = Astro.props;
34
40
  </p>
35
41
  )}
36
42
 
37
- {buildtime && (
43
+ {machineBuildtime && (
38
44
  <div
39
45
  class="stalux-stats-site-build-time stalux-u-text-center"
40
- data-stalux-buildtime={buildtime}
46
+ data-stalux-buildtime={machineBuildtime}
41
47
  data-stalux-runtime-format={t("footer.runtimeFormat")}
42
48
  >
43
49
  <span>
@@ -50,15 +50,42 @@ export interface CollectionPaths {
50
50
  // 文章 Schema
51
51
  // ---------------------------------------------------------------------------
52
52
 
53
+ /**
54
+ * 日期字段的严格格式:月/日/时/分/秒**必须补零**。
55
+ *
56
+ * 允许:`2025-07-04`、`"2025-07-04 18:44:32"`、`2025-05-10T12:00:00+08:00`。
57
+ * 拒绝:`2026-6-9 9:11:00` 这类未补零写法——站点时区解析器(dayjs.ts
58
+ * NAIVE_DATE_PATTERN)只认补零形式,未补零会静默回退到 new Date() 按
59
+ * **构建机器本地时区**解释,导致 RSS/页面时间整体平移(2026-10-24 事故)。
60
+ * 因此在 schema 层直接拒绝,让构建尽快失败,而不是产出错误时间。
61
+ */
62
+ export const PADDED_DATETIME_PATTERN =
63
+ /^\d{4}-\d{2}-\d{2}([ T]\d{2}:\d{2}(:\d{2}(\.\d{1,3})?)?(Z|[+-]\d{2}:?\d{2})?)?$/;
64
+
65
+ const datetimeField = (label: string) =>
66
+ z
67
+ .string({ error: `${label} must be a string` })
68
+ .regex(
69
+ PADDED_DATETIME_PATTERN,
70
+ `${label} 月/日/时/分/秒必须补零,如 "2025-07-04 18:44:32"(拒绝 2026-6-9 9:11:00 这类未补零写法)`,
71
+ );
72
+
73
+ /** words 的日期字段:允许空串(模板预留),非空时必须补零 */
74
+ const paddedOrEmpty = (label: string) =>
75
+ z
76
+ .string()
77
+ .refine(
78
+ (value) => value === "" || PADDED_DATETIME_PATTERN.test(value),
79
+ `${label} 为空或月/日/时/分/秒必须补零,如 "2025-07-04 18:44:32"`,
80
+ );
81
+
53
82
  const postSchema = z.object({
54
83
  title: z.string().min(1, "title is required"),
55
84
  abbrlink: z.union([z.string(), z.number().transform((num) => num.toString())], {
56
85
  error: "abbrlink must be a string or number",
57
86
  }),
58
- date: z
59
- .string({ error: "date must be a string" })
60
- .min(1, "date is required, format: YYYY-MM-DD HH:mm:ss"),
61
- updated: z.string().optional(),
87
+ date: datetimeField("date"),
88
+ updated: datetimeField("updated").optional(),
62
89
  draft: z.boolean().optional().default(false),
63
90
  tags: z.preprocess(
64
91
  (val) => (typeof val === "string" ? [val] : val),
@@ -136,9 +163,11 @@ export function defineCollections(paths: CollectionPaths = {}) {
136
163
  schema: z.object({
137
164
  source: z.string().optional(),
138
165
  link: z.url("words.link must be a valid URL").optional(),
166
+ // sourceDate 是自由文本(如 "1950"、"春秋"),不是机器时间,不做格式约束
139
167
  sourceDate: z.string().optional(),
140
- date: z.string().optional(),
141
- updated: z.string().optional(),
168
+ // words 允许空串(_template.md 预留字段),非空时必须补零
169
+ date: paddedOrEmpty("date").optional(),
170
+ updated: paddedOrEmpty("updated").optional(),
142
171
  draft: z.boolean().optional().default(false),
143
172
  title: z.string().optional(),
144
173
  abbrlink: z.string().optional(),