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:
RSS readers send duplicate notifications. A changed
publishedAtlooks like a new article to feed readers, re-notifying everyone who already read it.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
dateif 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.