Skip to content

Commit a4b3fe5

Browse files
committed
Add public repository metrics to website hero
1 parent 8e90a2a commit a4b3fe5

7 files changed

Lines changed: 153 additions & 4 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22

33
All notable changes to StackBrief are documented here.
44

5+
## Unreleased
6+
7+
### Added
8+
9+
- The product website hero now displays public, source-linked GitHub stars and npm downloads since StackBrief launched. Metrics refresh hourly, remain token-free, and disappear individually rather than making the page fail when a provider is unavailable.
10+
511
## 1.1.2 — 2026-07-18
612

713
### Changed

apps/web/README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ The website uses two complementary pieces of product language:
99

1010
The first names the product; the second describes the developer outcome. Do not merge or replace either without a deliberate product decision.
1111

12+
## Public metrics
13+
14+
The hero can show two public, source-linked credibility signals: GitHub stars and npm downloads since StackBrief launched on 2026-07-12. They are fetched server-side and cached for one hour, so the page never exposes a token or depends on client-side requests. If either public API is unavailable or rate-limited, that one metric is omitted; the homepage still renders normally.
15+
16+
The npm label deliberately says **downloads since launch**. npm's download endpoint reports a requested date range, so StackBrief does not present it as an ambiguous all-time total.
17+
1218
## Development
1319

