Skip to content

Commit 1fc07b1

Browse files
Add New/Updated recency pills to docs sidebar (#959)
* Add New/Updated recency pills to docs sidebar Doc pages can now surface a New or Updated pill in the docs sidebar, driven by optional `addedAt` / `updatedAt` ISO dates in each library's `docs/config.json`. Pills auto-expire after 7 days, so the sidebar self-cleans with no follow-up edits and no per-page GitHub API calls. - config.ts: add optional addedAt/updatedAt to the sidebar child valibot schema (core + per-framework) and the MenuItem type - DocsLayout.tsx: add pure getDocRecency() helper (7-day window, guards invalid and future dates) and DocRecencyPill; render in both the external <a> and internal <Link> branches; New takes priority over Updated when both are recent - tanstack-docs-config.schema.json: add the two fields so maintainers get editor validation/autocomplete * ci: apply automated fixes --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent 4363ad0 commit 1fc07b1

3 files changed

Lines changed: 108 additions & 1 deletion

File tree

src/components/DocsLayout.tsx

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,75 @@ import { Card } from './Card'
3030
import { PartnersRail, RightRail } from './RightRail'
3131
import { trackEvent, useTrackedImpression } from '~/utils/analytics'
3232

33+
// Number of days a doc page is flagged as "New"/"Updated" in the sidebar.
34+
const RECENCY_WINDOW_DAYS = 7
35+
const RECENCY_WINDOW_MS = RECENCY_WINDOW_DAYS * 24 * 60 * 60 * 1000
36+
37+
type DocRecency = 'new' | 'updated' | null
38+
39+
// Determine whether a doc page should show a recency pill, based on the
40+
// maintainer-supplied `addedAt` / `updatedAt` dates in the repo's docs/config.json.
41+
// "New" (added) takes priority over "Updated" (edited) when both are recent.
42+
function getDocRecency(addedAt?: string, updatedAt?: string): DocRecency {
43+
const now = Date.now()
44+
45+
const isRecent = (iso?: string) => {
46+
if (!iso) return false
47+
const time = new Date(iso).getTime()
48+
if (Number.isNaN(time)) return false
49+
const age = now - time
50+
// Reject future dates; only flag within the window.
51+
return age >= 0 && age <= RECENCY_WINDOW_MS
52+
}
53+
54+
if (isRecent(addedAt)) return 'new'
55+
if (isRecent(updatedAt)) return 'updated'
56+
return null
57+
}
58+
59+
function DocRecencyPill({
60+
recency,
61+
date,
62+
}: {
63+
recency: Exclude<DocRecency, null>
64+
date?: string
65+
}) {
66+
const isNew = recency === 'new'
67+
const label = isNew ? 'New' : 'Updated'
68+
69+
let title: string | undefined
70+
if (date) {
71+
// Parse date-only strings (YYYY-MM-DD) as local time so the tooltip doesn't
72+
// drift to the previous day in negative-UTC timezones (new Date('2026-06-01')
73+
// is UTC midnight, which toLocaleDateString would render as the prior day).
74+
const dateOnly = /^(\d{4})-(\d{2})-(\d{2})$/.exec(date)
75+
const parsed = dateOnly
76+
? new Date(
77+
Number(dateOnly[1]),
78+
Number(dateOnly[2]) - 1,
79+
Number(dateOnly[3]),
80+
)
81+
: new Date(date)
82+
if (!Number.isNaN(parsed.getTime())) {
83+
title = `${isNew ? 'Added' : 'Updated'} ${parsed.toLocaleDateString()}`
84+
}
85+
}
86+
87+
return (
88+
<span
89+
title={title}
90+
className={twMerge(
91+
'shrink-0 rounded-full px-1.5 py-0.5 text-[9px] font-bold uppercase tracking-wide leading-none',
92+
isNew
93+
? 'bg-emerald-500/15 text-emerald-600 dark:text-emerald-400'
94+
: 'bg-sky-500/15 text-sky-600 dark:text-sky-400',
95+
)}
96+
>
97+
{label}
98+
</span>
99+
)
100+
}
101+
33102
// Mobile partners strip - inline in the docs toggle bar
34103
function MobilePartnersStrip({
35104
partners,
@@ -696,6 +765,14 @@ export function DocsLayout({
696765
? ({ libraryId, version } as never)
697766
: undefined
698767

768+
const recency = getDocRecency(child.addedAt, child.updatedAt)
769+
const recencyPill = recency ? (
770+
<DocRecencyPill
771+
recency={recency}
772+
date={recency === 'new' ? child.addedAt : child.updatedAt}
773+
/>
774+
) : null
775+
699776
return (
700777
<li key={i}>
701778
{child.to.startsWith('http') ? (
@@ -705,7 +782,8 @@ export function DocsLayout({
705782
target="_blank"
706783
rel="noopener noreferrer"
707784
>
708-
{child.label}
785+
<span className="w-full">{child.label}</span>
786+
{recencyPill}
709787
</a>
710788
) : (
711789
<Link
@@ -741,6 +819,7 @@ export function DocsLayout({
741819
>
742820
{child.label}
743821
</div>
822+
{recencyPill}
744823
</div>
745824
)
746825
}}

src/utils/config.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ export type MenuItem = {
1212
label: string | React.ReactNode
1313
to: string
1414
badge?: string
15+
/** ISO date string marking when the page was added. Drives the "New" sidebar pill. */
16+
addedAt?: string
17+
/** ISO date string marking when the page was last meaningfully updated. Drives the "Updated" sidebar pill. */
18+
updatedAt?: string
1519
}[]
1620
collapsible?: boolean
1721
defaultCollapsed?: boolean
@@ -26,6 +30,8 @@ const configSchema = v.object({
2630
label: v.string(),
2731
to: v.string(),
2832
badge: v.optional(v.string()),
33+
addedAt: v.optional(v.string()),
34+
updatedAt: v.optional(v.string()),
2935
}),
3036
),
3137
frameworks: v.optional(
@@ -37,6 +43,8 @@ const configSchema = v.object({
3743
label: v.string(),
3844
to: v.string(),
3945
badge: v.optional(v.string()),
46+
addedAt: v.optional(v.string()),
47+
updatedAt: v.optional(v.string()),
4048
}),
4149
),
4250
}),

tanstack-docs-config.schema.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,16 @@
5353
},
5454
"badge": {
5555
"type": "string"
56+
},
57+
"addedAt": {
58+
"type": "string",
59+
"format": "date",
60+
"description": "Date the page was added (e.g. \"2026-06-01\"). Shows a \"New\" pill in the sidebar for 7 days."
61+
},
62+
"updatedAt": {
63+
"type": "string",
64+
"format": "date",
65+
"description": "Date the page was last meaningfully updated (e.g. \"2026-06-01\"). Shows an \"Updated\" pill in the sidebar for 7 days."
5666
}
5767
}
5868
}
@@ -79,6 +89,16 @@
7989
},
8090
"badge": {
8191
"type": "string"
92+
},
93+
"addedAt": {
94+
"type": "string",
95+
"format": "date",
96+
"description": "Date the page was added (e.g. \"2026-06-01\"). Shows a \"New\" pill in the sidebar for 7 days."
97+
},
98+
"updatedAt": {
99+
"type": "string",
100+
"format": "date",
101+
"description": "Date the page was last meaningfully updated (e.g. \"2026-06-01\"). Shows an \"Updated\" pill in the sidebar for 7 days."
82102
}
83103
}
84104
}

0 commit comments

Comments
 (0)