riccardo-sapuppo

Decisions and trade-offs

Each entry also states what it costs: a decision with no downside usually is not a decision.

NgModules instead of standalone components

The application stays on NgModules. Converting it would be a wide refactor with no measurable benefit at this size, and the regression risk would be paid by the only thing that matters here — that the site works. Cost: a little more ceremony in app.module.ts.

Bootstrap for CSS only, and only the modules in use

Layout uses Bootstrap's grid and utilities. Its JavaScript is never loaded. Rewriting the grid would have been a redesign dressed up as cleanup.

The whole framework was being pulled in, though, of which the site uses grid, utilities, buttons, forms, nav and badges. Now only the referenced modules are compiled from the SCSS sources — modals, carousel, dropdowns, accordion, offcanvas, tooltips, popovers, tables, list groups, alerts, pagination and spinners are out.

Two figures used to stand here, "242 kB" before and "158 kB" after, with no statement of which build they came from. A number without an artifact and a date is not a measurement: it is a figure that keeps looking current while it ages. The stylesheet's current size is in the .quality/quality-run.json sidecar (bundles field), measured from the files and dated; the "before" value belonged to a build that no longer exists and cannot be reproduced, so it was removed rather than kept for the record. Cost: adding a Bootstrap component means uncommenting its import in src/bootstrap-subset.scss. When a class has no effect, that is almost always why.

Routes loaded on demand

Each page is its own module, loaded by the router when needed. They all used to sit in the initial bundle: opening the home page also downloaded the career timeline, the contact form and the project detail view. Cost: one module per page plus a SharedModule for what is genuinely common. In Angular a directive can be declared in exactly one module, so the shared surface has to be decided rather than left implicit.

Server routes declare the HTTP status

The server runs on AngularNodeAppEngine, the official v22 engine, no longer on the deprecated CommonEngine. The change is not cosmetic: with the old engine the HTTP status could not be derived from the routes, so every response came back 200 — including the "not found" page, unknown project slugs and /404 itself. To a search engine those were soft 404s: addresses that do not exist, indexed as good pages.

core/server-routes.ts now declares, for each shape of address, how to render it and which status to answer with. PrerenderFallback.None does the rest: a slug that was not prerendered — because it does not exist — is not picked up by its route and falls through to the catch-all, which answers 404.

None of the lists are hand-written: slugs and documents come from the same sources that feed the cards, the explorer and the sitemap. Cost: server routes have to mirror the router's, and the builder checks it. A path declared in only one of the two fails the build — which is exactly what you want.

No Monaco Editor

The code view renders data structures regenerated from the same sources as the UI, not hand-written text. Monaco weighs several megabytes and would show code nobody can edit. Cost: no folding, no in-file search.

Hand-rolled i18n dictionary

Two languages, one build, switching at runtime without a reload. @angular/localize produces one bundle per language and needs one URL per language: on a static hosting that is a more complicated deployment for no gain. For the same reason there is no hreflang: there are no per-language URLs to declare. Cost: the translation pipe is impure, so it runs on every change detection cycle. Irrelevant at this size; it would not be on a large app.

The terminal minimises itself

The boot sequence shows itself and then gets out of the way by typing minimize, a command that genuinely exists. Leaving it open forced visitors to close it before they could read anything. Cost: a few seconds of animation a returning visitor has already seen — which is why reduced motion shortens it.

Engineering notes: preview and source

.md files open as a formatted preview — headings, paragraphs, lists, tables — with a button to switch to the Markdown source in the code view; JSON files have the source only. They used to be shown as source only, to avoid a second typography: that held for the engineering notes and not for the quick profile, the page written for whoever is evaluating a candidate, which came out with asterisks and square brackets. Cost: one more Markdown renderer, written in-house (core/doc-blocks.ts), in the documents chunk and not in the initial bundle.

The contact form sends from the domain's hosting

The site is static, but the message no longer goes through the visitor's email app: it goes to a small PHP endpoint on the same hosting (hosting/contact-api/), which delivers it to the site's mailbox with the server's own mail. No third-party service and no reCAPTCHA, which would bring cookies and a third party into a site that has neither: against bots there is a proof-of-work challenge solved by the browser, a honeypot field and a per-address limit, keyed by a hashed fingerprint of the IP, never the IP itself. Cost: a piece of code that lives outside the app, with its own deploy; and when the endpoint does not answer, the form falls back to a mailto with the text already written.

main
decisions.mdUTF-8TypeScript