1420
```bash

apps/web/app/globals.css

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/web/app/page.tsx

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,70 @@ import { BrandMark } from "../components/brand-mark";
22
import { CopyButton } from "../components/copy-button";
33
import { FieldMap } from "../components/field-map";
44
import { OriginArtwork } from "../components/origin-artwork";
5+
import {
6+
getRepositoryMetrics,
7+
type RepositoryMetrics,
8+
} from "../lib/repository-metrics";
59

610
const GITHUB = "https://github.com/mojeebdev/stackbrief";
711
const NPM = "https://www.npmjs.com/package/@blindspotlab/stackbrief";
812
const ORIGIN_POST = "https://x.com/MojeebMotion/status/2078447946782163291?s=20";
913
const FOUNDER_X = "https://x.com/MojeebMotion";
1014

15+
// Next requires route-segment values to be statically analyzable literals.
16+
export const revalidate = 3600;
17+
1118
function ExternalArrow() {
1219
return <span aria-hidden="true"></span>;
1320
}
1421

15-
export default function HomePage() {
22+
function formatCount(value: number): string {
23+
return new Intl.NumberFormat("en-US").format(value);
24+
}
25+
26+
function HeroMetrics({ metrics }: { metrics: RepositoryMetrics }) {
27+
const hasMetrics =
28+
metrics.githubStars !== undefined ||
29+
metrics.npmDownloadsSinceLaunch !== undefined;
30+
31+
if (!hasMetrics) return null;
32+
33+
return (
34+
<div className="hero-metrics" aria-label="Public StackBrief repository metrics">
35+
<div className="hero-metric-list">
36+
{metrics.githubStars !== undefined ? (
37+
<a
38+
className="hero-metric"
39+
href={GITHUB}
40+
target="_blank"
41+
rel="noreferrer"
42+
aria-label={`${formatCount(metrics.githubStars)} GitHub stars. View the StackBrief repository.`}
43+
>
44+
<strong>{formatCount(metrics.githubStars)}</strong>
45+
<span>GitHub stars</span>
46+
</a>
47+
) : null}
48+
{metrics.npmDownloadsSinceLaunch !== undefined ? (
49+
<a
50+
className="hero-metric"
51+
href={NPM}
52+
target="_blank"
53+
rel="noreferrer"
54+
aria-label={`${formatCount(metrics.npmDownloadsSinceLaunch)} npm downloads since StackBrief launched. View the package.`}
55+
>
56+
<strong>{formatCount(metrics.npmDownloadsSinceLaunch)}</strong>
57+
<span>npm downloads since launch</span>
58+
</a>
59+
) : null}
60+
</div>
61+
<p>Public repository metrics · refreshed hourly</p>
62+
</div>
63+
);
64+
}
65+
66+
export default async function HomePage() {
67+
const metrics = await getRepositoryMetrics();
68+
1669
return (
1770
<>
1871
<a className="skip-link" href="#main">Skip to content</a>
@@ -38,6 +91,7 @@ export default function HomePage() {
3891
<a className="button button-primary" href="#install">Start with the CLI <span aria-hidden="true"></span></a>
3992
<a className="text-link" href={GITHUB} target="_blank" rel="noreferrer">Read the repository <ExternalArrow /></a>
4093
</div>
94+
<HeroMetrics metrics={metrics} />
4195
<div className="hero-proof" aria-label="StackBrief principles"><span>Offline analysis</span><i /><span>Source-cited</span><i /><span>Agent-neutral</span></div>
4296
</div>
4397
<FieldMap />

apps/web/lib/repository-metrics.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
const GITHUB_REPOSITORY = "mojeebdev/stackbrief";
2+
const NPM_PACKAGE = "@blindspotlab/stackbrief";
3+
const PACKAGE_LAUNCH_DATE = "2026-07-12";
4+
5+
export const REPOSITORY_METRICS_REVALIDATE_SECONDS = 60 * 60;
6+
7+
export interface RepositoryMetrics {
8+
githubStars?: number;
9+
npmDownloadsSinceLaunch?: number;
10+
}
11+
12+
interface GitHubRepositoryResponse {
13+
stargazers_count?: unknown;
14+
}
15+
16+
interface NpmDownloadsResponse {
17+
downloads?: unknown;
18+
}
19+
20+
function asCount(value: unknown): number | undefined {
21+
return typeof value === "number" && Number.isFinite(value) && value >= 0
22+
? Math.floor(value)
23+
: undefined;
24+
}
25+
26+
function currentUtcDate(): string {
27+
return new Date().toISOString().slice(0, 10);
28+
}
29+
30+
async function fetchJson<T>(url: string, headers?: HeadersInit): Promise<T | undefined> {
31+
try {
32+
const response = await fetch(url, {
33+
headers,
34+
next: { revalidate: REPOSITORY_METRICS_REVALIDATE_SECONDS },
35+
});
36+
37+
if (!response.ok) return undefined;
38+
39+
return (await response.json()) as T;
40+
} catch {
41+
return undefined;
42+
}
43+
}
44+
45+
async function fetchGitHubStars(): Promise<number | undefined> {
46+
const repository = await fetchJson<GitHubRepositoryResponse>(
47+
`https://api.github.com/repos/${GITHUB_REPOSITORY}`,
48+
{
49+
Accept: "application/vnd.github+json",
50+
"X-GitHub-Api-Version": "2022-11-28",
51+
},
52+
);
53+
54+
return asCount(repository?.stargazers_count);
55+
}
56+
57+
async function fetchNpmDownloadsSinceLaunch(): Promise<number | undefined> {
58+
const range = `${PACKAGE_LAUNCH_DATE}:${currentUtcDate()}`;
59+
const packageName = encodeURIComponent(NPM_PACKAGE);
60+
const downloads = await fetchJson<NpmDownloadsResponse>(
61+
`https://api.npmjs.org/downloads/point/${range}/${packageName}`,
62+
);
63+
64+
return asCount(downloads?.downloads);
65+
}
66+
67+
/**
68+
* Public signals only. A missing provider result intentionally does not make the
69+
* homepage fail: the available metric can still be rendered on its own.
70+
*/
71+
export async function getRepositoryMetrics(): Promise<RepositoryMetrics> {
72+
const [githubStars, npmDownloadsSinceLaunch] = await Promise.all([
73+
fetchGitHubStars(),
74+
fetchNpmDownloadsSinceLaunch(),
75+
]);
76+
77+
return { githubStars, npmDownloadsSinceLaunch };
78+
}

apps/web/package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/web/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@blindspotlab/stackbrief-web",
3-
"version": "0.1.0",
3+
"version": "0.1.1",
44
"private": true,
55
"scripts": {
66
"dev": "next dev",

0 commit comments

Comments
 (0)