From b3266a242ee33e1a8df850261752ac22eded8e44 Mon Sep 17 00:00:00 2001 From: Ewan Croft Date: Sat, 15 Aug 2026 07:43:16 +0100 Subject: [PATCH] fix(blog): derive blog date URLs in Europe/London timezone --- src/lib/utils/date.ts | 27 +++++++++++++++++++ src/routes/+page.svelte | 6 ++--- src/routes/blog/+page.server.ts | 15 ++++++----- src/routes/blog/+page.svelte | 20 +++++++------- src/routes/blog/[rkey]/+page.server.ts | 8 +++--- .../[month]/[day]/[slug]/+page.server.ts | 9 ++----- 6 files changed, 51 insertions(+), 34 deletions(-) create mode 100644 src/lib/utils/date.ts diff --git a/src/lib/utils/date.ts b/src/lib/utils/date.ts new file mode 100644 index 0000000..d4b65f7 --- /dev/null +++ b/src/lib/utils/date.ts @@ -0,0 +1,27 @@ +/** + * Extract the year/month/day components of an ISO timestamp in the site's + * canonical timezone (Europe/London). + * + * Used to derive stable blog URLs (`/blog/YYYY/MM/DD/slug`). Do not round-trip + * through `toLocaleString` + `new Date()` — the resulting "dd/mm/yyyy" string + * is re-parsed as US format and yields the wrong date. + */ +export function blogDateParts(iso: string): { + year: string; + month: string; + day: string; +} { + const parts = new Intl.DateTimeFormat("en-GB", { + timeZone: "Europe/London", + year: "numeric", + month: "2-digit", + day: "2-digit", + }).formatToParts(new Date(iso)); + + const byType = new Map(parts.map((p) => [p.type, p.value])); + return { + year: byType.get("year") ?? "", + month: byType.get("month") ?? "", + day: byType.get("day") ?? "", + }; +} diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 446650d..08ca188 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -8,6 +8,7 @@ import Pentacle from '$lib/components/icons/Pentacle.svelte'; import { ArrowRight, ExternalLink, Music } from '@lucide/svelte'; import { normalizeSlug } from '$lib/utils/slugify'; + import { blogDateParts } from '$lib/utils/date'; import type { ProfileData } from '@ewanc26/atproto'; import { PUBLIC_LEAFLET_BLOG_PUBLICATION } from '$env/static/public'; @@ -51,10 +52,7 @@ }); function getBlogUrl(post: any) { - const date = new Date(post.createdAt); - const y = date.getFullYear(); - const m = (date.getMonth() + 1).toString().padStart(2, '0'); - const d = date.getDate().toString().padStart(2, '0'); + const { year: y, month: m, day: d } = blogDateParts(post.createdAt); const slug = normalizeSlug(post.title); return `/blog/${y}/${m}/${d}/${slug}`; } diff --git a/src/routes/blog/+page.server.ts b/src/routes/blog/+page.server.ts index cb48aff..5e9f3c5 100644 --- a/src/routes/blog/+page.server.ts +++ b/src/routes/blog/+page.server.ts @@ -13,6 +13,7 @@ import { PUBLIC_ATPROTO_DID, PUBLIC_LEAFLET_BLOG_PUBLICATION, } from "$env/static/public"; +import { blogDateParts } from "$lib/utils/date"; const PAGE_SIZE = 20; @@ -43,14 +44,14 @@ export const load: PageServerLoad = async ({ fetch, setHeaders }) => { const grouped = new Map>(); for (const post of publicationPosts) { - const date = new Date(post.createdAt); - const year = date.getFullYear(); - const month = date.getMonth() + 1; + const { year, month } = blogDateParts(post.createdAt); + const yearNum = parseInt(year, 10); + const monthNum = parseInt(month, 10); - if (!grouped.has(year)) grouped.set(year, new Map()); - const yearMap = grouped.get(year)!; - if (!yearMap.has(month)) yearMap.set(month, []); - yearMap.get(month)!.push(post); + if (!grouped.has(yearNum)) grouped.set(yearNum, new Map()); + const yearMap = grouped.get(yearNum)!; + if (!yearMap.has(monthNum)) yearMap.set(monthNum, []); + yearMap.get(monthNum)!.push(post); } // Flatten for initial page — take first PAGE_SIZE posts across all groups diff --git a/src/routes/blog/+page.svelte b/src/routes/blog/+page.svelte index 4846b8e..9a47ae6 100644 --- a/src/routes/blog/+page.svelte +++ b/src/routes/blog/+page.svelte @@ -1,5 +1,6 @@