-
Notifications
You must be signed in to change notification settings - Fork 112
Update datetime field to correctly store in UTC
#1401
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Update datetime field to correctly store in UTC
#1401
Conversation
…UTC, display in user's local time); update `datetime` documentation
|
|
This would be really nice to see 👍 |
|
@BarnabyBishop @emmatown @JedWatson Happy holidays, hope you all are doing well. 🎄 Would love to see this reviewed and get merged into the next release! This is causing headaches for getting the local timezone right. If I write the date |
|
I tried writing a script to properly convert Instead, I'm parsing these incorrect // Keystatic's datetime field writes local time with a "Z" suffix (treating
// it as UTC), but the value is actually local time. This preprocessor
// interprets "Z" suffix dates as local time in the site's timezone.
import { site } from "@/data/site"; // { timeZone: "America/Los_Angeles" }
function parseInTimeZone(val: unknown) {
let strVal = "";
if (val instanceof Date) {
// If midnight UTC, likely a date-only YAML string
if (val.getUTCHours() === 0 && val.getUTCMinutes() === 0 && val.getUTCSeconds() === 0) {
strVal = val.toISOString().split("T")[0];
} else {
return val;
}
} else if (typeof val === "string") {
strVal = val;
} else {
return val;
}
// If it already has a proper timezone offset (not Z), keep it as-is
if (strVal.includes("T") && /[+-]\d{2}(?::?\d{2})?$/.test(strVal)) {
return strVal;
}
// WORKAROUND: Treat Z suffix as local time (Keystatic bug)
// Convert "2025-12-29T12:00:00.000Z" -> "2025-12-29T12:00:00" + site offset
if (strVal.endsWith("Z")) {
strVal = strVal.slice(0, -1); // Remove the Z
// Strip milliseconds if present (for cleaner canonical format)
strVal = strVal.replace(/\.\d+$/, "");
}
// Handle date-only strings "YYYY-MM-DD"
let iso = strVal;
if (/^\d{4}-\d{2}-\d{2}$/.test(strVal)) {
iso = `${strVal}T00:00:00`;
}
// Find the offset for this date in the target timezone
try {
const date = new Date(iso);
if (isNaN(date.getTime())) return val;
const parts = new Intl.DateTimeFormat("en-US", {
timeZone: site.timeZone,
timeZoneName: "shortOffset",
}).formatToParts(date);
const offsetPart = parts.find((p) => p.type === "timeZoneName")?.value; // "GMT-8"
if (offsetPart?.startsWith("GMT")) {
let offset = offsetPart.slice(3);
if (!offset) offset = "Z";
else {
// Normalize "-8" to "-08:00"
const sign = offset.startsWith("-") ? "-" : "+";
const absOffset = offset.replace(/[+-]/, "");
const [hours, minutes = "00"] = absOffset.split(":");
offset = `${sign}${hours.padStart(2, "0")}:${minutes.padStart(2, "0")}`;
}
return iso + offset;
}
} catch {
// Fallback to default parsing
}
return val;
}
// Use in your Zod schema:
const timezoneDate = z.preprocess(parseInTimeZone, z.coerce.date());Note that date-only dates like As an aside, fixing this kind of seems like a breaking change - dates will start being written differently after this gets fixed, which might break some sites' assumptions on how dates are written by Keystatic. |
fixes #1400
datetimefield (UI component & validation) to serialize/deserialize the value in UTC time, and render the value in the user's local timezonedatetimedocumentation to make it clear thatdatetimestores its value in UTC time, and displays it in the user's local timezone in the editing interface