TODO: What I learned building this site

TODO: A short walk through a decision you made and what it cost.

TODO: Replace this file. It is a second example post, here to show what the article index and the "related posts" list look like with more than one entry.

The dependency budget

This site has three runtime dependencies: express, ejs, and marked. Each one has zero transitive dependencies of its own, so npm ls --omit=dev is short enough to read in one screen.

A few things that were deliberately not installed, and what replaced them:

Skipped Replaced by
dotenv ~40 commented lines in src/config/env.ts
helmet ~10 res.setHeader calls in src/middleware/security-headers.ts
zod one hand-written function in src/validation/
express-rate-limit a Map and Date.now()

None of that is a criticism of those packages — they are good, and on a bigger project most of them earn their place. The point is that on a site this size each one would have been a black box where a readable file works just as well.

The bug that only appears after npm run build

views/ and public/ sit at the project root, not inside src/. That means the compiled output in dist/ can reuse them without a copy step, as long as one thing holds: src/config/paths.ts must land at the same depth in both trees, so that path.resolve(__dirname, '..', '..') is the project root either way.

It is easy to break by accident. Put test/ in the build's include list and TypeScript infers a common root of ., emitting dist/src/config/paths.js instead of dist/config/paths.js. Now ROOT points at dist/, and every single page returns a 500 — but only in the built output, never in dev.

Two things guard against it: tsconfig.build.json narrows include to src only, and paths.ts checks that views/ and public/ actually exist when it loads. A wrong path becomes a named error at boot instead of a mystery later.

Progressive enhancement is not nostalgia

The theme toggle and the copy-link button both ship with the hidden attribute, and JavaScript removes it. With scripts blocked you do not get a dead button that does nothing when clicked — you get no button, and the site still works. Dark mode still follows the operating system, and the contact form still submits, because it is a real <form> doing a real POST.

That took no extra effort. It is mostly a matter of deciding which parts are enhancement and which parts are the actual page.