Publish Date Stuck in the Past: Articles Look Stale After Real Refreshes

Your publishedAt never moves so SERP shows an old date. Add updatedAt, show ONE date, match schema to the visible date, and bump only on substantial edits.

Your article on “ChatGPT plugins” went out in 2023. You rewrote it last month — new screenshots, new model names, new examples — but the publishedAt field still reads 2023-03-14. The SERP snippet shows “Mar 14, 2023” next to your title, readers skip it for a fresher-looking competitor, and Google’s freshness signal treats the page as stale. The content was refreshed; the date just never moved.

Fastest fix: add an optional updatedAt field to your content schema, set it on the articles you genuinely rewrote, display only that one date labeled “Updated,” and emit it as dateModified in your Article JSON-LD so the visible date and the structured date match exactly. Do not bulk-bump every page to today — that does more harm than the stale date did.

The opposite trap is just as costly: bumping the date on every cosmetic edit. If your sitemap lastmod or your byline jumps to today every time someone fixes a typo, Google learns your dates are noise and stops trusting them. Google’s trust in your date signals is effectively binary — full trust or none — and a history of fake-fresh dates poisons the well for every page on the site (per John Mueller’s repeated guidance).

Which bucket are you in?

SymptomRoot causeFix
SERP shows an old date; you definitely rewrote the pageNo updatedAt field, or it exists but nobody set itAdd/set updatedAt; drive visible date + dateModified from it
SERP shows old date even though your updatedAt is recentVisible date and schema date disagree, so Google ignores the schemaMake the visible byline and JSON-LD dateModified identical
You show both “Published” and “Updated” and CTR droppedGoogle often picks the older datePublished to display when both are presentShow only ONE date (Updated, for refreshed content)
Every page reads “updated today”Cosmetic edits bump the date; sitemap lastmod auto-set to build timeBump only on substantial edits; make lastmod reflect real changes
Time-sensitive page (pricing/limits) silently rotsNot flagged volatile; no refresh cadenceAdd volatile: true and a quarterly staleness audit

Common causes

1. Only one date field — and it never moves

Your schema has publishedAt and nothing else. So the date either never moves (the page looks stale) or you bump publishedAt itself (which destroys the real first-publication date and reads as date manipulation). Both are wrong.

How to spot it: open your content collection schema and look for a separate updatedAt / updatedDate field. If it is missing, you have this problem by design.

2. updatedAt exists but authors don’t set it

The field is in the schema, but authors forget to set it on real refreshes. The page header shows “Updated: 2023-03-14” even though the article was rewritten last week.

How to spot it: compare git history against frontmatter. Articles whose last substantive commit is months newer than updatedAt are mislabeled as stale.

# last commit date for a file vs its frontmatter
git log -1 --format=%cs -- src/content/articles/en/troubleshooting/your-slug.mdx

3. The visible date and the schema date disagree

This is the most common reason a correct updatedAt still does not show up. Google does not treat your structured data as the source of truth — it cross-checks the JSON-LD date against the prominent on-page date, the sitemap lastmod, and other signals. When they conflict, Google may ignore your schema entirely and display whichever date it trusts more (frequently the older byline). As of June 2026, the Search Central guidance is explicit: “Ensure that the date (and optional time and timezone) match between the equivalent user-visible and structured values.”

How to spot it: view source on a refreshed article and confirm the date in the visible byline equals the date inside dateModified.

curl -s https://site.com/en/articles/your-slug/ | grep -Eo 'dateModified":"[^"]+'

4. Showing both “Published” and “Updated” — and losing CTR

Counter-intuitively, displaying both dates can hurt you. A widely-cited 2026 test found roughly a 22% CTR drop after adding both a published and an updated date, because Google began surfacing the older datePublished in the snippet even though both were marked up. For frequently-refreshed content, show one date — “Updated” — and let datePublished live only inside the schema.

How to spot it: if your template prints two dates and your refreshed articles still show the old one in search, collapse to a single visible “Updated” date.

5. Every cosmetic edit bumps the date

An author fixes a typo and bumps the date; a week later, another typo, another bump. The article has not substantively changed in a year, but the date moves weekly. Google’s own guidance says minor edits like typo and grammar fixes do not justify a new dateModified. Worse, if your sitemap lastmod follows the same noisy pattern, Google’s trust in the tag is binary — once it sees a history of inaccurate lastmod, it ignores lastmod site-wide.

How to spot it: run git log -p for the date field. If most bumps coincide with single-line diffs, you have date-spam.

6. Fast-moving topics not marked volatile

An article on “current ChatGPT pricing” goes stale every quarter by nature. Without a volatile: true flag it lives in the same publishing rhythm as evergreen content and silently rots between refreshes.

How to spot it: list articles with keywords like “current,” “latest,” a year number, “pricing,” or “limits.” If they lack a volatile flag, they need one.

Shortest path to fix

Step 1: Add a real updatedAt field

Update your content collection schema to carry both dates. publishedAt is set once and never changes; updatedAt is optional and set only on substantial refreshes.

// src/content.config.ts  (Astro's content config; older projects used src/content/config.ts)
import { defineCollection, z } from "astro:content";
import { glob } from "astro/loaders";

const articles = defineCollection({
  loader: glob({ pattern: "**/*.mdx", base: "./src/content/articles" }),
  schema: z.object({
    title: z.string(),
    publishedAt: z.coerce.date(),
    updatedAt: z.coerce.date().optional(),
    volatile: z.boolean().default(false),
    // ...
  }),
});

export const collections = { articles };

z.coerce.date() lets you keep writing plain 2026-05-24 strings in frontmatter while getting real Date objects in your layout.

