diff --git a/web/app/docs/layout.tsx b/web/app/docs/layout.tsx index 68f71ff..1a1c483 100644 --- a/web/app/docs/layout.tsx +++ b/web/app/docs/layout.tsx @@ -10,6 +10,12 @@ import styles from '../../components/docs/docs.module.css'; import { getSearchIndex } from '../../lib/docs'; import { productBasePath, productSections, getProductSearchIndex } from '../../lib/product-docs'; +// Docs are deployed as pre-rendered assets. The Cloudflare incremental cache +// configured for this site is intentionally read-only, so allowing Next's +// default hourly revalidation would eventually send a stale docs request down +// a regeneration path that cannot be persisted. +export const revalidate = false; + const searchIndex = getSearchIndex(); const productScopes = productSections.map((section) => ({ id: section.id, diff --git a/web/components/DocsGitHubStarsBadge.tsx b/web/components/DocsGitHubStarsBadge.tsx index 7c5153e..4cdee25 100644 --- a/web/components/DocsGitHubStarsBadge.tsx +++ b/web/components/DocsGitHubStarsBadge.tsx @@ -1,5 +1,6 @@ 'use client'; +import { useEffect, useState } from 'react'; import { usePathname } from 'next/navigation'; import { getProductSectionForPath } from '../lib/product-docs-nav'; @@ -8,11 +9,43 @@ import s from './github-stars.module.css'; export type DocsStarRepo = { /** Product section id (`file`, `loop`) or `null` for the default Agent Relay repo. */ id: string | null; + repo: string; href: string; label: string; - count: string | null; }; +type StarCacheEntry = { count: string; expiresAt: number }; + +const STAR_CACHE_PREFIX = 'agentrelay:github-stars:'; +const STAR_CACHE_TTL_MS = 60 * 60 * 1000; + +function formatStarCount(stars: number): string { + return stars >= 1000 ? `${(stars / 1000).toFixed(1)}k` : String(stars); +} + +function getCachedStarCount(repo: string): string | null { + try { + const cached = localStorage.getItem(`${STAR_CACHE_PREFIX}${repo}`); + if (!cached) return null; + + const entry = JSON.parse(cached) as StarCacheEntry; + return entry.expiresAt > Date.now() ? entry.count : null; + } catch { + return null; + } +} + +function cacheStarCount(repo: string, count: string) { + try { + localStorage.setItem( + `${STAR_CACHE_PREFIX}${repo}`, + JSON.stringify({ count, expiresAt: Date.now() + STAR_CACHE_TTL_MS } satisfies StarCacheEntry) + ); + } catch { + // Storage can be unavailable in private browsing; the badge still works. + } +} + function GithubIcon() { return ( - {entry.count} + {count} ) : null} diff --git a/web/components/GitHubStars.tsx b/web/components/GitHubStars.tsx index cb734ee..961004a 100644 --- a/web/components/GitHubStars.tsx +++ b/web/components/GitHubStars.tsx @@ -41,8 +41,8 @@ async function getGitHubStars(repo: string = DEFAULT_REPO): Promise getGitHubStars(t.repo))); - const repos: DocsStarRepo[] = targets.map((t, i) => ({ + const repos: DocsStarRepo[] = targets.map((t) => ({ id: t.id, + repo: t.repo, href: `https://github.com/${t.repo}`, label: t.label, - count: counts[i], })); return ;