Next.js
Server-first by default, and one deliberate heresy on this particular site.
Server components are the starting point and a client component is a decision. The question I ask is not “does this need interactivity” but “does this need state that outlives a render” — because a surprising amount of what looks interactive is a link, a form, or a details element that the platform already handles.
This site takes that further than most, and the reason is worth explaining rather than defending.
These pages are ported from a static design set, and each one owns its own CSS and its own script.
A client-side transition keeps the document alive. That is normally the point — but here it means page A’s stylesheet is still attached when page B renders, and page B’s script, which ran once on first load, does not run again. The design set assumes a fresh document per page. So internal links are plain anchors, the router is the browser, and @next/next/no-html-link-for-pages is switched off in eslint.config.mjs with that paragraph written next to it. A disabled rule with no reason is technical debt; a disabled rule with a reason is a decision.
Page scripts mutated the DOM before React hydrated, so React found markup it had not rendered and threw it away. Fixed by moving them to next/script with afterInteractive — the script now runs after hydration rather than racing it.
Whitespace text nodes between <tr> and <td> are relocated by the HTML parser, because a table's content model does not allow them there. React rendered them in one place and found them in another.
A NO_WS_CHILDREN set in scripts/port.mjs strips whitespace-only text nodes inside table, thead, tbody, tfoot, tr, colgroup, select and optgroup as they are emitted.
Fixing the output by hand would have lasted until the next re-port. The bug lived in the generator, so the fix did too.
A disabled lint rule with no reason is technical debt. With a reason, it is a decision.
eslint.config.mjs, on the two rules this repo switches offA design revision re-ports. It is never re-typed.
Twenty-one pages are generated by one script. Nothing in views/ is hand-edited — every file carries a header saying so — because the moment you edit output, the generator stops being the source of truth and the next revision costs a day instead of a command. Fidelity is checked by pixel-diffing every page against the original under forced reduced motion, at three widths, rather than by looking at it.
hallelx2.com
scripts/port.mjs is the interesting file: an htmlparser2 walk with a JSX emitter, including the style-object camel-casing and the whitespace rules above. This page was built by it.
The framework's defaults are good. The reasons to leave them should be written down.
Two lint rules are off in this repo and both have a paragraph next to them. That is the standard — not that the defaults are always right, but that departing from them is argued rather than assumed.