Skip to content

Commit 79c57bf

Browse files
authored
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.
1 parent 96c67e9 commit 79c57bf

3 files changed

Lines changed: 69 additions & 21 deletions

File tree

.claude/rules/landing-seo-geo.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
---
22
paths:
33
- "apps/sim/app/(landing)/**/*.tsx"
4+
- "apps/sim/content/**/*.mdx"
45
---
56

67
# Landing Page — SEO / GEO
@@ -24,3 +25,22 @@ paths:
2425
- **Keyword density**: first 150 visible chars of Hero must name "Sim", "AI workspace", "AI agents".
2526
- **sr-only summaries**: Hero and Templates each have a `<p className="sr-only">` (~50 words) as an atomic product/catalog summary for AI citation.
2627
- **Specific numbers**: prefer concrete figures ("1,000+ integrations", "15+ AI providers") over vague claims.
28+
29+
## Citations and linking (`/library`, `/blog`, `/comparisons`)
30+
31+
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.
32+
33+
- **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"`.
34+
- **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.
35+
- **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.
36+
- **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.
37+
38+
## Freshness
39+
40+
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.
41+
42+
- **Emit `dateModified`** in the page's structured data (JSON-LD or microdata), and emit it exactly once per document.
43+
- **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.
44+
- **Only surface a modified date when it differs from the publish date** — an "Updated" label on the publish day is noise.
45+
- **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.
46+
- **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.

apps/sim/app/(landing)/comparisons/[provider]/page.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,16 @@ export default async function ComparisonProviderPage({
177177
Sim is the open-source AI workspace where teams build, deploy, and manage AI agents
178178
visually, conversationally, or with code. Here is how Sim compares to{' '}
179179
{competitor.name} on platform architecture, AI capabilities, integrations, pricing,
180-
security, and support. Every fact below is sourced and dated.
180+
security, and support. Every fact below is sourced and dated, last verified{' '}
181+
<time dateTime={latestVerified.toISOString().slice(0, 10)}>
182+
{latestVerified.toLocaleDateString('en-US', {
183+
month: 'long',
184+
day: 'numeric',
185+
year: 'numeric',
186+
timeZone: 'UTC',
187+
})}
188+
</time>
189+
.
181190
</p>
182191
<p className='sr-only'>
183192
Sim is an open-source AI workspace for building, deploying, and managing AI agents.

apps/sim/app/(landing)/components/content-post-page/content-post-page.tsx

Lines changed: 39 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,16 @@ import { BackLink } from '@/app/(landing)/components/back-link'
77
import { JsonLd } from '@/app/(landing)/components/json-ld'
88
import { ShareButton } from '@/app/(landing)/components/share-button'
99

10+
/** Renders an ISO date as "Jul 1, 2026". Pinned to UTC so the day matches the frontmatter date in every reader's timezone. */
11+
function formatDate(iso: string): string {
12+
return new Date(iso).toLocaleDateString('en-US', {
13+
month: 'short',
14+
day: 'numeric',
15+
year: 'numeric',
16+
timeZone: 'UTC',
17+
})
18+
}
19+
1020
interface ContentPostPageProps {
1121
/** Route base path, e.g. `/blog` or `/library`. */
1222
basePath: string
@@ -32,6 +42,8 @@ export function ContentPostPage({
3242
shareUrl,
3343
}: ContentPostPageProps) {
3444
const Article = post.Content
45+
const modifiedIso = post.updated ?? post.date
46+
const showUpdated = modifiedIso.slice(0, 10) !== post.date.slice(0, 10)
3547

3648
return (
3749
<article className='w-full bg-[var(--bg)]' itemScope itemType='https://schema.org/BlogPosting'>
@@ -73,20 +85,32 @@ export function ContentPostPage({
7385
{post.description}
7486
</p>
7587
</div>
76-
<div className='mt-6 flex items-center gap-6'>
77-
<time
78-
className='text-[var(--text-muted)] text-xs uppercase tracking-[0.1em]'
79-
dateTime={post.date}
80-
itemProp='datePublished'
81-
>
82-
{new Date(post.date).toLocaleDateString('en-US', {
83-
month: 'short',
84-
day: 'numeric',
85-
year: 'numeric',
86-
timeZone: 'UTC',
87-
})}
88-
</time>
89-
<meta itemProp='dateModified' content={post.updated ?? post.date} />
88+
<div className='mt-6 flex flex-wrap items-center gap-x-6 gap-y-2'>
89+
<div className='flex items-center gap-2'>
90+
<time
91+
className='text-[var(--text-muted)] text-xs uppercase tracking-[0.1em]'
92+
dateTime={post.date}
93+
itemProp='datePublished'
94+
>
95+
{formatDate(post.date)}
96+
</time>
97+
{showUpdated ? (
98+
<>
99+
<span aria-hidden='true' className='text-[var(--text-muted)] text-xs'>
100+
·
101+
</span>
102+
<time
103+
className='text-[var(--text-muted)] text-xs uppercase tracking-[0.1em]'
104+
dateTime={modifiedIso}
105+
itemProp='dateModified'
106+
>
107+
Updated {formatDate(modifiedIso)}
108+
</time>
109+
</>
110+
) : (
111+
<meta itemProp='dateModified' content={modifiedIso} />
112+
)}
113+
</div>
90114
<div className='flex items-center gap-3'>
91115
{(post.authors || [post.author]).map((a) => (
92116
<div key={a?.name} className='flex items-center gap-2'>
@@ -151,12 +175,7 @@ export function ContentPostPage({
151175
</div>
152176
<div className='flex flex-col gap-2'>
153177
<span className='text-[var(--text-muted)] text-xs uppercase tracking-[0.1em]'>
154-
{new Date(p.date).toLocaleDateString('en-US', {
155-
month: 'short',
156-
day: 'numeric',
157-
year: 'numeric',
158-
timeZone: 'UTC',
159-
})}
178+
{formatDate(p.date)}
160179
</span>
161180
<h3 className='text-[var(--text-primary)] text-lg leading-tight tracking-[-0.01em]'>
162181
{p.title}

0 commit comments

Comments
 (0)