Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 80 additions & 1 deletion src/components/DocsLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Comment on lines +39 to +46
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 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:


🏁 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]}")
PY

Repository: 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.tsx

Repository: 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
PY

Repository: 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 200

Repository: 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.ts

Repository: 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 120

Repository: 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]}")
PY

Repository: 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.


if (isRecent(addedAt)) return 'new'
if (isRecent(updatedAt)) return 'updated'
return null
}

function DocRecencyPill({
recency,
date,
}: {
recency: Exclude<DocRecency, null>
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 (
<span
title={title}
className={twMerge(
'shrink-0 rounded-full px-1.5 py-0.5 text-[9px] font-bold uppercase tracking-wide leading-none',
isNew
? 'bg-emerald-500/15 text-emerald-600 dark:text-emerald-400'
: 'bg-sky-500/15 text-sky-600 dark:text-sky-400',
)}
>
{label}
</span>
)
}

// Mobile partners strip - inline in the docs toggle bar
function MobilePartnersStrip({
partners,
Expand Down Expand Up @@ -696,6 +765,14 @@ export function DocsLayout({
? ({ libraryId, version } as never)
: undefined

const recency = getDocRecency(child.addedAt, child.updatedAt)
const recencyPill = recency ? (
<DocRecencyPill
recency={recency}
date={recency === 'new' ? child.addedAt : child.updatedAt}
/>
) : null

return (
<li key={i}>
{child.to.startsWith('http') ? (
Expand All @@ -705,7 +782,8 @@ export function DocsLayout({
target="_blank"
rel="noopener noreferrer"
>
{child.label}
<span className="w-full">{child.label}</span>
{recencyPill}
</a>
) : (
<Link
Expand Down Expand Up @@ -741,6 +819,7 @@ export function DocsLayout({
>
{child.label}
</div>
{recencyPill}
</div>
)
}}
Expand Down
8 changes: 8 additions & 0 deletions src/utils/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()),
Comment on lines +33 to +34
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 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:


🏁 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 || true

Repository: 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 || true

Repository: 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 200

Repository: 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
PY

Repository: 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.

}),
),
frameworks: v.optional(
Expand All @@ -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()),
}),
),
}),
Expand Down
20 changes: 20 additions & 0 deletions tanstack-docs-config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
}
}
}
Expand All @@ -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."
}
}
}
Expand Down
Loading