Commit 47e1468

HPCesia <me@hpcesia.com>
2026-03-05 10:22:47
Time zone config for posts
1 parent e959c0f
src/contents/posts/test.typ
@@ -6,12 +6,12 @@
     title: "Test page",
     author: "HPCesia",
     desc: "Test Page",
-    date:  datetime(
+    date: datetime(
       year: 2025,
       month: 2,
       day: 3,
     ),
-  )
+  ),
 )<frontmatter>
 
 // ======================================
src/pages/posts/index.astro
@@ -1,14 +1,16 @@
 ---
 import { getCollection } from "astro:content";
 import GlobalLayout from "@/layouts/GlobalLayout.astro";
+import { DateTime } from "luxon";
+import { SITE_CONFIG } from "@/config";
 
 const posts = await getCollection("posts");
 const sortedPosts = posts.sort(
-  (a, b) => new Date(b.data.date).getTime() - new Date(a.data.date).getTime(),
+  (a, b) => b.data.date.getTime() - a.data.date.getTime(),
 );
 const postsByYear = sortedPosts.reduce(
   (acc, post) => {
-    const year = new Date(post.data.date).getFullYear();
+    const year = post.data.date.getFullYear();
     if (!acc[year]) {
       acc[year] = [];
     }
@@ -29,17 +31,22 @@ const postsByYear = sortedPosts.reduce(
           <section>
             <h2>{year}</h2>
             <ul class="text-lg lg:text-xl">
-              {posts.map((post) => (
-                <li class="grid grid-rows-1 grid-cols-[1fr_auto] gap-2">
-                  <a href={`/posts/${post.id}`}>{post.data.title}</a>
-                  <time
-                    datetime={post.data.date.toISOString().split("T")[0]}
-                    class="text-sm lg:text-base text-neutral-600 dark:text-neutral-400"
-                  >
-                    {new Date(post.data.date).toLocaleDateString()}
-                  </time>
-                </li>
-              ))}
+              {posts.map((post) => {
+                const dt = DateTime.fromJSDate(post.data.date, {
+                  zone: SITE_CONFIG.timeZone,
+                });
+                return (
+                  <li class="grid grid-rows-1 grid-cols-[1fr_auto] gap-2">
+                    <a href={`/posts/${post.id}`}>{post.data.title}</a>
+                    <time
+                      datetime={dt.toISO()}
+                      class="text-sm lg:text-base text-neutral-600 dark:text-neutral-400"
+                    >
+                      {dt.toISODate()}
+                    </time>
+                  </li>
+                );
+              })}
             </ul>
           </section>
         ))
src/types/config.d.ts
@@ -15,4 +15,10 @@ export type SiteConfig = {
    * Description of you website.
    */
   description: string;
+  /**
+   * Time zone of typst posts' date.
+   * It should be a valid IANA time zone name,
+   * e.g. "Etc/UTC", "America/New_York", "Asia/Shanghai".
+   */
+  timeZone: string;
 };
src/config.ts
@@ -6,4 +6,5 @@ export const SITE_CONFIG: SiteConfig = {
   author: "HPCesia",
   description:
     "A Tufted style static blog template built with Astro and Typst.",
+  timeZone: "Etc/UTC",
 };
src/content.config.ts
@@ -1,6 +1,8 @@
 import { defineCollection } from "astro:content";
 import { glob } from "astro/loaders";
 import { z } from "astro/zod";
+import { DateTime } from "luxon";
+import { SITE_CONFIG } from "./config";
 
 const typCollection = defineCollection({
   loader: glob({
@@ -13,14 +15,30 @@ const typCollection = defineCollection({
     desc: z.any().optional(),
     date: z.string().transform((str) => {
       const match = str.match(
-        /datetime\(year:\s*(\d+),\s*month:\s*(\d+),\s*day:\s*(\d+)\)/,
+        /datetime\(\s*year:\s*(\d+),?\s*month:\s*(\d+),?\s*day:\s*(\d+)(?:,?\s*hour:\s*(\d+))?(?:,?\s*minute:\s*(\d+))?(?:,?\s*second:\s*(\d+))?,?\s*\)/,
       );
       if (match) {
-        return new Date(
-          parseInt(match[1], 10),
-          parseInt(match[2], 10) - 1,
-          parseInt(match[3], 10),
+        const year = parseInt(match[1], 10);
+        const month = parseInt(match[2], 10);
+        const day = parseInt(match[3], 10);
+        const hour = match[4] ? parseInt(match[4], 10) : 0;
+        const minute = match[5] ? parseInt(match[5], 10) : 0;
+        const second = match[6] ? parseInt(match[6], 10) : 0;
+
+        const dt = DateTime.fromObject(
+          {
+            year,
+            month,
+            day,
+            hour,
+            minute,
+            second,
+          },
+          {
+            zone: SITE_CONFIG.timeZone,
+          },
         );
+        return dt.toJSDate();
       }
       return z.NEVER;
     }),
package.json
@@ -16,10 +16,12 @@
     "@tailwindcss/vite": "^4.2.1",
     "astro": "^5.18.0",
     "astro-typst": "^0.12.3",
+    "luxon": "^3.7.2",
     "tailwindcss": "^4.2.1",
     "unist-util-visit": "^5.1.0"
   },
   "devDependencies": {
-    "@biomejs/biome": "^2.4.5"
+    "@biomejs/biome": "^2.4.5",
+    "@types/luxon": "^3.7.1"
   }
 }
pnpm-lock.yaml
@@ -29,6 +29,9 @@ importers:
       astro-typst:
         specifier: ^0.12.3
         version: 0.12.3(@myriaddreamin/typst-ts-node-compiler@0.7.0-rc2)(@myriaddreamin/typst-ts-renderer@0.7.0-rc2)(@myriaddreamin/typst.ts@0.7.0-rc2(@myriaddreamin/typst-ts-renderer@0.7.0-rc2))(astro@5.18.0(jiti@2.6.1)(lightningcss@1.31.1)(rollup@4.59.0)(typescript@5.9.3))(cheerio@1.2.0)(rollup@4.59.0)
+      luxon:
+        specifier: ^3.7.2
+        version: 3.7.2
       tailwindcss:
         specifier: ^4.2.1
         version: 4.2.1
@@ -39,6 +42,9 @@ importers:
       '@biomejs/biome':
         specifier: ^2.4.5
         version: 2.4.5
+      '@types/luxon':
+        specifier: ^3.7.1
+        version: 3.7.1
 
 packages:
 
@@ -954,6 +960,9 @@ packages:
   '@types/hast@3.0.4':
     resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==}
 
+  '@types/luxon@3.7.1':
+    resolution: {integrity: sha512-H3iskjFIAn5SlJU7OuxUmTEpebK6TKB8rxZShDslBMZJ5u9S//KM1sbdAisiSrqwLQncVjnpi2OK2J51h+4lsg==}
+
   '@types/mdast@4.0.4':
     resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==}
 
@@ -1531,6 +1540,10 @@ packages:
     resolution: {integrity: sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==}
     engines: {node: 20 || >=22}
 
+  luxon@3.7.2:
+    resolution: {integrity: sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==}
+    engines: {node: '>=12'}
+
   magic-string@0.30.21:
     resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
 
@@ -2891,6 +2904,8 @@ snapshots:
     dependencies:
       '@types/unist': 3.0.3
 
+  '@types/luxon@3.7.1': {}
+
   '@types/mdast@4.0.4':
     dependencies:
       '@types/unist': 3.0.3
@@ -3644,6 +3659,8 @@ snapshots:
 
   lru-cache@11.2.6: {}
 
+  luxon@3.7.2: {}
+
   magic-string@0.30.21:
     dependencies:
       '@jridgewell/sourcemap-codec': 1.5.5