What we write down when we decide something
A decision that only exists in a conversation gets re-argued every few months. Writing it down is cheaper than remembering it.
A short walkthrough of the four files a new page touches here, and the two conventions that keep it from drifting from the rest of the site.
1 min read
Written by
Seratlas Team
Engineering and delivery
1 min read
We build and operate assessment software. We write here about the parts that are harder than they look.
This is the internal version of the instructions, published because it also explains how the site is organised.
Create the strings first, in src/data/, as a single exported object with a
section per block of the page. Nothing user-visible is typed into a component or a
route here — every text slot is a prop fed from a data file, which is what makes a
copy pass possible without touching JSX, and what makes "is this string
hard-coded?" a search rather than a reading exercise.
A directory under src/app/ with a page.tsx that exports metadata and a
component composing sections from src/components/sections/. Set
alternates.canonical to the route's own path; leaving it off lets a stray query
string become a second URL for the same page.
If the route has a [slug], two lines are mandatory rather than advisory:
export const dynamicParams = false;
and a notFound() when the lookup misses. Under a static export there is no
server to render a param that generateStaticParams did not list, so without the
first line the build treats an unlisted slug as something to render on demand,
which is a runtime that does not exist.
Add it to src/lib/nav.ts and, if it belongs there, to the footer columns. Then
add it to src/app/sitemap.ts — that file derives its entries from the same
sources the routes generate from, so a new static page is the one case that needs a
line by hand. A sitemap listing a URL the export never wrote is a 404 handed
straight to a crawler.
npm run verify
Typecheck, lint, tests, build. Then look in out/ for the directory you expected
and open its index.html: rendered section text means the page prerendered, and an
empty shell means something in it only exists after hydration.
No user-visible string literals under src/app/ or src/components/. And
anything that reads from disk lives in a *.server.ts module with
import "server-only" at the top, so importing it from a client component is a
build error instead of an empty module and a page with no cards on it.
A decision that only exists in a conversation gets re-argued every few months. Writing it down is cheaper than remembering it.
What this site is for, what is still missing from it, and why we would rather publish it in that state than wait.
A consent checkbox that starts checked is not consent. A few small decisions separate a form that asks from one that assumes.
A reading-time estimate is a guess presented as a fact. That is fine, as long as the guess is not wrong in a way the reader will notice.
We would rather answer a direct question than write a follow-up post nobody asked for.