ehsan.blog
~/blog/how-to-format-dates-with-a-reusable-astro-component — zsh
cat how-to-format-dates-with-a-reusable-astro-component.md

How to format dates consistently with a reusable Astro component

·4 min read

Dates show up all over a blog: post publish dates, “last updated” labels, archive listings. If I format them by hand each time, they end up inconsistent — “Jun 2, 2026” here, “2026-06-02” there — and I repeat the same fiddly formatting code everywhere. The fix is one tiny reusable component. Here is the exact one this blog uses, and why it is built the way it is.

The whole component

astro
// blog/src/components/FormattedDate.astro
---
interface Props {
  date: Date
  class?: string
}
const { date, class: className = "" } = Astro.props
const iso = date.toISOString()
const formatted = date.toLocaleDateString("en-US", {
  year: "numeric",
  month: "short",
  day: "numeric",
})
---

<time datetime={iso} class:list={["font-mono", className]}>{formatted}</time>

That is all of it. Small, but it does three important jobs at once. Let me unpack them.

Typing the props

ts
interface Props {
  date: Date
  class?: string
}
const { date, class: className = "" } = Astro.props

The Props interface tells Astro (and TypeScript) what this component accepts: a required date (a real JavaScript Date object) and an optional class string. Astro reads this interface to type-check how you use the component — pass a string where a Date is expected and you get an error before you ever build.

One small trick: class is a reserved word in JavaScript, so I rename it while destructuring — class: className — and default it to an empty string. That lets callers pass extra CSS classes without breaking anything.

Formatting for humans with Intl.DateTimeFormat

ts
const formatted = date.toLocaleDateString("en-US", {
  year: "numeric",
  month: "short",
  day: "numeric",
})

toLocaleDateString is part of the built-in Intl API — the browser and Node’s standard tools for formatting dates, numbers, and currencies for a given locale. No library needed.

  • "en-US" picks the locale, which controls ordering and language.
  • The options object asks for a numeric year, a short month name, and a numeric day.

So a Date for June 2, 2026 comes out as Jun 2, 2026. Because every date on the site flows through this one function, they all look identical. That is the entire point of centralizing it.

Formatting for machines with <time datetime>

astro
<time datetime={iso} class:list={["font-mono", className]}>{formatted}</time>

Here is the part beginners often miss. The visible text Jun 2, 2026 is friendly for people but ambiguous for computers. So the component also produces a machine-readable version:

ts
const iso = date.toISOString()

toISOString() gives a standardized string like 2026-06-02T00:00:00.000Z. That goes into the datetime attribute of the HTML <time> element. The <time> element is HTML’s semantic tag for “this text is a date/time,” and its datetime attribute holds the unambiguous version.

Why bother? Because search engines, feed readers, and assistive tech read datetime — not the pretty text. So one element serves both audiences:

flowchart LR
  A[Date object] --> B["toLocaleDateString -> Jun 2, 2026 (humans)"]
  A --> C["toISOString -> 2026-06-02T... (machines)"]
  B --> D["&lt;time datetime=machine&gt;human&lt;/time&gt;"]
  C --> D

The class:list helper

astro
class:list={["font-mono", className]}

class:list is an Astro convenience that joins an array into a single class string, skipping empty values cleanly. It always applies font-mono (the site’s monospace look) and appends whatever extra class the caller passed. No manual string concatenation, no stray spaces.

Using it

Anywhere a date needs to appear:

astro
---
import FormattedDate from "@/components/FormattedDate.astro"
---
<FormattedDate date={post.data.date} />
<FormattedDate date={post.data.date} class="text-sm opacity-70" />

Note you pass a real Date, not a string. If your data source gives you a string, wrap it: new Date("2026-06-02"). In this blog the dates come from post frontmatter already typed as Date, thanks to the Zod z.coerce.date() field in the content collection schema, so it just works.

Why one component beats formatting inline

Centralizing this means: consistent output everywhere, one place to change the format if I ever want “June 2, 2026” instead, and the accessibility/SEO win of <time datetime> baked in so I never forget it. That is a lot of value from ten lines of code.

What to remember

  • Put date formatting in one small component so every date on the site matches.
  • Intl via toLocaleDateString formats dates for humans with no external library.
  • toISOString() plus <time datetime> gives machines an unambiguous version of the same date — the same ISO format also feeds the BlogPosting JSON-LD datePublished field.
  • Rename the class prop (class: className) since class is a reserved word, and default it.
  • class:list cleanly merges your base classes with caller-supplied ones.
ls ./related
cat ./comments

Comments are not configured yet. Enable GitHub Discussions and paste the giscus repo-id / category-id into src/consts.ts.