diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-08-03 21:33:33 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-08-03 21:39:25 +0300 |
| commit | 22bb476d646ccadec71f584b90c2a135fec92929 (patch) | |
| tree | c6d4b6dffe0b8891c52e7e0cc10c8e6051734c30 /website/src/site-data.ts | |
| parent | eacea2daf0b72973173b728936b27478374f2cd2 (diff) | |
feat(website): rebuild marketing pages on new brand chassis
Diffstat (limited to 'website/src/site-data.ts')
| -rw-r--r-- | website/src/site-data.ts | 103 |
1 files changed, 103 insertions, 0 deletions
diff --git a/website/src/site-data.ts b/website/src/site-data.ts new file mode 100644 index 00000000..94836fe7 --- /dev/null +++ b/website/src/site-data.ts @@ -0,0 +1,103 @@ +import { readdir, readFile } from 'node:fs/promises'; +import { resolve } from 'node:path'; + +/* The numbers the site's chrome shows, resolved once per build. + Stars, installs and plugin counts come from the same R2 snapshots /stats/ + and /plugins/ read; the version comes from latest.json, which release CI + owns. Nothing here is hand-maintained, so no figure on the site can go + stale independently of a deploy. + + Memoised because SiteLayout renders on every blog post, and a build that + renders many pages should still fetch once. */ + +export type SiteStats = { + starCount?: number; + installCount?: number; + pluginCount?: number; +}; + +async function snapshot(url: string): Promise<any | null> { + try { + const res = await fetch(url); + return res.ok ? await res.json() : null; + } catch { + return null; + } +} + +async function load(): Promise<SiteStats> { + const [stats, plugins] = await Promise.all([ + snapshot('https://assets.herdr.dev/stats/stats.json'), + snapshot('https://assets.herdr.dev/plugins/index.json'), + ]); + + return { + starCount: stats?.stars?.total, + installCount: stats?.downloads + ? (stats.downloads.github || 0) + (stats.downloads.brew || 0) + : undefined, + /* `pluginCount` counts validated manifests, which is what the marketplace + lists. `source.totalCount` is the raw GitHub topic search and still + includes blacklisted repositories and ones with no manifest. */ + pluginCount: plugins?.pluginCount ?? plugins?.plugins?.length, + }; +} + +let statsPromise: Promise<SiteStats> | null = null; + +export function siteStats(): Promise<SiteStats> { + statsPromise ??= load(); + return statsPromise; +} + +/* latest.json carries the full release notes, so read and pick rather than + importing it and bundling all of that into the page. */ +async function readVersion(): Promise<string | null> { + try { + const raw = await readFile(new URL('../latest.json', import.meta.url), 'utf8'); + return JSON.parse(raw).version ?? null; + } catch { + return null; + } +} + +let versionPromise: Promise<string | null> | null = null; + +export function releaseVersion(): Promise<string | null> { + versionPromise ??= readVersion(); + return versionPromise; +} + +/* How many agents Herdr detects out of the box, counted from the bundled + manifests. The site is built from the same repo as the binary, so the number + tracks the code and cannot drift the way a written-down one would — add a + manifest and the homepage says so on the next deploy. */ +async function countAgentManifests(): Promise<number | null> { + /* Resolved from cwd, not import.meta.url: once Vite bundles this module for + the build the module URL no longer points at the source file, so a + relative URL silently found nothing and the line vanished from dist while + still rendering in dev. Both candidates cover running the build from + website/ or from the repo root. */ + for (const rel of ['../src/detect/manifests', 'src/detect/manifests']) { + try { + const files = await readdir(resolve(process.cwd(), rel)); + const count = files.filter((f) => f.endsWith('.toml')).length; + if (count > 0) return count; + } catch { + /* try the next candidate */ + } + } + return null; +} + +let agentCountPromise: Promise<number | null> | null = null; + +export function agentCount(): Promise<number | null> { + agentCountPromise ??= countAgentManifests(); + return agentCountPromise; +} + +/* A fetch that failed at build shows an em dash. A stale hardcoded number + would look authoritative and be wrong, which is worse. */ +export const fmt = (n: unknown): string => + typeof n === 'number' ? n.toLocaleString('en-US') : '—'; |
