A structured-data validator can accept a date that is wrong for the page. Check the actual offer, article, or event as well as the markup: a sale may have expired, an event may have moved, or a template may be claiming that an unchanged article was updated today.
Google's structured-data guidelines require information that is current and matches the page. Use the properties supported by the relevant Search feature; a valid Schema.org field is not automatically used in every rich result.
Check the dates that matter for each page
Use these checks when reviewing the markup:
| Content type | Relevant property | Accuracy check |
|---|---|---|
| Product offer | validFrom and priceValidUntil or validThrough, when applicable | The sale price and visible offer are actually active for that period |
| Review | datePublished | The date matches the visible review publication date |
| Article | datePublished, dateModified | Dates match substantive publication and update history |
| Local business | openingHoursSpecification | Hours and validity periods match what customers can use |
| Event | startDate, endDate | Time, timezone, rescheduling, and cancellation state are current |
Find offer dates that need review
In a Laravel e-commerce application, the priceValidUntil property is often set once when the product is created and never updated. A scheduled command can flag products where this date is approaching or has passed:
namespace App\Console\Commands;
use App\Models\Product;
use Illuminate\Console\Command;
class CheckPriceValidity extends Command
{
protected $signature = 'seo:check-price-validity';
public function handle()
{
$expiring = Product::where('price_valid_until', '<', now()->addDays(7))
->where('price_valid_until', '>', now())
->get();
$expired = Product::where('price_valid_until', '<', now())->get();
$this->info("Expiring soon: {$expiring->count()} products");
$this->info("Expired: {$expired->count()} products");
return self::SUCCESS;
}
}
Keeping article dates accurate in Go
Use the article's actual publication and content-update dates. Page views, routine deployments, and unrelated database changes should not advance its modification date. In this example, ContentUpdatedAt is a nullable timestamp maintained when the article itself changes:
type ArticleSchema struct {
Context string `json:"@context"`
Type string `json:"@type"`
Headline string `json:"headline"`
DatePublished string `json:"datePublished"`
DateModified string `json:"dateModified,omitempty"`
}
func BuildArticleSchema(article Article) ArticleSchema {
schema := ArticleSchema{
Context: "https://schema.org",
Type: "Article",
Headline: article.Title,
DatePublished: article.PublishedAt.Format(time.RFC3339),
}
if article.ContentUpdatedAt != nil && article.ContentUpdatedAt.After(article.PublishedAt) {
schema.DateModified = article.ContentUpdatedAt.Format(time.RFC3339)
}
return schema
}
Google's Article documentation recommends ISO 8601 values with timezone information. If the article changes substantively and the page shows an updated date, dateModified should reflect that same event.
Article date accuracy in Next.js
Next.js blog templates can serialise the article's dates from the CMS into JSON-LD. Keep the source fields explicit rather than deriving them during rendering:
const articleJsonLd = {
'@context': 'https://schema.org',
'@type': 'Article',
headline: post.title,
datePublished: post.publishedAt,
...(post.substantiveUpdatedAt
? { dateModified: post.substantiveUpdatedAt }
: {}),
};
An ORM's generic updatedAt can advance for workflow flags, imports, or relation changes. If that timestamp does not mean "visible article changed," add a separate substantive-content timestamp rather than exposing it directly:
// Prisma schema
model Post {
id String @id @default(cuid())
title String
body String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
substantiveUpdatedAt DateTime?
publishedAt DateTime @default(now())
}
Find inconsistent dates in a URL sample
A crawler can extract date fields and produce review candidates. It cannot decide whether a change was substantive without visible content and CMS history. An old article without dateModified is not automatically wrong:
import json, re, requests
from datetime import datetime
def parse_iso8601(value):
return datetime.fromisoformat(value.replace('Z', '+00:00'))
def audit_schema_dates(sitemap_url):
response = requests.get(sitemap_url, timeout=10)
response.raise_for_status()
urls = re.findall(r'<loc>(.*?)</loc>', response.text)
stale_pages = []
for url in urls[:200]: # sample
page = requests.get(url, timeout=10)
page.raise_for_status()
blocks = re.findall(
r'<script type="application/ld\+json">(.*?)</script>',
page.text, re.DOTALL
)
for block in blocks:
data = json.loads(block)
nodes = data.get('@graph', [data]) if isinstance(data, dict) else data
for node in nodes:
if not isinstance(node, dict):
continue
published = node.get('datePublished')
modified = node.get('dateModified')
if published and modified and parse_iso8601(modified) < parse_iso8601(published):
stale_pages.append((url, 'dateModified precedes datePublished'))
return stale_pages
Stop using the request time as an update date
Many CMS templates generate schema with new Date().toISOString() or Carbon::now() at render time, meaning every page claims to have changed on every request. That contradicts the visible content history and makes the metadata unreliable.
Read the date from content history. Keep the real modification date for an unchanged article, or omit an optional modification field when no reliable value exists. After deployment, compare a few live pages with their CMS records and visible dates.
