Add New/Updated recency pills to docs sidebar#959
Conversation
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
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughThis pull request adds visual "New" and "Updated" indicators to sidebar documentation menu items. The feature reads optional ChangesSidebar Recency Pills
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~22 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/components/DocsLayout.tsx`:
- Around line 39-46: getDocRecency and DocRecencyPill parse date-only strings
inconsistently (UTC vs local), causing the recency pill to disagree with the
tooltip across timezones; create a shared parser (e.g., parseDateOnlyIso or
parseDocDate) that detects "YYYY-MM-DD" and constructs a local Date (new
Date(year, month-1, day)) and use that parser in isRecent (or getDocRecency) and
in DocRecencyPill when formatting the tooltip so both recency gating and
displayed date are computed from the same normalized Date instance.
In `@src/utils/config.ts`:
- Around line 33-34: Replace the loose v.optional(v.string()) validators for
addedAt/updatedAt with a shared recencyDateField using Valibot’s v.isoDate
wrapped in v.optional to enforce YYYY-MM-DD; update the validators in the
top-level fields and the nested frameworks[].children[] entries (the same
properties referenced in parseDocsConfig / the config schema) to use
recencyDateField so safeParse only accepts ISO date strings and preserves the
“New/Updated” recency logic.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 178ce36e-e3ee-42b9-bb5f-5eb08f8046d4
📒 Files selected for processing (3)
src/components/DocsLayout.tsxsrc/utils/config.tstanstack-docs-config.schema.json
| 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 | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
In JavaScript, does new Date('YYYY-MM-DD') parse as UTC or local time, and what timezone edge cases does that create for day-based windows like “last 7 days”?
💡 Result:
In JavaScript, the new Date('YYYY-MM-DD') constructor parses date-only strings as UTC midnight [1][2][3]. This is a specific behavior mandated by the ECMAScript specification for date-only strings, which differs from ISO 8601's general recommendation to treat such strings as local time [2][4][5]. Conversely, date-time strings (such as 'YYYY-MM-DDTHH:mm:ss') are parsed as local time [2][6][3]. This discrepancy creates significant edge cases for day-based windows like "last 7 days": 1. Off-by-one Date Shifts: Because new Date('YYYY-MM-DD') creates a UTC timestamp, it will effectively represent a different calendar day for users in timezones west of UTC [2][7]. For example, a user in New York (UTC-5) parsing '2026-06-04' will get a Date object representing '2026-06-03' at 7:00 PM local time [2]. When the application performs comparisons against "today," this shifted timestamp may cause the system to misidentify the current day [7]. 2. DST and Calculation Errors: If you calculate date ranges by dividing milliseconds (e.g., diff / 86400000), you may encounter errors when the window crosses a Daylight Saving Time (DST) boundary [8]. In these cases, a "day" may not consist of exactly 24 hours, leading to non-integer results or incorrect counts [8]. 3. Inconsistent Comparison Context: Comparing a UTC-parsed date object against local-time-based "current" time often leads to silent bugs [7][9]. If your application logic mixes UTC-parsed inputs with local-time-calculated windows, the "last 7 days" window will drift based on the user's specific timezone and their system's local clock [10][9]. To avoid these issues, developers are encouraged to: - Avoid new Date(string) for parsing whenever possible [2][4]. - Use explicit component-based construction, such as new Date(year, monthIndex, day), which correctly uses the local timezone [11][2]. - Normalize dates to a "UTC day index" (e.g., Date.UTC(y, m, d) / 86400000) for comparisons to ensure they remain consistent regardless of the user's timezone or DST status [8]. - Rely on user-profile-specific timezones instead of the host machine's local time for application-wide date filtering [10].
Citations:
- 1: https://stackoverflow.com/questions/57600841/ambiguous-behavior-of-javascript-new-date-constructor-for-same-different-forma
- 2: https://dev.to/work_hau_cb718f47075930f9/stop-using-new-date2026-01-01-to-parse-date-strings-523p
- 3: https://thelinuxcode.com/javascript-dateparse-practical-mental-model-safe-formats-and-production-patterns-2026/
- 4: https://dateandtimeconverter.com/guides/javascript-date-parsing/
- 5: https://stackoverflow.com/questions/2587345/why-does-date-parse-give-incorrect-results
- 6: https://apisurfer.com/js-date-constructor-timezone-oddity
- 7: Relative date display is off by one day for users west of UTC twentyhq/twenty#19634
- 8: https://dev.to/cornel_gabriel_d4237164ca/i-built-16-date-time-calculators-in-vanilla-javascript-heres-what-i-learned-about-time-and-2pdn
- 9: https://dev.to/nishchaldev/why-date-and-time-bugs-happen-in-javascript-and-how-apps-actually-get-it-right-hjh
- 10: fix(web): use user profile timezone for date range filtering langgenius/dify#32119
- 11: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/Date
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Locate the targeted file and functions/components
ls -la src/components/DocsLayout.tsx
rg -n "getDocRecency|DocRecencyPill" src/components/DocsLayout.tsx
# Show the relevant code around the indicated lines and around the located functions
python3 - <<'PY'
import itertools, pathlib
path = pathlib.Path("src/components/DocsLayout.tsx")
lines = path.read_text().splitlines()
for start,end in [(1,120),(30,110),(35,95),(1,80)]:
print("\n### slice",start,"-",end)
for i in range(start-1,min(end,len(lines))):
print(f"{i+1:4d}: {lines[i]}")
PYRepository: TanStack/tanstack.com
Length of output: 13914
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Show code around the call site where DocRecencyPill is rendered
python3 - <<'PY'
import pathlib
path = pathlib.Path("src/components/DocsLayout.tsx")
lines = path.read_text().splitlines()
start, end = 700, 780
for i in range(start, min(end, len(lines))):
print(f"{i+1:4d}: {lines[i]}")
PY
# Find where addedAt/updatedAt are defined/typed/constructed
rg -n "addedAt|updatedAt" src | head -n 80
# Also search for config.json references mentioning recency fields
rg -n "config\.json|docs/config" -S src | head -n 80
# Identify the type of child (where getDocRecency(child.addedAt, child.updatedAt) comes from)
rg -n "getDocRecency\(" src/components/DocsLayout.tsxRepository: TanStack/tanstack.com
Length of output: 10332
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n "addedAt" src/utils src/components | head -n 200
rg -n "updatedAt" src/utils/config* src/utils | head -n 200
# Inspect config schema definitions around where MenuItem type is defined
rg -n "type MenuItem|interface MenuItem|addedAt\\??:" src/utils/config.ts src/utils -S || true
# Print relevant sections of src/utils/config.ts (small targeted windows)
python3 - <<'PY'
import pathlib, re
p = pathlib.Path("src/utils/config.ts")
if not p.exists():
print("missing src/utils/config.ts")
raise SystemExit(0)
lines = p.read_text().splitlines()
# print windows around likely occurrences
for pat in ["addedAt", "updatedAt", "MenuItem", "docs/config", "recency", "type ConfigSchema"]:
for i,l in enumerate(lines):
if pat in l:
start=max(0,i-20); end=min(len(lines), i+40)
print(f"\n=== around line {i+1} (pattern {pat}) ===")
for j in range(start,end):
print(f"{j+1:4d}: {lines[j]}")
break
PYRepository: TanStack/tanstack.com
Length of output: 18490
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n "dateOnly|YYYY-MM-DD|\\(\\\\d\\{4\\}\\)-\\(\\\\d\\{2\\}\\)-\\(\\\\d\\{2\\}\\)|toLocaleDateString\\(" src | head -n 200
rg -n "parseDocDate|getDocRecency|RECENCY_WINDOW" src/components src/utils | head -n 200Repository: TanStack/tanstack.com
Length of output: 3719
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Check file sizes quickly
wc -l src/utils/dates.ts
# Print relevant part of dates.ts
nl -ba src/utils/dates.ts | sed -n '1,220p'Repository: TanStack/tanstack.com
Length of output: 149
🏁 Script executed:
#!/bin/bash
set -euo pipefail
cat -n src/utils/dates.tsRepository: TanStack/tanstack.com
Length of output: 3749
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Find likely config.json files in-repo (avoid build dirs)
fd -e json "config.json" . | head -n 200
# Search for addedAt/updatedAt fields in JSON files (prioritize config.json)
rg -n "\"addedAt\"|\"updatedAt\"" --glob='**/*.{json,ts,tsx,js,jsx,mjs,cjs}' . | head -n 120Repository: TanStack/tanstack.com
Length of output: 383
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Print the relevant sections around the schema lines
python3 - <<'PY'
import pathlib
p = pathlib.Path("tanstack-docs-config.schema.json")
lines = p.read_text().splitlines()
for start,end in [(40,110),(80,120)]:
print(f"\n### slice {start}-{end}")
for i in range(start-1, min(end, len(lines))):
print(f"{i+1:4d}: {lines[i]}")
PYRepository: TanStack/tanstack.com
Length of output: 5082
Deduplicate date parsing for addedAt/updatedAt to keep recency gating and tooltip aligned
getDocRecency parses addedAt/updatedAt via new Date(iso) (date-only strings become UTC midnight), while DocRecencyPill parses YYYY-MM-DD as local time before using toLocaleDateString(). Since the docs config schema defines these as date-only (e.g. "2026-06-01"), the “last 7 days” pill can shift relative to the tooltip day across timezones.
💡 Suggested fix
+const DATE_ONLY_RE = /^(\d{4})-(\d{2})-(\d{2})$/
+
+function parseDocDate(value?: string): Date | null {
+ if (!value) return null
+ const m = DATE_ONLY_RE.exec(value)
+ const parsed = m
+ ? new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]))
+ : new Date(value)
+ return Number.isNaN(parsed.getTime()) ? null : parsed
+}
+
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 parsed = parseDocDate(iso)
+ if (!parsed) return false
+ const time = parsed.getTime()
const age = now - time
const age = now - time
return age >= 0 && age <= RECENCY_WINDOW_MS
}
@@
let title: string | undefined
if (date) {
- 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())) {
+ const parsed = parseDocDate(date)
+ if (parsed) {
title = `${isNew ? 'Added' : 'Updated'} ${parsed.toLocaleDateString()}`
}
}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/components/DocsLayout.tsx` around lines 39 - 46, getDocRecency and
DocRecencyPill parse date-only strings inconsistently (UTC vs local), causing
the recency pill to disagree with the tooltip across timezones; create a shared
parser (e.g., parseDateOnlyIso or parseDocDate) that detects "YYYY-MM-DD" and
constructs a local Date (new Date(year, month-1, day)) and use that parser in
isRecent (or getDocRecency) and in DocRecencyPill when formatting the tooltip so
both recency gating and displayed date are computed from the same normalized
Date instance.
| addedAt: v.optional(v.string()), | ||
| updatedAt: v.optional(v.string()), |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
For valibot v1.3.1, what is the recommended validator for optional YYYY-MM-DD date strings (and is there a built-in helper preferred over regex)?
💡 Result:
For Valibot v1.3.1, the recommended validator for a YYYY-MM-DD date string is the built-in isoDate action [1][2]. This is preferred over using a custom regex, as it is a standard, maintained, and type-safe solution provided by the library [2][3]. You can implement this by piping v.string with v.isoDate [1]. To make the field optional, use the v.optional wrapper [4][5]. Example implementation: import * as v from 'valibot'; const OptionalDateSchema = v.optional( v.pipe( v.string, v.isoDate('Invalid date format, expected YYYY-MM-DD.'))); Please note that as documented by Valibot, the built-in isoDate validator uses a regex that checks for the YYYY-MM-DD format but does not validate the maximum number of days based on the specific month or leap years (e.g., "2023-06-31" would be considered valid) [1][2]. If strict calendar date validation is required, you may need to add an additional custom validation step [6].
Citations:
- 1: https://valibot.dev/api/isoDate/
- 2: https://jsr.io/@valibot/valibot/doc/~/isoDate
- 3: https://valibot.dev/api/IsoDateAction/
- 4: https://valibot.dev/api/date/
- 5: https://jsr.io/@valibot/valibot/doc
- 6: https://valibot.dev/guides/pipelines/
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Locate config schema file
SCHEMA_FILE="$(fd -i 'tanstack-docs-config\.schema\.json' . || true)"
echo "SCHEMA_FILE=$SCHEMA_FILE"
# Inspect the current validation code around the reported lines
echo "---- src/utils/config.ts (lines 1-120) ----"
if [ -f "src/utils/config.ts" ]; then
sed -n '1,120p' src/utils/config.ts | nl -ba | sed -n '1,120p'
else
echo "Missing src/utils/config.ts"
fi
# Search for addedAt/updatedAt in config utils
echo "---- usages of addedAt/updatedAt in src/utils/config.ts ----"
rg -n "addedAt|updatedAt" src/utils/config.ts || true
# Inspect schema json if found
if [ -n "${SCHEMA_FILE:-}" ] && [ -f "$SCHEMA_FILE" ]; then
echo "---- schema: occurrences of addedAt/updatedAt ----"
rg -n "\"addedAt\"|\"updatedAt\"|addedAt|updatedAt" "$SCHEMA_FILE" || true
echo "---- schema: surrounding context (first few matches) ----"
rg -n "\"addedAt\"|\"updatedAt\"" "$SCHEMA_FILE" -n | head -n 20 || true
else
echo "No tanstack-docs-config.schema.json found via fd"
fi
# Find where configSchema is used/validated (optional but helps justify impact)
echo "---- search for configSchema usage ----"
rg -n "configSchema" src || trueRepository: TanStack/tanstack.com
Length of output: 218
🏁 Script executed:
#!/bin/bash
set -euo pipefail
SCHEMA_FILE="$(fd -i 'tanstack-docs-config\.schema\.json' . || true)"
echo "SCHEMA_FILE=${SCHEMA_FILE:-<not found>}"
echo "---- src/utils/config.ts (lines 1-120) ----"
if [ -f "src/utils/config.ts" ]; then
python3 - <<'PY'
import pathlib
p = pathlib.Path("src/utils/config.ts")
lines = p.read_text(encoding="utf-8").splitlines()
for i in range(1, min(120, len(lines))+1):
print(f"{i:4d}: {lines[i-1]}")
PY
fi
echo "---- reported snippet (lines 25-60) ----"
if [ -f "src/utils/config.ts" ]; then
python3 - <<'PY'
import pathlib
p = pathlib.Path("src/utils/config.ts")
lines = p.read_text(encoding="utf-8").splitlines()
start, end = 25, 60
for i in range(start, min(end, len(lines))+1):
print(f"{i:4d}: {lines[i-1]}")
PY
fi
echo "---- usages of addedAt/updatedAt in src/utils/config.ts ----"
if [ -f "src/utils/config.ts" ]; then
rg -n "addedAt|updatedAt" src/utils/config.ts || true
fi
if [ -n "${SCHEMA_FILE:-}" ] && [ -f "$SCHEMA_FILE" ]; then
echo "---- schema: occurrences of addedAt/updatedAt ----"
rg -n "\"addedAt\"|\"updatedAt\"|addedAt|updatedAt" "$SCHEMA_FILE" || true
echo "---- schema: surrounding context for those keys (first ~80 lines around first match) ----"
# grab first match line number
first=$(rg -n "\"addedAt\"|\"updatedAt\"" "$SCHEMA_FILE" | head -n 1 | cut -d: -f1 || true)
if [ -n "${first:-}" ]; then
start=$((first-40)); if [ $start -lt 1 ]; then start=1; fi
end=$((first+40))
python3 - <<PY
import pathlib
p = pathlib.Path("$SCHEMA_FILE")
lines = p.read_text(encoding="utf-8").splitlines()
start=$start; end=$end
for i in range(start, min(end, len(lines))+1):
print(f"{i:5d}: {lines[i-1]}")
PY
fi
else
echo "Schema file not found; skipping schema inspection."
fi
echo "---- search for schema validation usage (configSchema/config) ----"
rg -n "configSchema|tanstack-docs-config\.schema|validate|parse" src || trueRepository: TanStack/tanstack.com
Length of output: 50377
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Search for recency/new/updated rendering logic and usages of addedAt/updatedAt
rg -n "addedAt|updatedAt" src | head -n 200
echo "----"
rg -n "\"New\"|New\" pill|Updated\"|recency|sidebar pill|sidebar.*pill" src | head -n 200
echo "----"
rg -n "format\\s*===\\s*['\"]date['\"]|format.*date|isoDate|YYYY-MM-DD" src/utils src/components src/routes | head -n 200Repository: TanStack/tanstack.com
Length of output: 16463
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect recency rendering/parsing logic
python3 - <<'PY'
import pathlib
p = pathlib.Path("src/components/DocsLayout.tsx")
lines = p.read_text(encoding="utf-8").splitlines()
for i in range(1, 120):
print(f"{i:5d}: {lines[i-1]}")
PY
echo "---- recency usage near sidebar pill ----"
python3 - <<'PY'
import pathlib
p = pathlib.Path("src/components/DocsLayout.tsx")
lines = p.read_text(encoding="utf-8").splitlines()
start, end = 700, 820
for i in range(start, min(end, len(lines))+1):
print(f"{i:5d}: {lines[i-1]}")
PY
# Search for valibot isoDate usage in repo
rg -n "isoDate\\b" src || true
# Print the exact section of schema json around addedAt/updatedAt matches
SCHEMA_FILE="./tanstack-docs-config.schema.json"
python3 - <<'PY'
import pathlib, re
p = pathlib.Path("./tanstack-docs-config.schema.json")
lines = p.read_text(encoding="utf-8").splitlines()
# find first occurrences with line numbers
targets = ["\"addedAt\"", "\"updatedAt\""]
for t in targets:
for idx, line in enumerate(lines, start=1):
if t in line:
start=max(1, idx-8); end=min(len(lines), idx+20)
print(f"\n---- {t} around line {idx} ({start}-{end}) ----")
for j in range(start, end+1):
print(f"{j:5d}: {lines[j-1]}")
break
PYRepository: TanStack/tanstack.com
Length of output: 12630
Tighten addedAt/updatedAt validation to YYYY-MM-DD to preserve recency pills
src/utils/config.ts currently uses v.optional(v.string()) for addedAt/updatedAt (also at the nested frameworks[].children[] occurrence). This lets arbitrary strings pass parseDocsConfig’s v.safeParse, but src/components/DocsLayout.tsx treats non-parseable dates (new Date(iso) → NaN) as “not recent”, so the “New/Updated” sidebar pill is skipped silently. The published contract (tanstack-docs-config.schema.json) declares these fields as type: string with format: date (example "2026-06-01"), so runtime validation should match.
Use Valibot’s built-in v.isoDate (prefer it over a custom regex) wrapped in v.optional(...), e.g. via a shared recencyDateField and wire it to both addedAt and updatedAt.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/utils/config.ts` around lines 33 - 34, Replace the loose
v.optional(v.string()) validators for addedAt/updatedAt with a shared
recencyDateField using Valibot’s v.isoDate wrapped in v.optional to enforce
YYYY-MM-DD; update the validators in the top-level fields and the nested
frameworks[].children[] entries (the same properties referenced in
parseDocsConfig / the config schema) to use recencyDateField so safeParse only
accepts ISO date strings and preserves the “New/Updated” recency logic.
What
Adds "New" (green) and "Updated" (blue) recency pills to the docs sidebar, so readers can spot fresh content at a glance.
Each sidebar entry can carry optional
addedAt/updatedAtISO dates in a library'sdocs/config.json:{ "label": "Installation", "to": "framework/react/installation", "addedAt": "2026-06-01" }addedAtwithin the last 7 days → New pillupdatedAtwithin the last 7 days → Updated pill ("New" wins if both are recent)This is maintainer-controlled and costs nothing at request time (no per-page GitHub API calls). The dates live in each library repo's
docs/config.json; this PR makes tanstack.com support and render them.Why
Requested by a user on X (reply to @tan_stack): "can we have new badge on sidebar?? like for new features??"
Changes
src/utils/config.ts— optionaladdedAt/updatedAton sidebar children (valibot schema for core + per-framework entries, and theMenuItemtype)src/components/DocsLayout.tsx— puregetDocRecency()helper (7-day window; guards invalid and future dates) and aDocRecencyPillcomponent, rendered in both the external<a>and internal<Link>sidebar branchestanstack-docs-config.schema.json— the two new fields added so maintainers get editor validation/autocompleteVerification
pnpm run test(tsc + ESLint): clean (0 errors)Follow-up
To make pills visible on a given library, maintainers add
addedAt/updatedAtto entries in that repo'sdocs/config.json.Summary by CodeRabbit