improvement(content): surface last-verified and updated dates, codify citation rules (#5904)

* improvement(content): surface last-verified and updated dates, codify citation rules

Comparison pages already computed a latest-verified date from every fact
source's asOf and emitted it as JSON-LD dateModified, but never showed it.
Library and blog posts had the same gap: dateModified existed only as an
invisible meta tag. A freshness signal that no reader can see does nothing
for the reader deciding whether to trust the page.

- /comparisons/[provider] renders "Last verified <date>" from the existing
  getLatestVerifiedDate(), so the visible date and the JSON-LD read the
  same value and cannot drift
- /library and /blog posts render "Updated <date>" next to the publish
  date, only when the modified day actually differs; otherwise the meta
  fallback stays. dateModified is emitted exactly once either way
- collapse three duplicated toLocaleDateString blocks in
  content-post-page into one module-scope formatDate (same UTC pinning,
  identical output)
- landing-seo-geo rules: add citation/internal-linking and freshness
  sections, and extend the rule's paths to content MDX so it attaches when
  authoring posts, not just TSX

* fix(content): wrap post metadata row so the Updated date cannot overflow on narrow screens

The published/updated/authors/share row was a non-wrapping flex. With the
Updated label active and two authors it overflowed at 390px; the row also
already overflowed at 320px on staging before this PR, with no Updated
label at all. flex-wrap fixes both and leaves desktop identical.

* improvement(comparisons): fold last-verified date into the intro sentence

The paragraph already ended "Every fact below is sourced and dated" and a
separate line then stated the date, which read as redundant and added a
standalone metadata row to the header. Folding it into that sentence drops
the extra line and keeps the date as real server-rendered text in a <time>
element, so crawlers and AI answer engines still see it. A tooltip would
not: Tooltip.Content renders null during SSR and while closed.
This commit is contained in:
Waleed
2026-07-23 16:47:00 -07:00
committed by GitHub
parent 96c67e9123
commit 79c57bfaf6
3 changed files with 69 additions and 21 deletions
+20
View File
@@ -1,6 +1,7 @@
---
paths:
- "apps/sim/app/(landing)/**/*.tsx"
- "apps/sim/content/**/*.mdx"
---
# Landing Page — SEO / GEO
@@ -24,3 +25,22 @@ paths:
- **Keyword density**: first 150 visible chars of Hero must name "Sim", "AI workspace", "AI agents".
- **sr-only summaries**: Hero and Templates each have a `<p className="sr-only">` (~50 words) as an atomic product/catalog summary for AI citation.
- **Specific numbers**: prefer concrete figures ("1,000+ integrations", "15+ AI providers") over vague claims.
## Citations and linking (`/library`, `/blog`, `/comparisons`)
The Princeton GEO study (Aggarwal et al., KDD 2024, [arXiv:2311.09735](https://arxiv.org/abs/2311.09735)) found that adding citations, quotations, and statistics were the three strongest of nine tested tactics, worth 30–40% relative lifts in AI-answer visibility. Sourcing is also what makes a claim checkable by a human reader.
- **Every third-party factual claim carries an outbound source link.** Pricing, rate limits, feature availability, licensing, compliance certifications — link the primary source (the vendor's own pricing page, docs, changelog, or license file), not a secondary blog. External links get `rel="noopener noreferrer"`.
- **Prefer the primary source over a roundup.** Citing another vendor's comparison post to substantiate a fact about them is second-hand and ages badly.
- **Internal links: 3–5 per library post**, pointing at genuinely related library entries, using real `href`s (Next `<Link>` in TSX; a plain markdown link in MDX). A post with zero internal links is a dead end for crawlers and readers alike.
- **Never fabricate a citation.** An unlinked claim is better than a link that does not substantiate it. If a number cannot be sourced, cut the number.
## Freshness
Answer engines weight recency to avoid repeating stale facts, and a reader deciding whether to trust a pricing comparison wants to know when it was last checked. The vendor-published "fresh content earns Nx more citations" figures are directional, not measured — the reason to do this is that both signals must agree and both must be real.
- **Emit `dateModified`** in the page's structured data (JSON-LD or microdata), and emit it exactly once per document.
- **Show the same date to the reader.** `/comparisons/[provider]` renders "Last verified …" from `getLatestVerifiedDate()`; `/library` and `/blog` posts render "Updated …" next to the publish date. A date that exists only in metadata is invisible to a reader deciding whether to trust the page.
- **Only surface a modified date when it differs from the publish date** — an "Updated" label on the publish day is noise.
- **Bump the date only on a substantive edit.** Touching frontmatter without changing the content is date-washing; it degrades the signal for every other page on the domain.
- **Comparison facts are dated at the fact level.** Every `Fact` in `apps/sim/lib/compare/data` carries `sources: [{ url, label, asOf }]`. Re-checking a fact means updating its `asOf`, which flows through `getLatestVerifiedDate()` to the visible date, the JSON-LD, and the sitemap.
@@ -177,7 +177,16 @@ export default async function ComparisonProviderPage({
Sim is the open-source AI workspace where teams build, deploy, and manage AI agents
visually, conversationally, or with code. Here is how Sim compares to{' '}
{competitor.name} on platform architecture, AI capabilities, integrations, pricing,
security, and support. Every fact below is sourced and dated.
security, and support. Every fact below is sourced and dated, last verified{' '}
<time dateTime={latestVerified.toISOString().slice(0, 10)}>
{latestVerified.toLocaleDateString('en-US', {
month: 'long',
day: 'numeric',
year: 'numeric',
timeZone: 'UTC',
})}
</time>
.
</p>
<p className='sr-only'>
Sim is an open-source AI workspace for building, deploying, and managing AI agents.
@@ -7,6 +7,16 @@ import { BackLink } from '@/app/(landing)/components/back-link'
import { JsonLd } from '@/app/(landing)/components/json-ld'
import { ShareButton } from '@/app/(landing)/components/share-button'
/** Renders an ISO date as "Jul 1, 2026". Pinned to UTC so the day matches the frontmatter date in every reader's timezone. */
function formatDate(iso: string): string {
return new Date(iso).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
timeZone: 'UTC',
})
}
interface ContentPostPageProps {
/** Route base path, e.g. `/blog` or `/library`. */
basePath: string
@@ -32,6 +42,8 @@ export function ContentPostPage({
shareUrl,
}: ContentPostPageProps) {
const Article = post.Content
const modifiedIso = post.updated ?? post.date
const showUpdated = modifiedIso.slice(0, 10) !== post.date.slice(0, 10)
return (
<article className='w-full bg-[var(--bg)]' itemScope itemType='https://schema.org/BlogPosting'>
@@ -73,20 +85,32 @@ export function ContentPostPage({
{post.description}
</p>
</div>
<div className='mt-6 flex items-center gap-6'>
<time
className='text-[var(--text-muted)] text-xs uppercase tracking-[0.1em]'
dateTime={post.date}
itemProp='datePublished'
>
{new Date(post.date).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
timeZone: 'UTC',
})}
</time>
<meta itemProp='dateModified' content={post.updated ?? post.date} />
<div className='mt-6 flex flex-wrap items-center gap-x-6 gap-y-2'>
<div className='flex items-center gap-2'>
<time
className='text-[var(--text-muted)] text-xs uppercase tracking-[0.1em]'
dateTime={post.date}
itemProp='datePublished'
>
{formatDate(post.date)}
</time>
{showUpdated ? (
<>
<span aria-hidden='true' className='text-[var(--text-muted)] text-xs'>
·
</span>
<time
className='text-[var(--text-muted)] text-xs uppercase tracking-[0.1em]'
dateTime={modifiedIso}
itemProp='dateModified'
>
Updated {formatDate(modifiedIso)}
</time>
</>
) : (
<meta itemProp='dateModified' content={modifiedIso} />
)}
</div>
<div className='flex items-center gap-3'>
{(post.authors || [post.author]).map((a) => (
<div key={a?.name} className='flex items-center gap-2'>
@@ -151,12 +175,7 @@ export function ContentPostPage({
</div>
<div className='flex flex-col gap-2'>
<span className='text-[var(--text-muted)] text-xs uppercase tracking-[0.1em]'>
{new Date(p.date).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
timeZone: 'UTC',
})}
{formatDate(p.date)}
</span>
<h3 className='text-[var(--text-primary)] text-lg leading-tight tracking-[-0.01em]'>
{p.title}