diff --git a/src/components/DocsLayout.tsx b/src/components/DocsLayout.tsx index 426258c7..8813dee0 100644 --- a/src/components/DocsLayout.tsx +++ b/src/components/DocsLayout.tsx @@ -30,6 +30,75 @@ import { Card } from './Card' import { PartnersRail, RightRail } from './RightRail' import { trackEvent, useTrackedImpression } from '~/utils/analytics' +// Number of days a doc page is flagged as "New"/"Updated" in the sidebar. +const RECENCY_WINDOW_DAYS = 7 +const RECENCY_WINDOW_MS = RECENCY_WINDOW_DAYS * 24 * 60 * 60 * 1000 + +type DocRecency = 'new' | 'updated' | null + +// Determine whether a doc page should show a recency pill, based on the +// maintainer-supplied `addedAt` / `updatedAt` dates in the repo's docs/config.json. +// "New" (added) takes priority over "Updated" (edited) when both are recent. +function getDocRecency(addedAt?: string, updatedAt?: string): DocRecency { + const now = Date.now() + + const isRecent = (iso?: string) => { + if (!iso) return false + const time = new Date(iso).getTime() + if (Number.isNaN(time)) return false + const age = now - time + // Reject future dates; only flag within the window. + return age >= 0 && age <= RECENCY_WINDOW_MS + } + + if (isRecent(addedAt)) return 'new' + if (isRecent(updatedAt)) return 'updated' + return null +} + +function DocRecencyPill({ + recency, + date, +}: { + recency: Exclude + date?: string +}) { + const isNew = recency === 'new' + const label = isNew ? 'New' : 'Updated' + + let title: string | undefined + if (date) { + // Parse date-only strings (YYYY-MM-DD) as local time so the tooltip doesn't + // drift to the previous day in negative-UTC timezones (new Date('2026-06-01') + // is UTC midnight, which toLocaleDateString would render as the prior day). + const dateOnly = /^(\d{4})-(\d{2})-(\d{2})$/.exec(date) + const parsed = dateOnly + ? new Date( + Number(dateOnly[1]), + Number(dateOnly[2]) - 1, + Number(dateOnly[3]), + ) + : new Date(date) + if (!Number.isNaN(parsed.getTime())) { + title = `${isNew ? 'Added' : 'Updated'} ${parsed.toLocaleDateString()}` + } + } + + return ( + + {label} + + ) +} + // Mobile partners strip - inline in the docs toggle bar function MobilePartnersStrip({ partners, @@ -696,6 +765,14 @@ export function DocsLayout({ ? ({ libraryId, version } as never) : undefined + const recency = getDocRecency(child.addedAt, child.updatedAt) + const recencyPill = recency ? ( + + ) : null + return (
  • {child.to.startsWith('http') ? ( @@ -705,7 +782,8 @@ export function DocsLayout({ target="_blank" rel="noopener noreferrer" > - {child.label} + {child.label} + {recencyPill} ) : ( {child.label} + {recencyPill} ) }} diff --git a/src/utils/config.ts b/src/utils/config.ts index ae3c2454..b9b5958d 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -12,6 +12,10 @@ export type MenuItem = { label: string | React.ReactNode to: string badge?: string + /** ISO date string marking when the page was added. Drives the "New" sidebar pill. */ + addedAt?: string + /** ISO date string marking when the page was last meaningfully updated. Drives the "Updated" sidebar pill. */ + updatedAt?: string }[] collapsible?: boolean defaultCollapsed?: boolean @@ -26,6 +30,8 @@ const configSchema = v.object({ label: v.string(), to: v.string(), badge: v.optional(v.string()), + addedAt: v.optional(v.string()), + updatedAt: v.optional(v.string()), }), ), frameworks: v.optional( @@ -37,6 +43,8 @@ const configSchema = v.object({ label: v.string(), to: v.string(), badge: v.optional(v.string()), + addedAt: v.optional(v.string()), + updatedAt: v.optional(v.string()), }), ), }), diff --git a/tanstack-docs-config.schema.json b/tanstack-docs-config.schema.json index 18d02309..9c3521d3 100644 --- a/tanstack-docs-config.schema.json +++ b/tanstack-docs-config.schema.json @@ -53,6 +53,16 @@ }, "badge": { "type": "string" + }, + "addedAt": { + "type": "string", + "format": "date", + "description": "Date the page was added (e.g. \"2026-06-01\"). Shows a \"New\" pill in the sidebar for 7 days." + }, + "updatedAt": { + "type": "string", + "format": "date", + "description": "Date the page was last meaningfully updated (e.g. \"2026-06-01\"). Shows an \"Updated\" pill in the sidebar for 7 days." } } } @@ -79,6 +89,16 @@ }, "badge": { "type": "string" + }, + "addedAt": { + "type": "string", + "format": "date", + "description": "Date the page was added (e.g. \"2026-06-01\"). Shows a \"New\" pill in the sidebar for 7 days." + }, + "updatedAt": { + "type": "string", + "format": "date", + "description": "Date the page was last meaningfully updated (e.g. \"2026-06-01\"). Shows an \"Updated\" pill in the sidebar for 7 days." } } }