Step 2: Show ONE date and wire it to dateModified

In your article layout, compute the most recent date once, render it as a single visible byline, and emit the same value as dateModified. Keep datePublished inside the JSON-LD only.

---
const { article } = Astro.props;
const lastUpdate = article.data.updatedAt ?? article.data.publishedAt;
const wasUpdated = !!article.data.updatedAt;
const fmt = (d) => d.toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric" });
---
<time datetime={lastUpdate.toISOString()}>
  {wasUpdated ? "Updated" : "Published"} {fmt(lastUpdate)}
</time>

<script type="application/ld+json" set:html={JSON.stringify({
  "@context": "https://schema.org",
  "@type": "Article",
  datePublished: article.data.publishedAt.toISOString(),
  dateModified: lastUpdate.toISOString(),
})} />

Including a time and timezone in the ISO string adds precision; just make sure the rendered byline and the schema describe the same instant.

Step 3: Make sitemap lastmod tell the truth

Drive lastmod from the same lastUpdate value, not from build time. If you can’t make it accurate, it is safer to omit lastmod than to set it to “now” on every build — an inaccurate history makes Google discard the tag site-wide.

// astro.config.mjs — @astrojs/sitemap
sitemap({
  serialize(item) {
    // map item.url back to the article and set item.lastmod = (updatedAt ?? publishedAt)
    return item;
  },
});

Step 4: Define a refresh policy

Write down exactly when updatedAt should move, then enforce it in review.

- Substantial (BUMP): new section, model/pricing facts updated,
  code example rewritten, screenshots replaced, materially new advice
- Cosmetic (DO NOT BUMP): typo, wording polish, internal link added,
  formatting-only change
- Counterpart-only translation fix: DO NOT BUMP (each language page
  owns its own date)

A reviewer enforces this on PRs; a date bump on a cosmetic-only diff earns a “revert the date” comment.

Step 5: Mark volatile articles and audit them

For pricing, model lists, and “current state of X” pieces, add volatile: true. A quarterly script flags any volatile article whose date is older than 90 days — that list becomes your refresh queue.

// scripts/audit-volatile-staleness.mjs
import fs from "node:fs";
import matter from "gray-matter";

const THRESHOLD_DAYS = 90;
const now = Date.now();
for (const f of /* iterate your article files */ []) {
  const { data } = matter(fs.readFileSync(f, "utf8"));
  if (!data.volatile) continue;
  const last = new Date(data.updatedAt || data.publishedAt).getTime();
  const ageDays = (now - last) / 86_400_000;
  if (ageDays > THRESHOLD_DAYS) {
    console.log(`STALE volatile: ${f} (${Math.round(ageDays)}d old)`);
  }
}

Step 6: Backfill the existing stale labels — selectively

Run a one-time backfill, judging each article rather than rubber-stamping the whole corpus.

1. List articles whose last substantive git commit is 6+ months
   newer than frontmatter publishedAt.
2. For each, judge: did it actually get a substantive refresh?
3. If yes, set updatedAt to the git date of that real edit.
4. If no, leave it alone.

Do not bulk-bump every article to today. Selective, defensible dates only — that is the whole difference between “freshness signal” and “date-spam.”

How to confirm it’s fixed

  1. View source on a refreshed article and confirm the visible byline date equals dateModified in the JSON-LD.
  2. Run the page through the Rich Results TestdatePublished and dateModified should both validate with no warnings.
  3. Check the sitemap entry: curl -s https://site.com/sitemap-0.xml | grep -A1 your-slug and confirm lastmod matches the article’s real update date, not the build timestamp.
  4. After Google recrawls (request indexing in Search Console to speed it up), the SERP snippet should show the new date. This can take days to weeks; the schema/visible-date match is what makes it stick.

Prevention

  • Two-field schema: publishedAt immutable, updatedAt optional for refreshes.
  • Show ONE visible date; never print both “Published” and “Updated” on refreshed content.
  • Visible byline, JSON-LD dateModified, and sitemap lastmod must all agree.
  • Refresh policy documented; reviewer enforces “substantial vs cosmetic” on PRs.
  • volatile: true for time-sensitive topics, with a quarterly staleness audit.
  • One-time, selective backfill using git history as the source of truth.

FAQ

Does Google actually use dateModified, or does it just pick its own date? Both. Google reads dateModified as one freshness signal but cross-checks it against the prominent on-page date and other cues. If those disagree it may ignore your schema and choose the date it trusts more — usually the older one. The reliable move is to make the visible date and the schema date identical.

Should I show both the published and the updated date? Generally no for frequently-refreshed content. As of June 2026, tests have shown CTR drops when both dates are present because Google tends to surface the older datePublished. Show “Updated” only, and keep datePublished inside the structured data.

Will Google penalize me for changing dates? There is no direct penalty, but there is a trust cost. If your sitemap lastmod or byline jumps to “today” without a real content change, Google stops trusting your date signals — often site-wide — which makes it slower to recognize your genuine updates.

How long until the new date shows in search? It only changes after Google recrawls the page. Requesting indexing in Search Console speeds it up, but it can take days to weeks. The schema/visible-date match is what makes the new date persist once it appears.

Should I bump the date when I only fix a typo? No. Google’s own guidance says minor fixes do not justify a new dateModified. Bump only when the substance of the page changes.

Should I update publishedAt instead of adding updatedAt? No. publishedAt is the page’s first-publication date and should be immutable. Overwriting it loses real history and looks like manipulation. Add a separate updatedAt and drive freshness from that.

Tags: #Content ops #Site quality #Site audit #Troubleshooting #publish-date