feat: add global and per-post timezone support (#491)

* refactor: replace embedded svg with svg icon

* feat: add global and per-post timezone support

- add `SITE.timezone` in config to define a global default timezone.
- allow per-post timezone overrides via `timezone` in frontmatter.
- update date formatting logic to use the correct timezone when rendering timestamps.
- ensure that posts without a timezone fallback to `SITE.timezone`.
- improve consistency between local and deployed timezones.

* refactor: replace `LOCALE` constant with  `SITE.lang`

* docs: update guides

* fix: update code word-break inside markdown table

Closes #466
This commit is contained in:
Sat Naing
2025-03-22 17:21:21 +07:00
committed by GitHub
parent a397677d84
commit 5cff7c50b9
16 changed files with 70 additions and 86 deletions
+2 -2
View File
@@ -10,7 +10,7 @@ export interface Props extends CollectionEntry<"blog"> {
const { variant = "h2", data, id, filePath } = Astro.props;
const { title, pubDatetime, modDatetime, description } = data;
const { title, description, pubDatetime, modDatetime, timezone } = data;
const headerProps = {
style: { viewTransitionName: slugifyStr(title) },
@@ -31,6 +31,6 @@ const headerProps = {
)
}
</a>
<Datetime {pubDatetime} {modDatetime} />
<Datetime {pubDatetime} {modDatetime} {timezone} />
<p>{description}</p>
</li>
+27 -35
View File
@@ -1,11 +1,19 @@
---
import { LOCALE } from "@/constants";
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
import timezone from "dayjs/plugin/timezone";
import IconCalendar from "@/assets/icons/IconCalendar.svg";
import { SITE } from "@/config";
dayjs.extend(utc);
dayjs.extend(timezone);
export interface Props {
class?: string;
size?: "sm" | "lg";
timezone: string | undefined;
pubDatetime: string | Date;
modDatetime: string | Date | undefined | null;
size?: "sm" | "lg";
class?: string;
}
const {
@@ -13,52 +21,36 @@ const {
modDatetime,
size = "sm",
class: className = "",
timezone: postTimezone,
} = Astro.props;
/* ========== Formatted Datetime ========== */
const myDatetime = new Date(
modDatetime && modDatetime > pubDatetime ? modDatetime : pubDatetime
);
const date = myDatetime.toLocaleDateString(LOCALE.langTag, {
year: "numeric",
month: "short",
day: "numeric",
});
const latestDatetime =
modDatetime && modDatetime > pubDatetime ? modDatetime : pubDatetime;
const datetime = dayjs(latestDatetime).tz(postTimezone || SITE.timezone);
const time = myDatetime.toLocaleTimeString(LOCALE.langTag, {
hour: "2-digit",
minute: "2-digit",
});
const date = datetime.format("D MMM, YYYY"); // e.g., '22 Mar, 2025'
const time = datetime.format("hh:mm A"); // e.g., '08:30 PM'
---
<div class={`flex items-end space-x-2 opacity-80 ${className}`.trim()}>
<svg
xmlns="http://www.w3.org/2000/svg"
class={`${
size === "sm" ? "scale-90" : "scale-100"
} inline-block h-6 w-6 min-w-[1.375rem] fill-foreground`}
aria-hidden="true"
>
<path
d="M7 11h2v2H7zm0 4h2v2H7zm4-4h2v2h-2zm0 4h2v2h-2zm4-4h2v2h-2zm0 4h2v2h-2z"
></path>
<path
d="M5 22h14c1.103 0 2-.897 2-2V6c0-1.103-.897-2-2-2h-2V2h-2v2H9V2H7v2H5c-1.103 0-2 .897-2 2v14c0 1.103.897 2 2 2zM19 8l.001 12H5V8h14z"
></path>
</svg>
<div class:list={["flex items-end space-x-2 opacity-80", className]}>
<IconCalendar
class:list={[
"inline-block size-6 min-w-[1.375rem]",
{ "scale-90": size === "sm" },
]}
/>
{
modDatetime && modDatetime > pubDatetime ? (
<span
class={`italic ${size === "sm" ? "text-sm" : "text-sm sm:text-base"}`}
>
<span class:list={["text-sm italic", { "sm:text-base": size === "lg" }]}>
Updated:
</span>
) : (
<span class="sr-only">Published:</span>
)
}
<span class={`italic ${size === "sm" ? "text-sm" : "text-sm sm:text-base"}`}>
<time datetime={myDatetime.toISOString()}>{date}</time>
<span class:list={["text-sm italic", { "sm:text-base": size === "lg" }]}>
<time datetime={datetime.toISOString()}>{date}</time>
<span aria-hidden="true"> | </span>
<span class="sr-only">&nbsp;at&nbsp;</span>
<span class="text-nowrap">{time}</span>