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.