Can CeylanVienna-based, globally curious.
Learn/Backend

Separate the editorial date from the publish timestamp, they mean different things

Content systems routinely conflate two different concepts: the date the author wrote something, and when it was actually published. Treating them as one field causes sorting bugs, broken date displays, and incorrect analytics. They need to be separate from the start.

2026-04-25·3 min read·beginner

The conflation

Most content systems start with a single date field. It is used for:

  • Displaying "Published on March 15"
  • Sorting the article list newest-first
  • RSS feed timestamps
  • Analytics attribution
  • SEO canonical dates

This works when all content is written and published at the same time. It breaks as soon as you introduce:

  • Drafts, written weeks before they go live
  • Scheduled publishing, date set in advance
  • Backdated content, articles written about past events with an editorial date that predates publication
  • Edits after publication, content updated months later, editorial date unchanged

The two fields

date (editorial date):

  • Set by the author when creating the content
  • Represents the conceptual "when this piece is about" or "when it was written"
  • Can be set to any date, past, present, future
  • Should be stable after initial creation

publishedAt (publish timestamp):

  • Set by the system when the content first becomes publicly visible
  • Represents "when this actually went live"
  • Must be immutable once set, if you unpublish and republish, keep the original
  • Used for sorting, RSS, analytics, and SEO
interface Article {
  date: string;          // editorial date, author-controlled, any value
  publishedAt?: string;  // ISO timestamp, set on first publish, never changed
  published: boolean;
}

Why publishedAt must be immutable

The temptation is to update publishedAt when an article is re-published after edits. This causes two problems:

  1. RSS readers send duplicate notifications. A changed publishedAt looks like a new article to feed readers, re-notifying everyone who already read it.

  2. Analytics lose their baseline. If you are tracking "views in the first 7 days after publication," changing the publish date resets the window. Your engagement data becomes meaningless.

The correct behavior: set publishedAt once, on the first publish event, and never touch it again.

function setPublishedAt(article: Article, now: string): Article {
  if (article.publishedAt) return article; // already set, do not overwrite
  if (!article.published) return article;  // not published, do not set yet
  return { ...article, publishedAt: now };
}

Backfilling legacy articles

If your system was built with a single date field and you are adding publishedAt now, you need to recover the real publish date for existing articles.

The safest approach is to infer it from signals that were recorded at publish time:

async function inferPublishedAt(slug: string): Promise<string | undefined> {
  const [newsletterSent, blogPublished, distributionRecord] = await Promise.all([
    kv.get(`newsletter:sent:${slug}`),
    kv.get(`blog:published:${slug}`),
    kv.get(`article:distribution:${slug}`),
  ]);

  // Return the earliest reliable timestamp we can find
  return earliest([
    newsletterSent,
    blogPublished?.publishedAt,
    distributionRecord?.doneAt,
  ]);
}

This is better than using date as a fallback, the editorial date is author-controlled and may not reflect when the article actually went live.

Which date to show publicly

On public-facing surfaces (article pages, article lists, RSS feeds), prefer publishedAt over date for display and sorting:

  • Sorting should use publishedAt, it reflects the actual publishing order
  • "Published on" display should use publishedAt, it is factually accurate
  • "Written in" or "Originally from" can use date if relevant

The editorial date is useful internally, for organizing drafts, understanding when something was conceived, but it should not drive public-facing behavior unless you have a specific editorial reason.

The broader rule

Whenever a single field is being used to mean two different things, add the second field. The short-term cost of one extra field is far lower than the long-term cost of incorrect sorting, duplicate RSS notifications, and analytics you cannot trust.

More like this, straight to your inbox.

I write about Backend and a handful of other things I actually care about. No schedule, no filler. Just when I have something worth saying.

More on Backend

Batch email sends before rate limits look like caps

A newsletter send to 13 people reported 5 accepted and 8 failed. It looked like a hidden recipient cap. The real problem was parallel API calls hitting a provider rate limit.

The data isolation audit: every endpoint must be scoped to the requesting user

The most common multi-tenant security bug is an endpoint that returns the right data for the right user, most of the time. A systematic audit ensures user_id filtering is never accidentally omitted.

Never use Promise.all() with the GitHub Contents API

The GitHub Contents API requires each file commit to complete before the next one starts. Parallel commits produce 409 conflicts, and the error message doesn't make it obvious why.

If this raised a question, I'd be happy to talk about it.

Find me →
← Back to Learn