How We Built This Docs Site
A high-level guide for anyone wanting to use this same approach — a Docusaurus v3 site styled with the BC Government design system — for a BC Gov or BC Gov-adjacent documentation project.
Stack
| Layer | Choice |
|---|---|
| Site framework | Docusaurus v3 (TypeScript, classic preset) |
| Font | @bcgov/bc-sans — BC Sans |
| Design tokens | @bcgov/design-tokens — CSS custom properties |
| Component library | @bcgov/design-system-react-components v0.8+ |
| Hosting | Cloudflare Pages — connected directly to the GitHub repo, auto-deploys on every push to main |
| Analytics (optional) | Umami Cloud — free-tier, cookieless, privacy-friendly |
Context and license
This site documents SELES (Spatially Explicit Landscape Event Simulator), landscape simulation software by Andrew Fall / Gowlland Technologies Ltd. It is not an official BC Government site — it uses the BC Government Design System purely as a demonstration of this build approach for other BC Gov and BC Gov-adjacent teams to reuse; the repo and its hosting are independent of government infrastructure.
Framework license: Docusaurus is MIT-licensed, maintained by Meta Open Source. The BC Gov component library and design tokens are Apache-2.0; BC Sans is distributed under the SIL Open Font License — no copyleft dependencies in the stack. Full dependency table in this repo's LICENSE.md.
This site's own content and code license is still a draft stub — pending Andrew Fall's confirmation of a license for the documentation content, and a final decision on the site code's license (MIT proposed, to match Docusaurus and the broader BC Gov open-source convention). See LICENSE.md for current status before reusing anything from this repo.
Steps
1. Scaffold a Docusaurus site
npx create-docusaurus@latest my-docs classic --typescript
cd my-docs
2. Install BC Gov packages
npm install @bcgov/bc-sans @bcgov/design-tokens @bcgov/design-system-react-components
3. Import the font and design tokens
In src/css/custom.css, add at the top:
@import '@bcgov/bc-sans/css/BCSans.css';
@import '@bcgov/design-tokens/css/variables.css';
4. Map Infima CSS variables to BC Gov colours
Docusaurus uses Infima for its design system. Override the primary colour scale in src/css/custom.css to match BC Gov blue (#013366):
:root {
--ifm-color-primary: #013366;
--ifm-color-primary-dark: #012e5c;
/* ... full scale ... */
--ifm-font-family-base: 'BCSans', sans-serif;
--ifm-navbar-background-color: #013366;
--ifm-navbar-link-color: #ffffff;
--ifm-navbar-link-hover-color: #fcba19; /* BC Gov gold */
}
5. Swizzle the Footer
Create src/theme/Footer/index.tsx to replace the default Docusaurus footer with a minimal custom one (or a full BC Gov <Footer> component if hosting under BC Gov).
# Optional — use the docusaurus swizzle CLI to start from the original source:
npx docusaurus swizzle @docusaurus/theme-classic Footer --wrap --typescript
6. Update docusaurus.config.ts
- Set
title,tagline,url,organizationName,projectName - Remove the default
footerblock fromthemeConfig(your swizzle handles it) - Trim the navbar to only your real nav items
7. Customise the homepage
Edit src/pages/index.tsx and src/components/HomepageFeatures/index.tsx with your project's title, description, and feature cards.
Deploying to Cloudflare Pages
This site deploys to Cloudflare Pages, connected directly to the GitHub repo rather than via docusaurus deploy (which targets GitHub Pages specifically):
- In the Cloudflare dashboard: Workers & Pages → Create → Pages → Connect to Git, pick the repo.
- Build command:
npm run build. Build output directory:build. - Add a custom domain under the Pages project's Custom domains tab; DNS for it can live in Cloudflare too if the domain is already on Cloudflare.
- Every push to
maintriggers an automatic build + deploy — no CI config needed in the repo itself.
Gotchas
docusaurus.config.ts'sorganizationName/projectNamefields are GitHub Pages config, not Cloudflare Pages config. They're still worth setting (Docusaurus uses them for edit-URL generation etc.), but don't assume their presence means the site deploys to GitHub Pages — check where the Pages project is actually connected in the Cloudflare/GitHub dashboards.- Never run
npm run deploy/docusaurus deployif the project actually deploys via Cloudflare (or any git-integration-based host). That command is GitHub-Pages-specific — it clones/pushes agh-pagesbranch and will fail loudly (or, on a repo with GitHub Pages actually enabled, publish an unwanted second copy of the site) if that's not really how the site goes live. - Cloudflare Pages builds from a clean git checkout, so a local, git-ignored
.envfile never reaches the production build. Any env vars the build needs (API keys, analytics IDs) must also be added under the Pages project's Settings → Environment variables, then a deployment retriggered.
Notes on the BC Gov component library
- The
@bcgov/design-system-react-componentslibrary ships no CSS bundle — styles are driven entirely by the CSS custom properties from@bcgov/design-tokens. Import the tokens before using any components. - The library is React-only (built on React Aria). No Vue/Svelte/web-components equivalent exists as of v0.8.0.
- Available navigation components:
Header,Footer,Subheader,Menu. These are ideal if your site is actually hosted by BC Gov and needs the provincial wordmark and territory acknowledgement.
Adding the provincial Header and Footer (BC Gov hosting)
When the site is hosted under a BC Gov domain, add the provincial identity bar:
npx docusaurus swizzle @docusaurus/theme-classic Layout --wrap --typescript
Then in src/theme/Layout/index.tsx:
import {Header, Footer} from '@bcgov/design-system-react-components';
export default function Layout({children, ...props}) {
return (
<>
<Header title="Your Site Name" />
<OriginalLayout {...props}>{children}</OriginalLayout>
</>
);
}
And replace the Footer swizzle with the BC Gov <Footer> component (which includes the land acknowledgement by default).
Adding multiple independent doc sections (e.g. Case Studies, Model Garden)
If you want more than one top-level nav section with its own sidebar — not nested under your main Docs sidebar — add extra instances of @docusaurus/plugin-content-docs in docusaurus.config.ts:
plugins: [
[
'@docusaurus/plugin-content-docs',
{
id: 'case-studies',
path: 'case-studies',
routeBasePath: 'case-studies',
sidebarPath: './sidebars-case-studies.ts',
},
],
],
Then reference it in the navbar with type: 'docSidebar', docsPluginId: 'case-studies', and a matching sidebarId.
Gotchas with this pattern
- Give each section's root
index.mdan explicitsidebar_position: 0. Docusaurus's autogenerated sidebar sorts items with an explicit position before items without one. If your section's landing page has no position but its category folders do (position: 1, 2, 3…), the navbar link resolves to the first category instead of the root page. - Breadcrumbs won't show the section root by default. Each plugin instance's own landing page isn't a node in its own sidebar tree, so Docusaurus's
useSidebarBreadcrumbs()has no way to know it exists — a deep page rendersHome > Conceptual Framework > ...instead ofHome > Docs > Conceptual Framework > .... Fixing this means swizzlingDocBreadcrumbsto prepend a section-root crumb keyed offuseActivePlugin().pluginId(seesrc/theme/DocBreadcrumbs/index.tsxin this repo for a working example) — and skip that injected crumb when it would duplicate the page's own (only) breadcrumb entry on the root page itself. - When swizzling a file that's a copy of Docusaurus's own source (as opposed to a fresh wrapper), keep its original MIT copyright header — it's a modified copy of licensed code, not a from-scratch file.
Adding local search
@easyops-cn/docusaurus-search-local is a good fit if you want search with no third-party service, no signup, and no API keys — unlike Algolia DocSearch, it builds its entire index locally at docusaurus build time and ships it as a static JSON file.
themes: [
[
'@easyops-cn/docusaurus-search-local',
{
hashed: true,
indexDocs: true,
indexPages: true,
docsRouteBasePath: ['docs', 'case-studies', 'model-garden'], // one per docs plugin instance
},
],
],
Gotchas
- The index only exists in production builds. In
npm start(dev mode) it shows a placeholder message instead of searching. Test withnpm run build && npm run serve. - If you reuse
@theme/SearchBaroutside the navbar (e.g. a big homepage search box), be aware that the underlyingautocomplete.jslibrary wraps the raw<input>in a new<span>the first time it's focused, to host the results dropdown. That wrapper has no width of its own — if your CSS gives the inputwidth: 100%, it can suddenly resolve against an unconstrained wrapper and collapse to the browser's default text-input size on focus. Fix by forcing that wrapper span (and only that span — not its siblings, which include an absolutely-positioned clear button and keyboard-shortcut hint that break if you stretch them too) towidth: 100%; display: block;. - Infima's built-in search icon uses
fill="currentColor"inside an inlined SVGbackground-image.currentColordoesn't inherit through abackground-imagethe way it does for a real inline<svg>element, so the icon renders black regardless of theme — invisible against a dark input in dark mode. If you restyle the search input, you'll likely need to supply your own icon SVG (with a hardcoded fill) for dark mode.
Adding privacy-friendly analytics (Umami)
Umami Cloud has a free tier, is cookieless, and collects no personal data — a good fit for a static docs site with no backend to proxy a heavier analytics stack through.
-
Add the site in the Umami dashboard and note its Website ID. Optionally enable that website's Share URL (a public, read-only dashboard link) if you want a report view embeddable elsewhere.
-
Load both as env vars via
dotenvindocusaurus.config.ts, and inject the website ID as a site-wide<script>tag using Docusaurus's built-inscriptsconfig:import 'dotenv/config';// ...customFields: {umamiShareUrl: process.env.UMAMI_SHARE_URL,},scripts: process.env.UMAMI_WEBSITE_ID? [{src: 'https://cloud.umami.is/script.js', 'data-website-id': process.env.UMAMI_WEBSITE_ID, defer: true}]: [],Gating on the env var being set means a build with no
.env(e.g. a contributor's local build) just silently skips the script instead of injecting a broken tag. -
To view stats inside the site itself, rather than only in Umami's own dashboard: make an unlisted page (e.g.
src/pages/admin/stats.tsx) that readsumamiShareUrlback out viauseDocusaurusContext().siteConfig.customFieldsand renders it in an<iframe>. Add<meta name="robots" content="noindex, nofollow" />via@docusaurus/Head, and astatic/robots.txtwithDisallow: /admin/— this keeps the page out of search engines and the sitemap, but it's not real access control (no login, and the page's HTML source reveals the Share URL to anyone who finds it). Use Cloudflare Access in front of the path if the stats need an actual login wall. -
An informational consent toast (no cookies/personal data means no legal consent gate is required, just disclosure) can be added globally via
src/theme/Root.tsx— Docusaurus's supported "wrap the whole app" extension point, which doesn't require swizzling any core theme component. Persist the dismissal inlocalStorageso it only shows once per browser.
Gotchas
- See the Cloudflare Pages section above — the env vars this feature needs (
UMAMI_WEBSITE_ID,UMAMI_SHARE_URL) must be set in the Pages project's environment variables, not just a local.env, or production builds silently ship with analytics/reporting disabled.
BC Gov navbar colour is theme-independent — plan for it
If (like this site) you hardcode the navbar to BC Gov dark blue regardless of light/dark mode, remember that anything relying on color: inherit (e.g. Infima's .clean-btn, used by the light/dark mode toggle and the mobile nav hamburger) will inherit the page's default text color, not the navbar's white link color. That's invisible in light mode against a dark navbar. Force it explicitly:
.navbar .clean-btn {
color: #ffffff;
}