Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions app/components/BlogPostListCard.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script setup lang="ts">
import type { Author } from '#shared/schemas/blog'

defineProps<{
const props = defineProps<{
/** Authors of the blog post */
authors: Author[]
/** Blog Title */
Expand All @@ -19,6 +19,8 @@ defineProps<{
/** Whether this post is an unpublished draft */
draft?: boolean
}>()

const formattedPublished = computed(() => toLocaleDateString(props.published))
</script>

<template>
Expand All @@ -33,7 +35,7 @@ defineProps<{
<!-- Text Content -->
<div class="flex-1 min-w-0 text-start gap-2">
<div class="flex items-center gap-2">
<span class="text-xs text-fg-muted font-mono">{{ published }}</span>
<span class="text-xs text-fg-muted font-mono">{{ formattedPublished }}</span>
<span
v-if="draft"
class="text-xs px-1.5 py-0.5 rounded badge-orange font-sans font-medium"
Expand Down
13 changes: 1 addition & 12 deletions app/components/OgImage/BlogPost.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,7 @@ const props = withDefaults(

const { resolvedAuthors } = useBlueskyAuthorProfiles(props.authors)

const formattedDate = computed(() => {
if (!props.date) return ''
try {
return new Date(props.date).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
})
} catch {
return props.date
}
})
const formattedDate = computed(() => toLocaleDateString(props.date))

const MAX_VISIBLE_AUTHORS = 2

Expand Down
14 changes: 14 additions & 0 deletions app/utils/formatters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,17 @@ export function toIsoDateString(date: Date): string {
const day = String(date.getUTCDate()).padStart(2, '0')
return `${year}-${month}-${day}`
}

export function toLocaleDateString(date: string): string {
if (!date) return ''

try {
return new Date(date).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
})
} catch {
Comment on lines +11 to +17
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

JavaScript new Date('invalid').toLocaleDateString() behaviour throw exception or Invalid Date

💡 Result:

new Date("invalid") creates a Date object whose internal time value is NaN (an “Invalid Date”).

For toLocaleDateString(), the ECMAScript Internationalization (ECMA‑402) spec says:

  • If the Date’s internal time value is NaN, return the string "Invalid Date" (no exception). [1]

So:

new Date("invalid").toLocaleDateString()  // "Invalid Date"

When you do see an exception with date formatting APIs, it’s typically a RangeError due to invalid locales/options (e.g., an invalid timeZone), not because the Date is invalid. [1]

(Separately: toISOString() does throw on an invalid date, which is a different method and behavior.) [2]

Sources: [1] [2]


🌐 Web query:

JavaScript Date.toLocaleDateString() timezone day shift date-only strings timeZone UTC

💡 Result:

Date.prototype.toLocaleDateString() formats the same instant in time in whatever time zone you tell it (default: the user’s local time zone). If that instant crosses midnight in that zone, the calendar day can shift. (developer.mozilla.org)

Why it happens with date-only strings ("YYYY-MM-DD")

In JavaScript, an ISO date-only string like "2026-03-03" is parsed as UTC midnight (2026-03-03T00:00:00.000Z). (stackoverflow.com)
If you then format it in a negative-offset time zone (e.g., America/Los_Angeles), that UTC midnight is still the previous local evening, so the displayed date becomes the day before. (developer.mozilla.org)

const d = new Date("2026-03-03");        // 2026-03-03T00:00:00.000Z (UTC)
d.toLocaleDateString("en-US");           // may show 3/2/2026 in US time zones west of UTC
d.toLocaleDateString("en-US", { timeZone: "UTC" }); // 3/3/2026

Fixes (pick based on what your string means)

  1. If the string is a UTC date (common for APIs): format in UTC
new Date("2026-03-03").toLocaleDateString("en-US", { timeZone: "UTC" });

timeZone is an Intl.DateTimeFormat option used by toLocaleDateString. (developer.mozilla.org)

  1. If the string is a “local calendar date” (e.g., user picked March 3): construct it as local midnight (avoid ISO-string parsing)
const [y, m, d] = "2026-03-03".split("-").map(Number);
new Date(y, m - 1, d).toLocaleDateString("en-US");  // won’t shift by timezone
  1. If you must keep it ISO but want consistent display: store/transport an ISO date-time with an explicit offset (e.g., Z or -05:00) rather than date-only strings. (stackoverflow.com)

Citations:


🏁 Script executed:

cd app/utils && cat -n formatters.ts | head -25

Repository: npmx-dev/npmx.dev

Length of output: 705


Stabilise date parsing and timezone to prevent incorrect published dates.

The try/catch fallback is ineffective: invalid dates do not throw exceptions—toLocaleDateString() returns the string "Invalid Date" instead. Additionally, ISO date-only strings (e.g. "2026-03-01") are parsed as UTC midnight and will display as the previous day in western timezones unless formatted with timeZone: 'UTC'.

Proposed fix
 export function toLocaleDateString(date: string): string {
   if (!date) return ''
 
-  try {
-    return new Date(date).toLocaleDateString('en-US', {
-      year: 'numeric',
-      month: 'short',
-      day: 'numeric',
-    })
-  } catch {
-    return date
-  }
+  const parsed = new Date(date)
+  if (Number.isNaN(parsed.getTime())) return date
+
+  return parsed.toLocaleDateString('en-US', {
+    year: 'numeric',
+    month: 'short',
+    day: 'numeric',
+    timeZone: 'UTC',
+  })
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try {
return new Date(date).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
})
} catch {
export function toLocaleDateString(date: string): string {
if (!date) return ''
const parsed = new Date(date)
if (Number.isNaN(parsed.getTime())) return date
return parsed.toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
timeZone: 'UTC',
})
}

return date
}
}
Loading