Personal portfolio developed with Angular 22, deployed at www.gabrielecabrini.it via ng deploy (angular-cli-ghpages).
Fully static site: outputMode: "static" in angular.json prerenders every route at
build time (src/app/app.routes.server.ts), and there is no server running in
production — ng deploy just publishes the generated HTML/JS/CSS.
Everything is data-driven. Page content lives in typed arrays under
src/app/core/data/, shaped by the models in src/app/core/models/; components
mostly just bind that data to a template. To change what a page says, start in
core/data/ — see src/app/core/data/README.md for the content model and the
...Key → i18n-JSON convention that most fields use.
Components are standalone + OnPush + signal-based throughout. State that must
survive SSR/hydration (language, theme) lives in root services under
core/services/. Browser-only work runs inside afterNextRender, which never
executes during prerendering; isPlatformBrowser is only used where a value is
needed synchronously during construction.
Per-page <head> tags go through core/services/page-seo.ts, which re-applies
them on every language switch. Tags that don't vary per route (og:type,
og:image, twitter:card) are static in src/index.html.
- Angular 22 (standalone components, signals, zoneless, SSR + static prerendering)
@ngx-translatefor i18n (it/en), translations bundled frompublic/assets/i18n/html2canvas+jspdffor the CV's client-side PDF export
npm start # dev server
npm run build # production build (prerendered)
npm test # unit tests (vitest, real Chromium)
npm run lint # eslint + angular-eslint (TS and templates)
npm run pretty # format everything in place (run before committing)
npm run pretty:check is the read-only variant. Both skip whatever .gitignore
lists, which Prettier reads by default.
Tests run in a real browser rather than a simulated DOM (browsers: ["chromium"]
on the test target). After a fresh npm ci, download the browser binary once
with npx playwright install chromium.
.github/workflows/deploy.yml runs lint, tests and a build on every push and pull
request; the Pages deploy and the image push only run outside pull requests, and
only if that passes.
The prerendered site is also published as a container image, at ghcr.io/gabrielecabrini/portfolio:
docker run --rm -p 8080:80 ghcr.io/gabrielecabrini/portfolio:latest
Since the image serves exactly what GitHub Pages serves, it exists for self-hosting, not because production depends on it.
They don't overlap, which is why all three are worth running:
- Prettier owns formatting, and nothing else.
- The TypeScript/Angular compiler owns types. It's configured as strictly as the
toolchain allows:
strictTemplatesplusstrictStandaloneandtypeCheckHostBindingson the Angular side,noUncheckedIndexedAccessandexactOptionalPropertyTypeson the TypeScript side, and every Angular extended diagnostic promoted to an error (extendedDiagnostics.defaultCategory).skipLibCheckstays on deliberately: the alternative is type-checking the.d.tsfiles ofjspdf/html2canvas, whose errors can't be fixed here. - ESLint owns what neither of the above can see: type-aware rules such as
no-floating-promises, and linting of Angular templates (accessibility and best practice). It enables no formatting rules, so it never fights Prettier.
The suite is deliberately small and targets rules that are otherwise enforced only by remembering them:
core/data/content-i18n.spec.ts— every...Keyincore/data/and every route title resolves in both translation files, and the two files declare the same keys. A missing translation doesn't crash; it renders the raw key on the page.core/pipes/date-format.pipe.spec.ts— month is 1-based in the data and 0-based inDate, so the January/December boundaries are covered, along with the "Presente"/"Present" fallback for an ongoing role.features/cv/cv.spec.ts— /cv sorts work entries while /experience shows them in declaration order. Asserts the ordering rule and that the source array is not sorted in place.