Landing page bakes the platform stats of the deploy into its static HTML; browsers then swap in the live numbers

Crawlers and LLM fetchers run no JS and saw only the skeleton of the stats
band. app/page.tsx now awaits one read of the public stats blob during
next build (lib/platformStats.ts: manifest, then the versioned file it
names) and renders the numbers into the export. StatsBand starts from that
snapshot and replaces it with the live numbers; a failed live read keeps
the baked numbers instead of hiding the band.

The read is awaited at page level, not under Suspense: React moves a
resolved boundary of a page this size into a hidden div at the end of the
body. It uses no-store (route force-static), so Next's fetch cache cannot
hand a later build the previous numbers. Fail-soft: without a snapshot the
page ships the skeleton and loads live as before, and deploy-web.yml warns.

The browser now revalidates both files (no-cache instead of force-cache):
the backend rewrites the per-day file every 2 hours under immutable, so a
copy cached earlier that day would have made the numbers drop after load.

The page body moved to components/landing/LandingPage.tsx, which the
marketing guards render synchronously; StatsBand left the .json() ratchet
baseline; the sitemap lastmod of / includes the body.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N4ddfosgdeCwXo7NvB9xd2
This commit is contained in:
Claude
2026-10-03 03:27:20 +00:00
parent bca492ba05
commit 762fef4e24
11 changed files with 524 additions and 154 deletions
+5
View File
@@ -108,6 +108,8 @@ jobs:
NEXT_PUBLIC_ENTRA_REDIRECT_URI: ${{ vars.NEXT_PUBLIC_ENTRA_REDIRECT_URI }}
NEXT_PUBLIC_ENTRA_POST_LOGOUT_REDIRECT_URI: ${{ vars.NEXT_PUBLIC_ENTRA_POST_LOGOUT_REDIRECT_URI }}
NEXT_PUBLIC_API_BASE_URL: ${{ vars.NEXT_PUBLIC_API_BASE_URL }}
# Browsers load the live stats through it, and the build reads it once to bake the
# numbers of this moment into the landing page's static HTML (lib/platformStats.ts).
NEXT_PUBLIC_PLATFORM_STATS_MANIFEST_URL: ${{ vars.NEXT_PUBLIC_PLATFORM_STATS_MANIFEST_URL }}
NEXT_PUBLIC_APPINSIGHTS_CONNECTION_STRING: ${{ secrets.NEXT_PUBLIC_APPINSIGHTS_CONNECTION_STRING }}
# Build stamp for public/version.json (scripts/generate-version.js in prebuild);
@@ -121,6 +123,9 @@ jobs:
npm run build
test -f out/version.json || { echo "out/version.json missing - export did not run"; exit 1; }
test -f out/whats-new.json || { echo "out/whats-new.json missing - whats-new prebuild did not run"; exit 1; }
# Fail-soft like the star count: browsers still load the stats live; only crawlers and
# LLM fetchers, which run no JS, see no numbers until the next deploy.
grep -q 'data-stats-snapshot=' out/index.html || echo "::warning::Platform stats were not baked into the landing page (reason in the build log) - crawlers and LLM fetchers see no numbers until the next deploy"
# One archive of the exact bytes that get uploaded, so the attestation has a single subject.
- name: Package export
+13 -37
View File
@@ -1,41 +1,17 @@
import { AuthGate } from "../components/landing/AuthGate";
import { LandingNavbar } from "../components/landing/LandingNavbar";
import { Hero } from "../components/landing/Hero";
import { StatsBand } from "../components/landing/StatsBand";
import { JustAsk } from "../components/landing/JustAsk";
import { Story } from "../components/landing/Story";
import { CapabilitiesStrip } from "../components/landing/CapabilitiesStrip";
import { Comparison } from "../components/landing/Comparison";
import { HowItWorks } from "../components/landing/HowItWorks";
import { Testimonials } from "../components/landing/Testimonials";
import { FinalCta } from "../components/landing/FinalCta";
import { SiteFooter } from "../components/SiteFooter";
import { MarketingTracker } from "../components/MarketingTracker";
import { LandingPage } from "../components/landing/LandingPage";
import { loadPlatformStatsSnapshot } from "../lib/platformStats";
// The stats read below runs once per `next build` and bypasses Next's fetch cache (`no-store`,
// see lib/platformStats.ts). force-static keeps that legal in the static export; the landing
// tree reads no request-time API (cookies, headers, search params) that it would blank out.
export const dynamic = "force-static";
/**
* Landing page v2 — a scroll story of one enrollment.
* All theming goes through the lp-* tokens (globals.css); sections live
* in components/landing/. Auth behavior (AuthGate redirect, LoginButton
* portal handoff) is unchanged from v1.
* The landing page with the platform stats of the build baked into its static HTML, so
* crawlers and LLM fetchers that run no JS see the numbers too; browsers then load the live
* ones (components/landing/StatsBand.tsx). Awaited here, not under a Suspense boundary: React
* would move a resolved boundary of a page this size into a hidden div at the end of the body.
*/
export default function LandingPage() {
return (
<div className="landing-v2 min-h-screen bg-[var(--lp-bg)]">
{/* Client component: handles auth redirect + loading overlay */}
<AuthGate />
<MarketingTracker page="landing">
<LandingNavbar />
<Hero />
<StatsBand />
<JustAsk />
<Story />
<CapabilitiesStrip />
<Comparison />
<HowItWorks />
<Testimonials />
<FinalCta />
<SiteFooter />
</MarketingTracker>
</div>
);
export default async function Page() {
return <LandingPage statsSnapshot={await loadPlatformStatsSnapshot()} />;
}
@@ -0,0 +1,67 @@
import { describe, it, expect, vi } from "vitest";
import { createElement } from "react";
import { renderToStaticMarkup } from "react-dom/server";
import { prerender } from "react-dom/static";
// LandingNavbar and LoginButton read the auth context; the public pages render signed out.
vi.mock("@/contexts/AuthContext", () => ({
useAuth: () => ({ login: async () => undefined, isAuthenticated: false, user: null, getAccessToken: async () => null }),
}));
// AuthGate only redirects signed-in visitors (it needs the app router) and renders no links.
vi.mock("@/components/landing/AuthGate", () => ({ AuthGate: () => null }));
const { SNAPSHOT } = vi.hoisted(() => ({
SNAPSHOT: {
items: [
{ label: "enrollments monitored", value: "12,481" },
{ label: "events processed", value: "8.3M" },
],
asOf: "2026-10-02T14:00:03.1234567Z",
},
}));
// The blob read itself is covered in lib/__tests__/platformStats.test.ts.
vi.mock("@/lib/platformStats", async importOriginal => ({
...(await importOriginal<typeof import("@/lib/platformStats")>()),
loadPlatformStatsSnapshot: async () => SNAPSHOT,
}));
import LandingRoute from "@/app/page";
import { StatsBand } from "@/components/landing/StatsBand";
/**
* Crawlers and LLM fetchers run no JS: the platform stats only reach them when the build renders
* the numbers into the landing page's HTML (lib/platformStats.ts, app/page.tsx).
*/
describe("platform stats in the landing page's static HTML", () => {
it("the band renders the baked numbers and marks the snapshot", () => {
const html = renderToStaticMarkup(createElement(StatsBand, { snapshot: SNAPSHOT }));
expect(html).toContain(">12,481<");
expect(html).toContain(">enrollments monitored<");
expect(html).toContain(">8.3M<");
expect(html).toContain(`data-stats-snapshot="${SNAPSHOT.asOf}"`);
expect(html).not.toContain("animate-pulse");
});
it("without a snapshot the band shows the loading skeleton and no marker", () => {
const html = renderToStaticMarkup(createElement(StatsBand, { snapshot: null }));
expect(html).toContain('data-track-section="stats"');
expect(html).toContain("animate-pulse");
expect(html).not.toContain("data-stats-snapshot");
});
// A Suspense boundary around the read would still resolve at build time, but React moves a
// resolved boundary of a page this size into a hidden div at the end of the body.
it("the page renders the numbers in place, not in a hidden streaming segment", async () => {
const { prelude } = await prerender(createElement(LandingRoute));
const html = await new Response(prelude).text();
const bandStart = html.indexOf('data-track-section="stats"');
const nextSection = html.indexOf('data-track-section="just_ask"');
const figure = html.indexOf(">12,481<");
expect(bandStart).toBeGreaterThan(-1);
expect(figure).toBeGreaterThan(bandStart);
expect(figure).toBeLessThan(nextSection);
expect(html).toContain(`data-stats-snapshot="${SNAPSHOT.asOf}"`);
expect(html).not.toMatch(/<div hidden id="S:/);
});
});
@@ -9,7 +9,7 @@ vi.mock("@/contexts/AuthContext", () => ({
// AuthGate only redirects signed-in visitors (it needs the app router) and renders no links.
vi.mock("@/components/landing/AuthGate", () => ({ AuthGate: () => null }));
import LandingPage from "@/app/page";
import { LandingPage } from "@/components/landing/LandingPage";
import GetStartedPage from "@/app/get-started/page";
import AiPage from "@/app/ai/page";
import AboutPage from "@/app/about/page";
@@ -25,7 +25,8 @@ import TermsPage from "@/app/terms/page";
*/
const PAGES: Record<string, ComponentType> = {
landing: LandingPage,
// app/page.tsx only awaits the build-time stats and renders this body; the sync renderer cannot await.
landing: () => createElement(LandingPage, { statsSnapshot: null }),
"get-started": GetStartedPage,
ai: AiPage,
about: AboutPage,
@@ -11,7 +11,7 @@ vi.mock("@/contexts/AuthContext", () => ({
// AuthGate only redirects signed-in visitors (it needs the app router) and renders no links.
vi.mock("@/components/landing/AuthGate", () => ({ AuthGate: () => null }));
import LandingPage from "@/app/page";
import { LandingPage } from "@/components/landing/LandingPage";
import GetStartedPage from "@/app/get-started/page";
import AiPage from "@/app/ai/page";
import robots from "@/app/robots";
@@ -27,6 +27,8 @@ import { sitePageUrl } from "@/utils/config";
*/
const render = (Page: ComponentType) => renderToStaticMarkup(createElement(Page));
// app/page.tsx only awaits the build-time stats and renders this body; the sync renderer cannot await.
const Landing = () => createElement(LandingPage, { statsSnapshot: null });
const trackIds = (html: string) => [...html.matchAll(/data-track="([^"]+)"/g)].map(m => m[1]);
const sectionIds = (html: string) => [...html.matchAll(/data-track-section="([^"]+)"/g)].map(m => m[1]);
const untracked = (html: string) =>
@@ -37,7 +39,7 @@ const untracked = (html: string) =>
const PAGES: { name: string; Page: ComponentType; sections: string[] }[] = [
{
name: "landing",
Page: LandingPage,
Page: Landing,
sections: ["hero", "stats", "just_ask", "story", "capabilities", "comparison", "how_it_works", "testimonials", "final_cta"],
},
{ name: "get-started", Page: GetStartedPage, sections: ["steps", "signup"] },
@@ -60,7 +62,7 @@ describe("marketing page tracking", () => {
}
it("the landing links to the AI page from the navbar, Then just ask and the footer", () => {
const ids = trackIds(render(LandingPage));
const ids = trackIds(render(Landing));
expect(ids).toEqual(expect.arrayContaining(["nav_ai", "just_ask_ai", "footer_ai"]));
});
@@ -0,0 +1,46 @@
import { AuthGate } from "./AuthGate";
import { LandingNavbar } from "./LandingNavbar";
import { Hero } from "./Hero";
import { StatsBand } from "./StatsBand";
import { JustAsk } from "./JustAsk";
import { Story } from "./Story";
import { CapabilitiesStrip } from "./CapabilitiesStrip";
import { Comparison } from "./Comparison";
import { HowItWorks } from "./HowItWorks";
import { Testimonials } from "./Testimonials";
import { FinalCta } from "./FinalCta";
import { SiteFooter } from "../SiteFooter";
import { MarketingTracker } from "../MarketingTracker";
import type { PlatformStatsSnapshot } from "@/lib/platformStats";
/**
* Landing page v2 — a scroll story of one enrollment.
* All theming goes through the lp-* tokens (globals.css); sections live
* in components/landing/. Auth behavior (AuthGate redirect, LoginButton
* portal handoff) is unchanged from v1.
*
* Synchronous on purpose: app/page.tsx awaits the build-time stats and hands
* them in, and the marketing guard tests render this body with the sync
* renderToStaticMarkup.
*/
export function LandingPage({ statsSnapshot }: { statsSnapshot: PlatformStatsSnapshot | null }) {
return (
<div className="landing-v2 min-h-screen bg-[var(--lp-bg)]">
{/* Client component: handles auth redirect + loading overlay */}
<AuthGate />
<MarketingTracker page="landing">
<LandingNavbar />
<Hero />
<StatsBand snapshot={statsSnapshot} />
<JustAsk />
<Story />
<CapabilitiesStrip />
<Comparison />
<HowItWorks />
<Testimonials />
<FinalCta />
<SiteFooter />
</MarketingTracker>
</div>
);
}
@@ -1,61 +1,12 @@
"use client";
import { useEffect, useState } from "react";
interface PlatformStatsManifest {
latest: string;
generatedAtUtc: string;
}
interface PlatformStatsPayload {
totalEnrollments?: number;
totalUsers?: number;
totalTenants?: number;
totalSignedUpTenants?: number;
uniqueDeviceModels?: number;
totalEventsProcessed?: number;
successfulEnrollments?: number;
issuesDetected?: number;
lastFullCompute?: string;
lastUpdated?: string;
}
function resolvePlatformStatsManifestUrl(rawUrl?: string): string {
const trimmed = rawUrl?.trim();
if (!trimmed) {
return "/platform-stats.json";
}
const hashIndex = trimmed.indexOf("#");
const withoutHash = hashIndex >= 0 ? trimmed.slice(0, hashIndex) : trimmed;
const hash = hashIndex >= 0 ? trimmed.slice(hashIndex) : "";
const queryIndex = withoutHash.indexOf("?");
const basePath = queryIndex >= 0 ? withoutHash.slice(0, queryIndex) : withoutHash;
const query = queryIndex >= 0 ? withoutHash.slice(queryIndex) : "";
if (/\.json$/i.test(basePath)) {
return trimmed;
}
const normalizedBasePath = basePath.endsWith("/") ? basePath.slice(0, -1) : basePath;
const manifestPath = `${normalizedBasePath}/platform-stats.json`;
return `${manifestPath}${query}${hash}`;
}
const PLATFORM_STATS_MANIFEST_URL =
resolvePlatformStatsManifestUrl(process.env.NEXT_PUBLIC_PLATFORM_STATS_MANIFEST_URL);
interface StatItem {
label: string;
value: string;
}
/** 8,341,206 → "8.3M"; smaller values keep their grouped form. */
function compact(n: number): string {
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
return n.toLocaleString("en-US");
}
import {
fetchLivePlatformStats,
toStatItems,
type PlatformStatsSnapshot,
type StatItem,
} from "@/lib/platformStats";
/**
* Sample values for local development only, where no stats blob is
@@ -71,81 +22,51 @@ const DEV_SAMPLE_STATS: StatItem[] = [
];
/**
* Full-bleed platform stats band under the hero shot. Skeleton while
* loading; on fetch failure it shows dev sample data locally and
* disappears entirely in production (never an endless skeleton, never
* fake numbers).
* Full-bleed platform stats band under the hero shot. Starts with the
* numbers the build baked in (`snapshot`, lib/platformStats.ts), so the
* static HTML carries them for crawlers and LLM fetchers, then swaps in the
* live numbers. A failed live read keeps the baked numbers; without either,
* it shows dev sample data locally and disappears entirely in production
* (never an endless skeleton, never fake numbers). The skeleton only shows
* while a page built without a snapshot loads.
*/
export function StatsBand() {
const [stats, setStats] = useState<StatItem[] | null>(null);
export function StatsBand({ snapshot }: { snapshot: PlatformStatsSnapshot | null }) {
const [live, setLive] = useState<StatItem[] | null>(null);
const [failed, setFailed] = useState(false);
useEffect(() => {
let cancelled = false;
const fail = () => {
const loadLiveStats = async () => {
const payload = await fetchLivePlatformStats();
if (cancelled) return;
if (process.env.NODE_ENV === "development") {
setStats(DEV_SAMPLE_STATS);
const items = payload ? toStatItems(payload) : [];
if (items.length > 0) {
setLive(items);
} else {
setFailed(true);
}
};
const loadPlatformStats = async () => {
try {
const manifestResponse = await fetch(PLATFORM_STATS_MANIFEST_URL, { cache: "no-store" });
if (!manifestResponse.ok) return fail();
const manifest = (await manifestResponse.json()) as PlatformStatsManifest;
if (!manifest?.latest) return fail();
const versionedUrl = new URL(manifest.latest, manifestResponse.url).toString();
const statsResponse = await fetch(versionedUrl, { cache: "force-cache" });
if (!statsResponse.ok) return fail();
const payload = (await statsResponse.json()) as PlatformStatsPayload;
if (cancelled) return;
const items: StatItem[] = [];
if (payload.totalEnrollments) {
items.push({ label: "enrollments monitored", value: payload.totalEnrollments.toLocaleString("en-US") });
}
if (payload.issuesDetected) {
items.push({ label: "issues detected", value: payload.issuesDetected.toLocaleString("en-US") });
}
if (payload.totalSignedUpTenants) {
items.push({ label: "organisations", value: payload.totalSignedUpTenants.toLocaleString("en-US") });
}
if (payload.uniqueDeviceModels) {
items.push({ label: "device models", value: payload.uniqueDeviceModels.toLocaleString("en-US") });
}
if (payload.totalEventsProcessed) {
items.push({ label: "events processed", value: compact(payload.totalEventsProcessed) });
}
if (items.length > 0) {
setStats(items);
} else {
fail();
}
} catch {
// The band is decorative and must never break the page.
fail();
}
};
loadPlatformStats();
loadLiveStats();
return () => {
cancelled = true;
};
}, []);
if (failed) {
const devFallback = failed && process.env.NODE_ENV === "development" ? DEV_SAMPLE_STATS : null;
const stats = live ?? snapshot?.items ?? devFallback;
if (!stats && failed) {
return null;
}
return (
<section data-track-section="stats" className="border-y border-[var(--lp-line-soft)] bg-[var(--lp-surface-2)]">
<section
data-track-section="stats"
// Which build snapshot the static HTML carries; deploy-web.yml warns when it is missing.
data-stats-snapshot={snapshot ? (snapshot.asOf ?? "") : undefined}
className="border-y border-[var(--lp-line-soft)] bg-[var(--lp-surface-2)]"
>
<div className="max-w-7xl mx-auto px-6 grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-y-6 py-8 lg:divide-x lg:divide-[var(--lp-line)]">
{(stats ?? Array.from({ length: 5 }, () => null)).map((item, i) =>
item ? (
@@ -6,7 +6,6 @@
/** Files that keep a raw site for a stated reason; they never leave the baseline. */
export const PERMANENT: Record<string, string> = {
"contexts/AuthContext.tsx": "auth/me bootstrap runs before the auth context exists: raw fetch by design",
"components/landing/StatsBand.tsx": "unauthenticated public blob manifest, not the API",
"app/health-check/page.tsx": "/version.json of the SWA itself, not the API",
"app/dashboard/hooks/deleteSessionResponse.ts": "safeJson on the 202 Response of the delete-cascade classifier (refusals arrive as ApiError)",
"app/settings/TenantConfigContext.tsx": "consent return path routes on TokenExpiredError (router.replace); the access check rethrows it as a non-outcome",
@@ -33,7 +32,6 @@ export const DEDUPEDAUTHFETCH_BASELINE: Record<string, number> = {
export const JSONPARSE_BASELINE: Record<string, number> = {
"app/dashboard/hooks/deleteSessionResponse.ts": 1,
"app/health-check/page.tsx": 1,
"components/landing/StatsBand.tsx": 2,
"contexts/AuthContext.tsx": 2,
};
@@ -0,0 +1,188 @@
import { afterEach, beforeEach, describe, expect, it, vi, type MockInstance } from "vitest";
import {
fetchLivePlatformStats,
loadPlatformStatsSnapshot,
resolvePlatformStatsManifestUrl,
toStatItems,
} from "../platformStats";
const CONTAINER = "https://stats.example.test/publicstats";
const MANIFEST_URL = `${CONTAINER}/platform-stats.json`;
const PAYLOAD_URL = `${CONTAINER}/platform-stats.2026-10-02.json`;
/** What the backend publishes (MaintenanceService.TryPublishPlatformStatsJsonAsync). */
const MANIFEST = { latest: "platform-stats.2026-10-02.json", generatedAtUtc: "2026-10-02T14:00:05.1234567Z" };
const PAYLOAD = {
totalEnrollments: 12481,
totalUsers: 310,
totalTenants: 92,
totalSignedUpTenants: 87,
uniqueDeviceModels: 214,
totalEventsProcessed: 8341206,
successfulEnrollments: 11020,
issuesDetected: 1847,
lastFullCompute: "2026-10-02T14:00:03.1234567Z",
lastUpdated: "2026-10-02T14:00:03.1234567Z",
};
const BAND = [
{ label: "enrollments monitored", value: "12,481" },
{ label: "issues detected", value: "1,847" },
{ label: "organisations", value: "87" },
{ label: "device models", value: "214" },
{ label: "events processed", value: "8.3M" },
];
function json(body: unknown, status = 200): Response {
return new Response(JSON.stringify(body), { status, headers: { "Content-Type": "application/json" } });
}
/** Answers by URL; anything not listed is a 404. */
function serve(routes: Record<string, () => Response>) {
const fetchMock = vi.fn<(input: string, init?: RequestInit) => Promise<Response>>(async input =>
routes[input]?.() ?? new Response("not found", { status: 404 }));
vi.stubGlobal("fetch", fetchMock);
return fetchMock;
}
const BOTH_FILES = { [MANIFEST_URL]: () => json(MANIFEST), [PAYLOAD_URL]: () => json(PAYLOAD) };
afterEach(() => {
vi.unstubAllGlobals();
vi.restoreAllMocks();
});
describe("resolvePlatformStatsManifestUrl", () => {
it("falls back to the same-origin manifest without a configured URL", () => {
expect([undefined, "", " "].map(resolvePlatformStatsManifestUrl)).toEqual(Array(3).fill("/platform-stats.json"));
});
it("appends the manifest name to a container URL, with or without a trailing slash", () => {
expect(resolvePlatformStatsManifestUrl(CONTAINER)).toBe(MANIFEST_URL);
expect(resolvePlatformStatsManifestUrl(`${CONTAINER}/`)).toBe(MANIFEST_URL);
});
it("keeps a URL that already names a JSON file", () => {
expect(resolvePlatformStatsManifestUrl(MANIFEST_URL)).toBe(MANIFEST_URL);
expect(resolvePlatformStatsManifestUrl(`${CONTAINER}/custom.JSON`)).toBe(`${CONTAINER}/custom.JSON`);
});
it("keeps query and hash behind the appended manifest name", () => {
expect(resolvePlatformStatsManifestUrl(`${CONTAINER}?v=1#x`)).toBe(`${MANIFEST_URL}?v=1#x`);
});
});
describe("toStatItems", () => {
it("maps the band's figures in display order", () => {
expect(toStatItems(PAYLOAD)).toEqual(BAND);
});
it("leaves out figures that are missing, zero, negative or not a finite number", () => {
const payload = {
totalEnrollments: 0,
issuesDetected: -5,
totalSignedUpTenants: Number.POSITIVE_INFINITY,
uniqueDeviceModels: "214" as unknown as number,
totalEventsProcessed: 999_999,
};
expect(toStatItems(payload)).toEqual([{ label: "events processed", value: "999,999" }]);
expect(toStatItems({})).toEqual([]);
});
});
describe("fetchLivePlatformStats", () => {
it("reads the manifest, then the file it names, revalidating both with the blob", async () => {
const fetchMock = serve(BOTH_FILES);
expect(await fetchLivePlatformStats(MANIFEST_URL)).toEqual(PAYLOAD);
expect(fetchMock.mock.calls).toEqual([
[MANIFEST_URL, { cache: "no-cache" }],
[PAYLOAD_URL, { cache: "no-cache" }],
]);
});
it("resolves the versioned file against the manifest as served, after redirects", async () => {
const redirected = json(MANIFEST);
Object.defineProperty(redirected, "url", { value: "https://stats.example.test/v2/platform-stats.json" });
const fetchMock = serve({
[MANIFEST_URL]: () => redirected,
"https://stats.example.test/v2/platform-stats.2026-10-02.json": () => json(PAYLOAD),
});
expect(await fetchLivePlatformStats(MANIFEST_URL)).toEqual(PAYLOAD);
expect(fetchMock).toHaveBeenCalledTimes(2);
});
const failures: [string, Record<string, () => Response>][] = [
["the manifest is missing", { [PAYLOAD_URL]: () => json(PAYLOAD) }],
["the manifest names no file", { [MANIFEST_URL]: () => json({ generatedAtUtc: MANIFEST.generatedAtUtc }) }],
["the manifest is not JSON", { [MANIFEST_URL]: () => new Response("<html></html>") }],
["the manifest names a file on another origin", { [MANIFEST_URL]: () => json({ latest: "https://elsewhere.example.test/stats.json" }) }],
["the versioned file is missing", { [MANIFEST_URL]: () => json(MANIFEST) }],
["the versioned file holds no object", { [MANIFEST_URL]: () => json(MANIFEST), [PAYLOAD_URL]: () => json(42) }],
];
for (const [name, routes] of failures) {
it(`returns null when ${name}`, async () => {
serve(routes);
expect(await fetchLivePlatformStats(MANIFEST_URL)).toBeNull();
});
}
it("returns null when the network fails", async () => {
vi.stubGlobal("fetch", vi.fn().mockRejectedValue(new TypeError("Failed to fetch")));
expect(await fetchLivePlatformStats(MANIFEST_URL)).toBeNull();
});
});
describe("loadPlatformStatsSnapshot", () => {
let log: MockInstance<typeof console.log>;
let warn: MockInstance<typeof console.warn>;
beforeEach(() => {
log = vi.spyOn(console, "log").mockImplementation(() => undefined);
warn = vi.spyOn(console, "warn").mockImplementation(() => undefined);
});
it("bakes nothing and sends no request without an absolute URL (local and CI builds)", async () => {
const fetchMock = serve(BOTH_FILES);
expect(await loadPlatformStatsSnapshot("/platform-stats.json")).toBeNull();
expect(fetchMock).not.toHaveBeenCalled();
expect(warn).not.toHaveBeenCalled();
});
it("returns the band's items and the stats timestamp, read past Next's fetch cache within a deadline", async () => {
const fetchMock = serve(BOTH_FILES);
expect(await loadPlatformStatsSnapshot(MANIFEST_URL)).toEqual({ items: BAND, asOf: PAYLOAD.lastUpdated });
const inits = fetchMock.mock.calls.map(([, init]) => init);
expect(inits).toHaveLength(2);
for (const init of inits) {
expect(init?.cache).toBe("no-store");
expect(init?.signal).toBeInstanceOf(AbortSignal);
}
expect(inits[0]?.signal).toBe(inits[1]?.signal);
expect(log).toHaveBeenCalledWith(expect.stringContaining(PAYLOAD.lastUpdated));
});
it("fails soft with the reason in the build log when the blob does not answer", async () => {
serve({ [MANIFEST_URL]: () => new Response("unavailable", { status: 503 }) });
expect(await loadPlatformStatsSnapshot(MANIFEST_URL)).toBeNull();
expect(warn).toHaveBeenCalledWith(expect.stringContaining("HTTP 503"));
});
it("names the network cause behind Node's generic fetch failure", async () => {
const failure = new TypeError("fetch failed", { cause: new Error("getaddrinfo ENOTFOUND stats.example.test") });
vi.stubGlobal("fetch", vi.fn().mockRejectedValue(failure));
expect(await loadPlatformStatsSnapshot(MANIFEST_URL)).toBeNull();
expect(warn).toHaveBeenCalledWith(expect.stringContaining("fetch failed (getaddrinfo ENOTFOUND stats.example.test)"));
});
it("bakes nothing when the file holds no positive figure", async () => {
serve({ [MANIFEST_URL]: () => json(MANIFEST), [PAYLOAD_URL]: () => json({ ...PAYLOAD, totalEnrollments: 0, issuesDetected: 0, totalSignedUpTenants: 0, uniqueDeviceModels: 0, totalEventsProcessed: 0 }) });
expect(await loadPlatformStatsSnapshot(MANIFEST_URL)).toBeNull();
expect(warn).toHaveBeenCalledWith(expect.stringContaining("no positive figure"));
});
});
@@ -0,0 +1,166 @@
import type { PlatformStats } from "@/utils/wire-types.generated";
/**
* The landing page's platform stats. A public blob container holds a manifest
* (`platform-stats.json`, rewritten by the backend maintenance run every 2 hours) that names the
* versioned file with the numbers (`latest`, relative to the manifest).
*
* Read twice: once by `next build`, which bakes the numbers of that moment into the static HTML
* (crawlers and LLM fetchers run no JS and would otherwise see only a skeleton), and again by
* every browser, which swaps in the live numbers (components/landing/StatsBand.tsx). Both reads
* share the URL, fetch and formatting rules here.
*/
interface PlatformStatsManifest {
latest: string;
generatedAtUtc: string;
}
/**
* The versioned file. The backend writes it field by field under the names of the wire type
* PlatformStats; every field stays optional because the body is parsed, not trusted.
*/
export type PlatformStatsPayload = Partial<PlatformStats>;
export interface StatItem {
label: string;
value: string;
}
/** The numbers baked into the static HTML at build time. */
export interface PlatformStatsSnapshot {
items: StatItem[];
/** When the backend computed them (the payload's lastUpdated): page marker and build log. */
asOf: string | null;
}
export function resolvePlatformStatsManifestUrl(rawUrl?: string): string {
const trimmed = rawUrl?.trim();
if (!trimmed) {
return "/platform-stats.json";
}
const hashIndex = trimmed.indexOf("#");
const withoutHash = hashIndex >= 0 ? trimmed.slice(0, hashIndex) : trimmed;
const hash = hashIndex >= 0 ? trimmed.slice(hashIndex) : "";
const queryIndex = withoutHash.indexOf("?");
const basePath = queryIndex >= 0 ? withoutHash.slice(0, queryIndex) : withoutHash;
const query = queryIndex >= 0 ? withoutHash.slice(queryIndex) : "";
if (/\.json$/i.test(basePath)) {
return trimmed;
}
const normalizedBasePath = basePath.endsWith("/") ? basePath.slice(0, -1) : basePath;
const manifestPath = `${normalizedBasePath}/platform-stats.json`;
return `${manifestPath}${query}${hash}`;
}
export const PLATFORM_STATS_MANIFEST_URL =
resolvePlatformStatsManifestUrl(process.env.NEXT_PUBLIC_PLATFORM_STATS_MANIFEST_URL);
/** Budget for both build-time requests together: a slow blob must not stall the deploy. */
const BUILD_FETCH_TIMEOUT_MS = 10_000;
/** 8,341,206 → "8.3M"; smaller values keep their grouped form. */
function compact(n: number): string {
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
return n.toLocaleString("en-US");
}
function grouped(n: number): string {
return n.toLocaleString("en-US");
}
/** The band's figures in display order. */
const STAT_FIELDS: { field: keyof PlatformStats; label: string; format: (n: number) => string }[] = [
{ field: "totalEnrollments", label: "enrollments monitored", format: grouped },
{ field: "issuesDetected", label: "issues detected", format: grouped },
{ field: "totalSignedUpTenants", label: "organisations", format: grouped },
{ field: "uniqueDeviceModels", label: "device models", format: grouped },
{ field: "totalEventsProcessed", label: "events processed", format: compact },
];
/**
* The band's items. Only finite positive numbers count: the values end up in the static HTML,
* and a zero means "not computed yet", not a figure to publish.
*/
export function toStatItems(payload: PlatformStatsPayload): StatItem[] {
return STAT_FIELDS.flatMap(({ field, label, format }) => {
const value = payload[field];
return typeof value === "number" && Number.isFinite(value) && value > 0
? [{ label, value: format(value) }]
: [];
});
}
/** Manifest, then the file it names. Throws with the reason; the callers decide what to report. */
async function readPlatformStats(manifestUrl: string, init: RequestInit): Promise<PlatformStatsPayload> {
const manifestResponse = await fetch(manifestUrl, init);
if (!manifestResponse.ok) throw new Error(`${manifestUrl} answered HTTP ${manifestResponse.status}`);
const manifest = (await manifestResponse.json()) as Partial<PlatformStatsManifest> | null;
if (typeof manifest?.latest !== "string" || !manifest.latest) {
throw new Error(`${manifestUrl} names no versioned file`);
}
// Relative to the manifest as served (after redirects); a hand-built Response has no url.
const base = manifestResponse.url || manifestUrl;
const payloadUrl = new URL(manifest.latest, base);
if (payloadUrl.origin !== new URL(base).origin) {
throw new Error(`${manifestUrl} names a file on another origin`);
}
const payloadResponse = await fetch(payloadUrl.toString(), init);
if (!payloadResponse.ok) throw new Error(`${payloadUrl} answered HTTP ${payloadResponse.status}`);
const payload: unknown = await payloadResponse.json();
if (typeof payload !== "object" || payload === null) throw new Error(`${payloadUrl} holds no stats object`);
return payload as PlatformStatsPayload;
}
/**
* The browser's read. `no-cache` revalidates both files with the blob: the versioned file is
* named per day but rewritten every 2 hours under `Cache-Control: immutable`, so a copy cached
* earlier that day would replace the newer baked numbers with older ones.
*/
export async function fetchLivePlatformStats(
manifestUrl: string = PLATFORM_STATS_MANIFEST_URL,
): Promise<PlatformStatsPayload | null> {
try {
return await readPlatformStats(manifestUrl, { cache: "no-cache" });
} catch {
// The band is decorative: a failed read only means no live numbers.
return null;
}
}
/**
* The build's read (app/page.tsx), once per `next build`. Only for an absolute URL: local and CI
* builds have none (the relative fallback exists only in a browser) and bake nothing.
* `no-store` keeps the read out of Next's fetch cache, which would otherwise hand the next build
* on the same machine the previous numbers; the page is `force-static`, so `no-store` stays
* legal in the static export. Fail-soft: without a snapshot the page ships the skeleton and
* browsers load the numbers live, as before; deploy-web.yml reports it as a warning.
*/
export async function loadPlatformStatsSnapshot(
manifestUrl: string = PLATFORM_STATS_MANIFEST_URL,
): Promise<PlatformStatsSnapshot | null> {
if (!/^https?:\/\//i.test(manifestUrl)) return null;
try {
const payload = await readPlatformStats(manifestUrl, {
cache: "no-store",
signal: AbortSignal.timeout(BUILD_FETCH_TIMEOUT_MS),
});
const items = toStatItems(payload);
if (items.length === 0) throw new Error("the stats file holds no positive figure");
const asOf = typeof payload.lastUpdated === "string" ? payload.lastUpdated : null;
console.log(`Platform stats baked into the landing page (as of ${asOf ?? "unknown"}).`);
return { items, asOf };
} catch (err) {
// Node's fetch reports every network failure as "fetch failed" and keeps the reason in `cause`.
const cause = err instanceof Error && err.cause instanceof Error ? ` (${err.cause.message})` : "";
const reason = err instanceof Error ? `${err.message}${cause}` : String(err);
console.warn(`Platform stats not baked into the landing page; browsers still load them live. ${reason}`);
return null;
}
}
@@ -18,7 +18,7 @@ const OUTPUT_FILE = path.join(WEB_ROOT, "utils/page-lastmod.generated.ts");
// fails the build for any URL missing here. Documentation and the changelog
// live on docs.autopilotmonitor.com (GitBook) and are not part of this site.
const PAGE_MAP = {
"/": ["app/page.tsx"],
"/": ["app/page.tsx", "components/landing/LandingPage.tsx"],
"/about": ["app/about/page.tsx", "app/about/faq.ts"],
"/ai": ["app/ai/page.tsx", "components/ai/questions.ts"],
"/get-started": ["app/get-started/page.tsx"],