mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
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:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user