diff --git a/README.md b/README.md
index 3429adb..41879f5 100644
--- a/README.md
+++ b/README.md
@@ -61,6 +61,33 @@ npm run dev
Requires a `.env` file with `DATABASE_URL` (defaults to `file:./dev.db`).
+## Environment variables
+
+Required in production:
+
+| Variable | Purpose | Notes |
+|----------|---------|-------|
+| `NEXT_PUBLIC_SITE_URL` | Canonical/OG/sitemap base URL | e.g. `https://buildcanada.com`. Used by `layout.tsx`, `sitemap.ts`, `robots.ts`, `lib/api/config.ts`, and JSON-LD. |
+| `NEXT_PUBLIC_TRACKER_API_BASE` | Backend host for `/tracker/api/*` rewrites | Set to wherever the Outcomes Tracker API is served. Falls back to `https://www.buildcanada.com`, which will loop after cutover. |
+| `YORK_FACTORY_API_URL` | York Factory CMS base URL | Defaults to `https://yorkfactory.buildcanada.com/api/v1`. Override per environment. |
+| `LUMA_API_KEY` | Luma events list (`/api/events`) | Without this the homepage events list silently returns empty. |
+
+Recommended:
+
+| Variable | Purpose |
+|----------|---------|
+| `NEXT_PUBLIC_POSTHOG_TOKEN` | PostHog analytics + client-side exception capture (`error.tsx` boundaries report here). |
+| `NEXT_PUBLIC_POSTHOG_HOST` | PostHog UI host. Defaults to `https://us.i.posthog.com`. |
+| `NEXT_PUBLIC_GA_MEASUREMENT_ID` | Google Analytics. Loader is conditional — omit to disable. |
+| `TRACKER_API_BASE` | Server-only override for tracker API base (read in `lib/tracker-api.ts` when no public var is set). |
+
+## Deployment notes
+
+- Set the env vars above before building. Several are baked into the static output (`NEXT_PUBLIC_*`), so a redeploy is required to change them.
+- After cutover, verify `/sitemap.xml` and `/robots.txt` reference the production domain.
+- Verify `/tracker` loads — if the API base is misconfigured it will silently fail to render data.
+- Cloudflare-proxied projects (e.g. `/exit-tax-calculator`, `/bills`) are not served by Next; ensure their proxy rules survive any DNS / origin change.
+
## Design
Custom fonts: **Söhne** (headings), **Financier Text** (body), **Founders Grotesk Mono** (labels/buttons).
diff --git a/instrumentation-client.ts b/instrumentation-client.ts
index 043eeef..ee2e7d3 100644
--- a/instrumentation-client.ts
+++ b/instrumentation-client.ts
@@ -5,5 +5,6 @@ if (typeof window !== "undefined" && process.env.NEXT_PUBLIC_POSTHOG_TOKEN) {
api_host: "/ph",
ui_host: process.env.NEXT_PUBLIC_POSTHOG_HOST || "https://us.i.posthog.com",
defaults: "2026-01-30",
+ capture_exceptions: true,
});
}
diff --git a/src/app/about/layout.tsx b/src/app/about/layout.tsx
index 162364d..bdae357 100644
--- a/src/app/about/layout.tsx
+++ b/src/app/about/layout.tsx
@@ -4,6 +4,7 @@ export const metadata: Metadata = {
title: "About",
description:
"Build Canada exists to platform the bold — individuals, ideas, and reforms — that can push our country to new frontiers.",
+ alternates: { canonical: "/about" },
openGraph: {
title: "About",
description:
diff --git a/src/app/builders/[slug]/page.tsx b/src/app/builders/[slug]/page.tsx
index 81626a6..97edea5 100644
--- a/src/app/builders/[slug]/page.tsx
+++ b/src/app/builders/[slug]/page.tsx
@@ -15,6 +15,7 @@ export async function generateMetadata({
return {
title: `${builder.name}: ${builder.tagline}`,
description: builder.quote ?? undefined,
+ alternates: { canonical: `/builders/${slug}` },
openGraph: {
title: `${builder.name}: ${builder.tagline} | Build Canada`,
description: builder.quote ?? undefined,
diff --git a/src/app/builders/opengraph-image.tsx b/src/app/builders/opengraph-image.tsx
new file mode 100644
index 0000000..b6538b7
--- /dev/null
+++ b/src/app/builders/opengraph-image.tsx
@@ -0,0 +1,16 @@
+import { ImageResponse } from "next/og";
+import { BuildCanadaOGImage } from "@/lib/og-image-template";
+
+export const size = { width: 1200, height: 630 };
+export const contentType = "image/png";
+
+export default async function Image() {
+ return new ImageResponse(
+ ,
+ { ...size },
+ );
+}
diff --git a/src/app/builders/page.tsx b/src/app/builders/page.tsx
index 1f9b29d..61b8c53 100644
--- a/src/app/builders/page.tsx
+++ b/src/app/builders/page.tsx
@@ -8,6 +8,7 @@ export const metadata: Metadata = {
title: "Great Canadian Builders",
description:
"Short stories celebrating the incredible builders who shaped Canada.",
+ alternates: { canonical: "/builders" },
};
export default async function BuildersPage() {
diff --git a/src/app/error.tsx b/src/app/error.tsx
new file mode 100644
index 0000000..8bb0e91
--- /dev/null
+++ b/src/app/error.tsx
@@ -0,0 +1,121 @@
+"use client";
+
+import { useEffect } from "react";
+import posthog from "posthog-js";
+import { Button } from "@/components/ui/button";
+
+export default function Error({
+ error,
+ reset,
+}: {
+ error: Error & { digest?: string };
+ reset: () => void;
+}) {
+ useEffect(() => {
+ if (typeof window !== "undefined" && process.env.NEXT_PUBLIC_POSTHOG_TOKEN) {
+ posthog.captureException(error, {
+ digest: error.digest,
+ pathname: window.location.pathname,
+ boundary: "app",
+ });
+ }
+ }, [error]);
+
+ return (
+
+
+
+
+
+ 500
+
+
+
+ Something Broke
+
+
+
+ A beam came loose.
+
+
+
+ Something went wrong on our end. We've been notified and are on
+ it. Try again, or head back to the home page.
+
+
+
+
+
+
+
+
+
+ {error.digest ? `ERR.${error.digest.slice(0, 6).toUpperCase()}` : "ERR.500"}
+
+
+ );
+}
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index a377b05..abb864e 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -15,15 +15,19 @@ export const metadata: Metadata = {
template: "%s | Build Canada",
},
description:
- "Canada's Voice for Builders. Bold thinking from builders, reformers, and leaders pushing Canada to new frontiers.",
+ "Bold thinking from builders, reformers, and leaders pushing Canada to new frontiers.",
metadataBase: new URL(
process.env.NEXT_PUBLIC_SITE_URL || "https://buildcanada.com"
),
+ alternates: {
+ canonical: "/",
+ },
openGraph: {
type: "website",
siteName: "Build Canada",
title: "Build Canada",
- description: "Canada's Voice for Builders.",
+ description:
+ "Bold thinking from builders, reformers, and leaders pushing Canada to new frontiers.",
},
twitter: {
card: "summary_large_image",
diff --git a/src/app/memos/[slug]/page.tsx b/src/app/memos/[slug]/page.tsx
index 7e71e8f..de905a7 100644
--- a/src/app/memos/[slug]/page.tsx
+++ b/src/app/memos/[slug]/page.tsx
@@ -42,6 +42,7 @@ export async function generateMetadata({
return {
title,
description,
+ alternates: { canonical: `/memos/${slug}` },
openGraph: {
title,
description,
diff --git a/src/app/memos/error.tsx b/src/app/memos/error.tsx
new file mode 100644
index 0000000..e284f68
--- /dev/null
+++ b/src/app/memos/error.tsx
@@ -0,0 +1,52 @@
+"use client";
+
+import { useEffect } from "react";
+import posthog from "posthog-js";
+import { Button } from "@/components/ui/button";
+
+export default function MemosError({
+ error,
+ reset,
+}: {
+ error: Error & { digest?: string };
+ reset: () => void;
+}) {
+ useEffect(() => {
+ if (typeof window !== "undefined" && process.env.NEXT_PUBLIC_POSTHOG_TOKEN) {
+ posthog.captureException(error, {
+ digest: error.digest,
+ pathname: window.location.pathname,
+ boundary: "memos",
+ });
+ }
+ }, [error]);
+
+ return (
+
+
+ Memos
+
+
+ We couldn't load this.
+
+
+ Something went wrong while loading the memos. Try again, or browse from
+ the home page.
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/memos/page.tsx b/src/app/memos/page.tsx
index dd17b46..60059f5 100644
--- a/src/app/memos/page.tsx
+++ b/src/app/memos/page.tsx
@@ -11,6 +11,7 @@ export const metadata: Metadata = {
title: "Memos",
description:
"Bold thinking from Canada's builders, reformers, and leaders. Read policy memos and ideas worth building on.",
+ alternates: { canonical: "/memos" },
openGraph: {
title: "Memos",
description:
diff --git a/src/app/opengraph-image.tsx b/src/app/opengraph-image.tsx
index ddd35dc..15d8598 100644
--- a/src/app/opengraph-image.tsx
+++ b/src/app/opengraph-image.tsx
@@ -7,9 +7,8 @@ export const contentType = "image/png";
export default async function Image() {
return new ImageResponse(
,
{ ...size }
);
diff --git a/src/app/page.tsx b/src/app/page.tsx
index c939146..685f50e 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -178,10 +178,12 @@ function SocialLinks() {
/>
))}
-
-
- Full Archive
-
+ {/* /content is being phased out — hide entry point until decision is finalized.
+
+
+ Full Archive
+
+ */}
);
diff --git a/src/app/projects/page.tsx b/src/app/projects/page.tsx
index 6fb30bd..cd86e9a 100644
--- a/src/app/projects/page.tsx
+++ b/src/app/projects/page.tsx
@@ -10,6 +10,7 @@ export const metadata: Metadata = {
title: "Projects",
description:
"Transparent government data and better tools for pro-growth voices.",
+ alternates: { canonical: "/projects" },
openGraph: {
title: "Projects",
description:
diff --git a/src/app/toronto/memos/[slug]/page.tsx b/src/app/toronto/memos/[slug]/page.tsx
index 41b72b2..615c90f 100644
--- a/src/app/toronto/memos/[slug]/page.tsx
+++ b/src/app/toronto/memos/[slug]/page.tsx
@@ -48,6 +48,7 @@ export async function generateMetadata({
return {
title,
description,
+ alternates: { canonical: `${BASE_PATH}/${slug}` },
openGraph: {
title,
description,
diff --git a/src/app/toronto/memos/page.tsx b/src/app/toronto/memos/page.tsx
index 8ff7c9d..5c69f32 100644
--- a/src/app/toronto/memos/page.tsx
+++ b/src/app/toronto/memos/page.tsx
@@ -8,6 +8,7 @@ export const metadata: Metadata = {
title: "Memos",
description:
"Bold thinking for Toronto. Read policy memos and ideas worth building on.",
+ alternates: { canonical: "/toronto/memos" },
openGraph: {
title: "🏗️ Toronto — Memos",
description: "Bold thinking for Toronto.",
diff --git a/src/app/tracker/commitments/[id]/layout.tsx b/src/app/tracker/commitments/[id]/layout.tsx
new file mode 100644
index 0000000..2bc4a48
--- /dev/null
+++ b/src/app/tracker/commitments/[id]/layout.tsx
@@ -0,0 +1,15 @@
+import type { Metadata } from "next";
+
+export const metadata: Metadata = {
+ title: "Commitment - Outcomes Tracker - Build Canada",
+ description:
+ "Detailed status, sources, and assessments for a tracked federal commitment.",
+};
+
+export default function CommitmentDetailLayout({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ return children;
+}
diff --git a/src/app/tracker/commitments/layout.tsx b/src/app/tracker/commitments/layout.tsx
new file mode 100644
index 0000000..1df836b
--- /dev/null
+++ b/src/app/tracker/commitments/layout.tsx
@@ -0,0 +1,16 @@
+import type { Metadata } from "next";
+
+export const metadata: Metadata = {
+ title: "Commitments - Outcomes Tracker - Build Canada",
+ description:
+ "Browse and search every tracked commitment from Canada's federal government, with status and progress updates.",
+ alternates: { canonical: "/tracker/commitments" },
+};
+
+export default function CommitmentsLayout({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ return children;
+}
diff --git a/src/app/tracker/commitments/page.tsx b/src/app/tracker/commitments/page.tsx
index fd85a64..762cc6c 100644
--- a/src/app/tracker/commitments/page.tsx
+++ b/src/app/tracker/commitments/page.tsx
@@ -1,6 +1,6 @@
"use client";
-import { Suspense, useCallback, useRef, useState } from "react";
+import { Suspense, useRef, useState } from "react";
import { useSearchParams } from "next/navigation";
import useSWR from "swr";
import { Search, ChevronUp, ChevronDown } from "lucide-react";
@@ -76,14 +76,14 @@ function CommitmentsPageInner() {
const perPage = 50;
const debounceRef = useRef | null>(null);
- const handleSearchChange = useCallback((value: string) => {
+ function handleSearchChange(value: string) {
setSearch(value);
if (debounceRef.current) clearTimeout(debounceRef.current);
debounceRef.current = setTimeout(() => {
setDebouncedSearch(value);
setPage(1);
}, 300);
- }, []);
+ }
const qs = buildQueryString({
q: debouncedSearch,
diff --git a/src/app/tracker/error.tsx b/src/app/tracker/error.tsx
new file mode 100644
index 0000000..ff22719
--- /dev/null
+++ b/src/app/tracker/error.tsx
@@ -0,0 +1,40 @@
+"use client";
+
+import { useEffect } from "react";
+import posthog from "posthog-js";
+
+export default function TrackerError({
+ error,
+ reset,
+}: {
+ error: Error & { digest?: string };
+ reset: () => void;
+}) {
+ useEffect(() => {
+ if (typeof window !== "undefined" && process.env.NEXT_PUBLIC_POSTHOG_TOKEN) {
+ posthog.captureException(error, {
+ digest: error.digest,
+ pathname: window.location.pathname,
+ boundary: "tracker",
+ });
+ }
+ }, [error]);
+
+ return (
+
+
+ We couldn't load tracker data.
+
+
+ The tracker API didn't respond. This may be a temporary outage —
+ try again in a moment.
+
+
+
+ );
+}
diff --git a/src/app/tracker/faq/page.tsx b/src/app/tracker/faq/page.tsx
index 27c0a17..247adeb 100644
--- a/src/app/tracker/faq/page.tsx
+++ b/src/app/tracker/faq/page.tsx
@@ -4,6 +4,7 @@ export const metadata: Metadata = {
title: "FAQ - Outcomes Tracker - Build Canada",
description:
"Frequently asked questions about the Build Canada Outcomes Tracker.",
+ alternates: { canonical: "/tracker/faq" },
};
const FAQS: { question: string; answer: React.ReactNode }[] = [
diff --git a/src/app/tracker/layout.tsx b/src/app/tracker/layout.tsx
index a8ee3dd..1570237 100644
--- a/src/app/tracker/layout.tsx
+++ b/src/app/tracker/layout.tsx
@@ -8,6 +8,7 @@ const description = "Track the progress of Canada's government initiatives";
export const metadata: Metadata = {
title,
description,
+ alternates: { canonical: "/tracker" },
icons: {
icon: "/tracker/buildcanada-logo-square.svg",
apple: "/tracker/buildcanada-logo-square.svg",
diff --git a/src/app/tracker/ministries/[slug]/page.tsx b/src/app/tracker/ministries/[slug]/page.tsx
index c5cbfe6..19a45c5 100644
--- a/src/app/tracker/ministries/[slug]/page.tsx
+++ b/src/app/tracker/ministries/[slug]/page.tsx
@@ -1,3 +1,4 @@
+import type { Metadata } from "next";
import Link from "next/link";
import BurnUpChartWrapper from "@/components/tracker/BurnUpChartWrapper";
import { fetchApi } from "@/lib/tracker-api";
@@ -7,6 +8,20 @@ import type {
BurnUpResponse,
} from "@/lib/commitment-types";
+export async function generateMetadata({
+ params,
+}: {
+ params: Promise<{ slug: string }>;
+}): Promise {
+ const { slug } = await params;
+ const name = slug.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
+ return {
+ title: `${name} - Outcomes Tracker - Build Canada`,
+ description: `Tracked commitments and progress under ${name}.`,
+ alternates: { canonical: `/tracker/ministries/${slug}` },
+ };
+}
+
const STATUS_LABELS: Record = {
not_started: "Not Started",
in_progress: "In Progress",
diff --git a/src/components/tracker/Sidebar.tsx b/src/components/tracker/Sidebar.tsx
index 46c4194..6f7e15c 100644
--- a/src/components/tracker/Sidebar.tsx
+++ b/src/components/tracker/Sidebar.tsx
@@ -44,13 +44,13 @@ function TrackerSubnav() {
function SidebarLogo() {
return (
-
+
-
+
Outcomes
diff --git a/src/components/tracker/ui/select.tsx b/src/components/tracker/ui/select.tsx
index 0410876..e338be0 100644
--- a/src/components/tracker/ui/select.tsx
+++ b/src/components/tracker/ui/select.tsx
@@ -85,11 +85,15 @@ const SelectContent = React.forwardRef<
>
{children}
diff --git a/src/lib/og-image-template.tsx b/src/lib/og-image-template.tsx
index 13b2a8f..3cf875c 100644
--- a/src/lib/og-image-template.tsx
+++ b/src/lib/og-image-template.tsx
@@ -2,14 +2,14 @@ import { readFile } from "node:fs/promises";
import { join } from "node:path";
const theme = {
- background: "#1a1a1a",
+ background: "#f6ece3",
+ backgroundAlt: "#fbf6f1",
accent: "#932f2f",
- accentAlpha20: "rgba(147, 47, 47, 0.20)",
- accentAlpha40: "rgba(147, 47, 47, 0.40)",
- foreground: "#ffffff",
- foreground80: "rgba(255, 255, 255, 0.80)",
- foreground50: "rgba(255, 255, 255, 0.50)",
- foreground30: "rgba(255, 255, 255, 0.30)",
+ accentSoft: "rgba(147, 47, 47, 0.20)",
+ foreground: "#272727",
+ foregroundMuted: "#5d5d5d",
+ foregroundFaint: "#888888",
+ border: "rgba(39, 39, 39, 0.18)",
} as const;
const sans = "system-ui, -apple-system, sans-serif";
@@ -31,7 +31,7 @@ export function BuildCanadaOGImage({ title, description, badge, label }: OGImage
flexDirection: "column",
backgroundColor: theme.background,
color: theme.foreground,
- padding: "100px 120px",
+ padding: "72px 96px",
position: "relative",
fontFamily: sans,
}}
@@ -47,37 +47,73 @@ export function BuildCanadaOGImage({ title, description, badge, label }: OGImage
}}
/>
-
-
-
- BUILD CANADA
-
- {label && (
- <>
-
-
- {label}
-
- >
- )}
-
+
+
+ BUILD CANADA
+
+ {label && (
+ <>
+
+
+ {label}
+
+ >
+ )}
-
+
-
+
{title}
@@ -86,15 +122,11 @@ export function BuildCanadaOGImage({ title, description, badge, label }: OGImage
{description && (
{description}
@@ -102,13 +134,24 @@ export function BuildCanadaOGImage({ title, description, badge, label }: OGImage
)}
-
- {badge && (
-
+
+ {badge ? (
+
{badge}
+ ) : (
+
)}
-
+
buildcanada.com
diff --git a/src/lib/schemas/entities/organization.ts b/src/lib/schemas/entities/organization.ts
index 4cb14e2..a13197f 100644
--- a/src/lib/schemas/entities/organization.ts
+++ b/src/lib/schemas/entities/organization.ts
@@ -1,6 +1,6 @@
import { Organization, WebSite } from "schema-dts";
-const SITE_URL = "https://buildcanada.com";
+const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL || "https://buildcanada.com";
export function createOrganization(): Organization {
return {