Skip to content
Consultation

My Homepage Keeps Forgetting I Published

Adityo Guni Waluyo

The homepage blog section used to be three hardcoded cards I edited by hand. Now it fetches the latest posts live, with dates pinned to one timezone.

New post goes live, I open the homepage to check it, and the blog section still shows the same three cards from months ago. The article exists on /blog, but the homepage never heard of it. Third time this happened before I admitted the pattern: the section wasn't broken, it was decorative.

For months frontend/src/components/Blog.tsx read from a blog-posts.json, three hardcoded cards with i18n keys for the titles. Every publish meant the same ritual: remember the homepage exists, edit the JSON, bump the dates by hand, commit. Those dates never came from anywhere. They came from me, typing.

My first guess was caching. I rebuilt the site twice and hard-refreshed like it was 2010. Nothing changed, because the JSON had no idea a new post existed. The source was the lie, not the pipeline.

Make the component async and let it fetch

The fix was smaller than I expected. Server Components in the App Router can fetch data directly: the component becomes an async function and awaits [1]. This is the same pattern I used when I built the related-articles block with a single parallel fetch. So app/[locale]/page.tsx now makes one call:

fetchArticles({ locale, perPage: 6 })

Six latest published articles, straight from the API. Every time I publish something new, the homepage updates itself and I get to keep my weekend.

But I didn't want the homepage blank if the API is down. So the fetch is wrapped in try/catch; if it fails, the blog section just quietly disappears instead of crashing the whole page.

try {
  const articles = await fetchArticles({ locale, perPage: 6 })
  // render the timeline
} catch {
  // the section disappears, no error
}

Blog.tsx stopped owning data and now takes articles and locale as props. Each entry renders through a new ArticleTimelineItem.tsx, a timeline instead of three cards, with a view-all link to /blog.

The try/catch is deliberate. If the API dies, the section vanishes quietly and the rest of the homepage renders fine. I'd rather ship a homepage with a missing section than one that shows an error box, or worse, stale content pretending to be current. That last one is exactly the trap the JSON had dug for me.

Two follow-up commits grew the family: 6 items became 8, and each item got a small chip with its publish time. That chip is where the second bug was hiding.

One date, one time zone

The publish-time chip looked fine to me and wrong to everyone in a different timezone. Same article, same day, but the homepage chip showed one time and the article page another. Wrong guess number two: I assumed server and client disagreed because of a hydration bug.

No. Intl.DateTimeFormat, given no locale and no options, falls back to the runtime's default locale and default time zone [2]. The server rendered the date in its zone, the visitor's browser rendered it in theirs. Both were "correct", which is the worst kind of correct.

So every date now goes through one helper, formatDateWIB in lib/blog/format-date.ts. The formatter stays Intl.DateTimeFormat, but the zone is pinned now instead of following the server:

new Intl.DateTimeFormat(locale, {
  timeZone: 'Asia/Jakarta',
  // ...other options
})

One pinned zone, "Asia/Jakarta", shared by SSR and every visitor. The chip says the same thing everywhere, and that's the whole point. My blog lives in one timezone, so the chips live there too. If you build for a single audience, this is the boring, correct choice; don't reach for visitor-local dates unless you actually want them.

Next.js 15 quietly changed the deal

One thing worth knowing if you copy this pattern: since Next.js 15, fetch is no longer cached by default, and you opt back in with cache: "force-cache" [3]. Swapping the static JSON for a live fetch means my homepage went from a page that never asked anything to one that asks on every render. I haven't tuned that yet. The moment it matters, force-cache with a revalidate window is one line away, and the homepage goes back to being static with fresh-enough data.

The rule I landed on: if content exists somewhere, render it from there. A JSON file mirroring your blog isn't a cache, it's a second source of truth that only knows what you remember to tell it.

Related articles