diff --git a/.dockerignore b/.dockerignore index 32f19463..17e5e297 100644 --- a/.dockerignore +++ b/.dockerignore @@ -42,6 +42,10 @@ Thumbs.db # Documentation *.md !README.md +# The feature vault is data, not documentation: /roadmap/graph reads these notes +# at runtime, so they have to survive the *.md filter above. +!vault +!vault/** docs # Docker diff --git a/src/lib/vault.ts b/src/lib/vault.ts index 38afb37a..57ed4c9d 100644 --- a/src/lib/vault.ts +++ b/src/lib/vault.ts @@ -174,9 +174,9 @@ function readNotes(dir: string): { title: string; raw: string }[] { /** * Build the graph. Throws when a feature names an area or state that has no note, - * so a typo surfaces at build time instead of silently dropping an edge. + * so a typo surfaces on the first read instead of silently dropping an edge. */ -export function loadVaultGraph(): VaultGraph { +function buildVaultGraph(): VaultGraph { const nodes: VaultNode[] = []; const edges: VaultEdge[] = []; @@ -280,3 +280,18 @@ export function loadVaultGraph(): VaultGraph { return { nodes, edges, counts, generatedFrom: "vault/" }; } + +let cached: VaultGraph | null = null; + +/** + * The graph, parsed once per process. + * + * The vault is immutable inside a deployed container, so re-reading ~66 files on + * every request would be pure waste. In development the cache is skipped, so + * editing a note and refreshing shows the change without a restart. + */ +export function loadVaultGraph(): VaultGraph { + if (process.env.NODE_ENV !== "production") return buildVaultGraph(); + cached ??= buildVaultGraph(); + return cached; +} diff --git a/src/pages/roadmap/graph.tsx b/src/pages/roadmap/graph.tsx index 59a68a10..d1d92bc0 100644 --- a/src/pages/roadmap/graph.tsx +++ b/src/pages/roadmap/graph.tsx @@ -1,20 +1,26 @@ -import type { InferGetStaticPropsType } from "next"; +import type { InferGetServerSidePropsType } from "next"; import { VaultGraphView } from "@/components/pages/homepage/roadmap/graph"; import { loadVaultGraph } from "@/lib/vault"; /** - * The vault is a folder of files that only changes when the repo does, so it is - * read once at build time rather than on every request. This is the one page in - * the app using `getStaticProps` instead of a no-op `getServerSideProps`, and the - * reason is that: no runtime filesystem access, and a malformed note fails the - * build rather than a visitor's request. + * Read the vault per request, like every other page here. + * + * This originally used `getStaticProps` — the vault only changes when the repo + * does, so parsing it once at build time was cheaper. That made Next *prerender* + * the page, which fails: the app is a client-only SPA whose shell calls + * `useRouter` outside a mounted router, so the export step dies with + * "NextRouter was not mounted". It survived a local build and broke the Railway + * one, which is exactly the kind of difference not worth fighting. + * + * The parse is memoised in {@link loadVaultGraph}, so per-request cost is one + * read per process rather than one per visitor. */ -export const getStaticProps = () => ({ props: { graph: loadVaultGraph() } }); +export const getServerSideProps = () => ({ props: { graph: loadVaultGraph() } }); export default function Page({ graph, -}: InferGetStaticPropsType) { +}: InferGetServerSidePropsType) { return (