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;