diff --git a/website/astro.config.mjs b/website/astro.config.mjs
index d237fc1b..8c0368d7 100644
--- a/website/astro.config.mjs
+++ b/website/astro.config.mjs
@@ -1,9 +1,30 @@
import sitemap from "@astrojs/sitemap";
import react from "@astrojs/react";
+import fs from "node:fs";
+import path from "node:path";
+import { fileURLToPath } from "node:url";
import { defineConfig } from "astro/config";
import remarkGithubAdmonitionsToDirectives from "remark-github-admonitions-to-directives";
import pagefindResources from "./src/integrations/pagefind-resources";
+// The contributor count is read from the repo's all-contributors manifest at
+// build time and inlined as a literal into both the server render and the
+// client bundle. It cannot be read at module scope in src/ because the shells
+// that display it are `client:load` hydrated: `node:fs` is unavailable in the
+// browser, so the read would fail there and the badge would reset to 0 on
+// hydration even though the server-rendered HTML had the right number.
+function readContributorsTotal() {
+ try {
+ const here = path.dirname(fileURLToPath(import.meta.url));
+ const rc = JSON.parse(
+ fs.readFileSync(path.resolve(here, "..", ".all-contributorsrc"), "utf8"),
+ );
+ return Array.isArray(rc.contributors) ? rc.contributors.length : 0;
+ } catch {
+ return 0;
+ }
+}
+
// Playbook course content mirrored from external workshop repos is authored in
// GitHub admonition syntax (`> [!NOTE]`). This remark plugin rewrites those
// callouts into directives before rendering, so the same syntax used in the
@@ -70,6 +91,9 @@ export default defineConfig({
},
trailingSlash: "always",
vite: {
+ define: {
+ __CONTRIBUTORS_TOTAL__: JSON.stringify(readContributorsTotal()),
+ },
// @primer/react-brand's default entrypoint is CJS, so Node's ESM loader
// cannot detect its named exports during SSR. The package also ships a
// proper ESM build; alias to it so named imports resolve in both the
diff --git a/website/src/components/brand/CookbookIndex.tsx b/website/src/components/brand/CookbookIndex.tsx
index 7d30b6f8..95de1cb3 100644
--- a/website/src/components/brand/CookbookIndex.tsx
+++ b/website/src/components/brand/CookbookIndex.tsx
@@ -3,6 +3,7 @@ import { Heading, Text } from "@primer/react-brand";
import styles from "./styles/github-copilot-app.module.css";
import { pageHref } from "./pageHref";
import type { SearchItem } from "./searchIndex";
+import { contributorsTotal as siteContributorsTotal } from "../../lib/site-data";
import {
LearningArticleLayout,
type TocSection,
@@ -42,7 +43,7 @@ export type CookbookSection = {
export function CookbookIndex({
sections,
searchIndex = [],
- contributorsTotal = 0,
+ contributorsTotal = siteContributorsTotal,
}: {
sections: CookbookSection[];
searchIndex?: SearchItem[];
diff --git a/website/src/components/brand/Custom.tsx b/website/src/components/brand/Custom.tsx
index 62700bb7..05dd432c 100644
--- a/website/src/components/brand/Custom.tsx
+++ b/website/src/components/brand/Custom.tsx
@@ -29,6 +29,7 @@ import brandDivider from "./brand-divider-copilot-sitting.webp";
import { LargeFooter } from "./LargeFooter";
import { ContributorsHoverCard } from "./ContributorsHoverCard";
import { ContributorsNavButton } from "./ContributorsNavButton";
+import { contributorsTotal as siteContributorsTotal } from "../../lib/site-data";
import { LearningIcon } from "./LearningIcon";
import type { PrototypePageProps } from "./pageHref";
import { getAwesomeCopilotNavLinks } from "./navigation";
@@ -137,7 +138,10 @@ export default function AwesomeCopilot({ pageHref }: PrototypePageProps) {
styles={styles}
inputAriaLabel="Search the library"
/>
-
+
diff --git a/website/src/components/brand/HomePage.tsx b/website/src/components/brand/HomePage.tsx
index 3f13f639..7c93ecdf 100644
--- a/website/src/components/brand/HomePage.tsx
+++ b/website/src/components/brand/HomePage.tsx
@@ -28,6 +28,7 @@ import { LearningIcon } from "./LearningIcon";
import { PageShell } from "./PageShell";
import { pageHref } from "./pageHref";
import type { SearchItem } from "./searchIndex";
+import { contributorsTotal as siteContributorsTotal } from "../../lib/site-data";
const REPO_URL = "https://github.com/github/awesome-copilot";
const CONTRIBUTING_URL =
@@ -123,7 +124,7 @@ const buildResources = (counts: HomePageCounts): Resource[] => [
export function HomePage({
counts,
searchIndex = [],
- contributorsTotal = 0,
+ contributorsTotal = siteContributorsTotal,
}: HomePageProps) {
const resources = buildResources(counts);
const internalHref = ({ page, anchor }: { page?: string; anchor?: string }) =>
diff --git a/website/src/components/brand/LearningArticleLayout.tsx b/website/src/components/brand/LearningArticleLayout.tsx
index 079633d6..d38e5ec4 100644
--- a/website/src/components/brand/LearningArticleLayout.tsx
+++ b/website/src/components/brand/LearningArticleLayout.tsx
@@ -35,6 +35,7 @@ import {
import { TopNavSearch } from "./TopNavSearch";
import { ContributorsNavButton } from "./ContributorsNavButton";
import type { SearchItem } from "./searchIndex";
+import { contributorsTotal as siteContributorsTotal } from "../../lib/site-data";
const CONTRIBUTING_URL =
"https://github.com/github/awesome-copilot/blob/main/CONTRIBUTING.md";
@@ -164,7 +165,7 @@ function LearningArticleLayoutBody({
heroExtra,
tocSections,
searchIndex = [],
- contributorsTotal = 0,
+ contributorsTotal = siteContributorsTotal,
upNext,
children,
}: LearningArticleLayoutProps) {
diff --git a/website/src/components/brand/PlaybookArticleBody.tsx b/website/src/components/brand/PlaybookArticleBody.tsx
index eebe648d..837f67de 100644
--- a/website/src/components/brand/PlaybookArticleBody.tsx
+++ b/website/src/components/brand/PlaybookArticleBody.tsx
@@ -11,6 +11,7 @@ import type {
} from "../../lib/playbook-article";
import { pageHref } from "./pageHref";
import type { SearchItem } from "./searchIndex";
+import { contributorsTotal as siteContributorsTotal } from "../../lib/site-data";
import styles from "./styles/github-copilot-app.module.css";
const CALLOUT_TITLES: Record = {
@@ -60,7 +61,7 @@ export function PlaybookArticleBody({
sections,
tocSections,
searchIndex = [],
- contributorsTotal = 0,
+ contributorsTotal = siteContributorsTotal,
}: {
/** Site path of this article, e.g. `learning-hub/agentic-workflows`. */
slug: string;
diff --git a/website/src/components/brand/PlaybookIndex.tsx b/website/src/components/brand/PlaybookIndex.tsx
index c62b7fe1..ffc7aaee 100644
--- a/website/src/components/brand/PlaybookIndex.tsx
+++ b/website/src/components/brand/PlaybookIndex.tsx
@@ -29,6 +29,7 @@ import { PageShell } from "./PageShell";
import { ScrambleText } from "./ScrambleText";
import { pageHref } from "./pageHref";
import type { SearchItem } from "./searchIndex";
+import { contributorsTotal as siteContributorsTotal } from "../../lib/site-data";
type Topic =
| "Getting started"
@@ -191,7 +192,7 @@ const recommendedCards = [
export function PlaybookIndex({
articles,
searchIndex = [],
- contributorsTotal = 0,
+ contributorsTotal = siteContributorsTotal,
}: {
articles: PlaybookArticle[];
searchIndex?: SearchItem[];
diff --git a/website/src/components/brand/TopNav.tsx b/website/src/components/brand/TopNav.tsx
index 961f7683..19ee08ad 100644
--- a/website/src/components/brand/TopNav.tsx
+++ b/website/src/components/brand/TopNav.tsx
@@ -4,6 +4,7 @@ import { Button } from "@primer/react-brand";
import { useEffect, useRef } from "react";
import type { SearchItem } from "./searchIndex";
+import { contributorsTotal as siteContributorsTotal } from "../../lib/site-data";
import mobileStyles from "./styles/TopNav.module.css";
import { ContributorsNavButton } from "./ContributorsNavButton";
import { LanguageSelect } from "./LanguageSelect";
@@ -26,7 +27,7 @@ export function TopNav({
libraryLabel = "Resources",
playbookLabel = "Playbook",
contributorsHref,
- contributorsTotal = 0,
+ contributorsTotal = siteContributorsTotal,
searchIndex,
contributorsCurrent = false,
searchAriaLabel = "Search the library",
diff --git a/website/src/env.d.ts b/website/src/env.d.ts
index f964fe0c..f992fdef 100644
--- a/website/src/env.d.ts
+++ b/website/src/env.d.ts
@@ -1 +1,6 @@
///
+
+/** Contributor count inlined at build time from the repo's `.all-contributorsrc`
+ * (see the `vite.define` entry in astro.config.mjs). Declared as a global so it
+ * works in both the server render and the hydrated client bundle. */
+declare const __CONTRIBUTORS_TOTAL__: number;
diff --git a/website/src/lib/site-data.ts b/website/src/lib/site-data.ts
index 82347b8a..788d0e53 100644
--- a/website/src/lib/site-data.ts
+++ b/website/src/lib/site-data.ts
@@ -5,9 +5,6 @@
* by the React components is defined in exactly one place and the shell always
* gets consistent counts and a consistent search index.
*/
-import fs from "node:fs";
-import path from "node:path";
-
import agentsData from "../../public/data/agents.json";
import extensionsData from "../../public/data/extensions.json";
import instructionsData from "../../public/data/instructions.json";
@@ -24,15 +21,7 @@ import {
const BASE = import.meta.env.BASE_URL ?? "/";
function getContributorsTotal(): number {
- try {
- const rcPath = path.resolve(process.cwd(), "..", ".all-contributorsrc");
- const rc = JSON.parse(fs.readFileSync(rcPath, "utf8")) as {
- contributors?: Array>;
- };
- return Array.isArray(rc.contributors) ? rc.contributors.length : 0;
- } catch {
- return 0;
- }
+ return typeof __CONTRIBUTORS_TOTAL__ === "number" ? __CONTRIBUTORS_TOTAL__ : 0;
}
export const agents = agentsData.items;