Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 17 additions & 2 deletions src/lib/vault.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[] = [];

Expand Down Expand Up @@ -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;
}
22 changes: 14 additions & 8 deletions src/pages/roadmap/graph.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof getStaticProps>) {
}: InferGetServerSidePropsType<typeof getServerSideProps>) {
return (
<div className="relative z-20 mx-auto w-full min-w-0 max-w-7xl py-10 lg:py-8">
<div className="px-8">
Expand Down
Loading