diff --git a/.env.example b/.env.example index 93fda00..6a87383 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,7 @@ # Node environment (development, production, test) -NODE_ENV=development +# For Next.js apps, we set this in production environments +# `next dev` and `next build` will set this automatically in your development environment +# NODE_ENV=development # NextAuth Configuration # Required for authentication diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4305f04..2b98702 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,3 +19,11 @@ jobs: - run: pnpm install --frozen-lockfile - run: pnpm run type-check - run: pnpm exec biome check --error-on-warnings + - name: Build + run: pnpm run build + env: + # Dummy values for build-time env vars (not needed at build time, only runtime) + NEXTAUTH_URL: http://localhost:3000 + NEXTAUTH_SECRET: build-time-secret + GOOGLE_CLIENT_ID: build-time-client-id + GOOGLE_CLIENT_SECRET: build-time-client-secret diff --git a/src/app/[id]/opengraph-image.tsx b/src/app/[id]/opengraph-image.tsx index f5d1f42..cba7ac5 100644 --- a/src/app/[id]/opengraph-image.tsx +++ b/src/app/[id]/opengraph-image.tsx @@ -37,9 +37,10 @@ async function loadGoogleFont(font: string, weight: number, text: string) { export default async function OpengraphImage({ params, }: { - params: { id: string }; + params: Promise<{ id: string }>; }) { - const bill = await getUnifiedBillById(params.id); + const { id } = await params; + const bill = await getUnifiedBillById(id); const status = (bill?.final_judgment || "abstain").toLowerCase(); const voteText = status === "yes" @@ -47,7 +48,7 @@ export default async function OpengraphImage({ : status === "no" ? "Vote: No" : "Vote: Abstain"; - const textForFont = `${bill?.short_title || bill?.title || params.id} ${voteText} ${PROJECT_NAME} Build Canada Policy Tracker Powered by The Civics Project`; + const textForFont = `${bill?.short_title || bill?.title || id} ${voteText} ${PROJECT_NAME} Build Canada Policy Tracker Powered by The Civics Project`; let interRegular: ArrayBuffer | undefined; let interBold: ArrayBuffer | undefined; try { @@ -69,7 +70,7 @@ export default async function OpengraphImage({ final_judgment: bill?.final_judgment, rationale: bill?.rationale, genres: bill?.genres, - fallbackId: params.id, + fallbackId: id, }} />, { diff --git a/src/app/[id]/page.tsx b/src/app/[id]/page.tsx index 6125c39..cd5772f 100644 --- a/src/app/[id]/page.tsx +++ b/src/app/[id]/page.tsx @@ -26,13 +26,12 @@ import { buildAbsoluteUrl, buildRelativePath } from "@/utils/basePath"; import { BUILD_CANADA_TWITTER_HANDLE, BUILD_CANADA_URL, - PAGE_REVALIDATE_INTERVAL, } from "@/consts/general"; import { BillShare } from "@/components/BillDetail/BillShare"; import { shouldShowDetermination } from "@/utils/should-show-determination/should-show-determination.util"; -// Cache individual bill pages. -export const revalidate = PAGE_REVALIDATE_INTERVAL; +// Next.js requires route segment configs to be literal values (not imported constants) +export const revalidate = 120; // seconds - cache individual bill pages interface Params { params: Promise<{ id: string }>; @@ -170,7 +169,12 @@ export async function generateMetadata( const defaultOgPath = buildRelativePath(id, "opengraph-image"); const defaultOg = `${baseUrl}${defaultOgPath}`; const questionsOgPath = q - ? buildRelativePath(id, "q", encodeURIComponent(q), "opengraph-image") + ? buildRelativePath( + id, + "question", + encodeURIComponent(q), + "opengraph-image", + ) : undefined; const questionsOg = questionsOgPath ? `${baseUrl}${questionsOgPath}` diff --git a/src/app/[id]/questions-opengraph-image.tsx b/src/app/[id]/question/[number]/opengraph-image.tsx similarity index 81% rename from src/app/[id]/questions-opengraph-image.tsx rename to src/app/[id]/question/[number]/opengraph-image.tsx index 62be306..5e03cfb 100644 --- a/src/app/[id]/questions-opengraph-image.tsx +++ b/src/app/[id]/question/[number]/opengraph-image.tsx @@ -16,16 +16,12 @@ export const revalidate = 3600; export default async function QuestionsOpengraphImage({ params, - searchParams, }: { - params: { id: string }; - searchParams: { index?: string }; + params: Promise<{ id: string; number: string }>; }) { - const bill = await getUnifiedBillById(params.id); - const indexParam = - typeof searchParams?.index === "string" - ? parseInt(searchParams.index, 10) - : 1; + const { id, number } = await params; + const bill = await getUnifiedBillById(id); + const indexParam = typeof number === "string" ? parseInt(number, 10) : 1; const index = Number.isFinite(indexParam) && indexParam > 0 ? indexParam : 1; const question = bill?.question_period_questions?.[index - 1]?.question || ""; @@ -39,7 +35,7 @@ export default async function QuestionsOpengraphImage({ isSocialIssue: bill?.isSocialIssue || false, question, index, - fallbackId: params.id, + fallbackId: id, }} />, { diff --git a/src/app/page.tsx b/src/app/page.tsx index b63d06d..0dcc1cc 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -8,11 +8,7 @@ import type { Metadata } from "next"; import { headers } from "next/headers"; import { env } from "@/env"; import { buildRelativePath } from "@/utils/basePath"; -import { - BUILD_CANADA_TWITTER_HANDLE, - PROJECT_NAME, - PAGE_REVALIDATE_INTERVAL, -} from "@/consts/general"; +import { BUILD_CANADA_TWITTER_HANDLE, PROJECT_NAME } from "@/consts/general"; import FAQModalTrigger from "./FAQModalTrigger"; const CANADIAN_PARLIAMENT_NUMBER = 45; @@ -20,7 +16,8 @@ type HomeSearchParams = { cache?: string }; // Force runtime generation (avoid build-time pre-render) and cache in-memory. export const dynamic = "auto"; -export const revalidate = PAGE_REVALIDATE_INTERVAL; +// Next.js requires route segment configs to be literal values (not imported constants) +export const revalidate = 120; // seconds - cache page data export async function generateMetadata(): Promise { const title = "Home"; @@ -207,9 +204,9 @@ async function getMergedBillsCached(): Promise { export default async function Home({ searchParams, }: { - searchParams?: HomeSearchParams | Promise; + searchParams?: Promise; }) { - const resolvedSearchParams = await Promise.resolve(searchParams); + const resolvedSearchParams = searchParams ? await searchParams : undefined; if (resolvedSearchParams?.cache === "clear") { mergedBillsCache = null; // Allow manual cache busting with ?cache=clear diff --git a/src/components/BillDetail/BillQuestions.tsx b/src/components/BillDetail/BillQuestions.tsx index 2f414cf..f0b4d04 100644 --- a/src/components/BillDetail/BillQuestions.tsx +++ b/src/components/BillDetail/BillQuestions.tsx @@ -87,10 +87,12 @@ export const BillQuestions = ({ const rawQuestion = q.question ?? ""; // stripMarkdown already handles trimming, so we can use it for both display and sharing const trimmedQuestion = rawQuestion.trim(); + const questionNumber = idx + 1; + const billUrlWithQuestion = `${billUrl}?q=${questionNumber}`; const shareText = buildXShareText({ title: shareTitle, question: trimmedQuestion, - url: billUrl, + url: billUrlWithQuestion, }); const xShareUrl = `https://x.com/intent/post?${new URLSearchParams({ text: shareText }).toString()}`; diff --git a/src/components/OpenGraph/QuestionOgCard.tsx b/src/components/OpenGraph/QuestionOgCard.tsx index 5faf800..f25e768 100644 --- a/src/components/OpenGraph/QuestionOgCard.tsx +++ b/src/components/OpenGraph/QuestionOgCard.tsx @@ -69,6 +69,7 @@ export function QuestionOgCard({ bill }: { bill: QuestionSubset }) { >
-
+
Question Period
-
{bill.billId}
+
+ {bill.billId} +
diff --git a/src/consts/general.ts b/src/consts/general.ts index a9ae44d..77ed50f 100644 --- a/src/consts/general.ts +++ b/src/consts/general.ts @@ -5,5 +5,7 @@ export const PROJECT_NAME = "Builder MP"; export const GOOGLE_ANALYTICS_ID = "G-VFXPGBE1PR"; export const BUILD_CANADA_TWITTER_HANDLE = "@buildcanada"; -export const PAGE_REVALIDATE_INTERVAL = 120; // All page cache (home, bill details) -export const BILL_API_REVALIDATE_INTERVAL = 600; // Bill API data cache +// Revalidation intervals (in seconds) +// Note: Route segment configs (export const revalidate in page.tsx files) must use literal values +// due to Next.js static analysis requirements. Use these constants only for runtime fetch calls. +export const BILL_API_REVALIDATE_INTERVAL = 600; // Bill API data cache (fetch revalidation)