About this page, specifically
This is a landing page built to double as a CV — a real ASP.NET Core application, deployed the way a real one would be, not a static template. It's also a from-scratch .NET port of the Laravel version of this same site, built to show the same engineering discipline carries across stacks. A few things worth pointing out:
Application
- → The animated chart on the ISG Macedonia entry is a React component, dropped in alongside the ASP.NET Core backend to show some frontend work too.
- → The visit counter below and the app's cache both run on Redis — and degrade gracefully instead of crashing the page if Redis is ever unreachable.
- → Submitting the contact form dispatches a Hangfire background job (Redis-backed) that sends a real email over SMTP — protected by Cloudflare Turnstile so it isn't just a bot magnet — and posts to Slack over a plain Incoming Webhook, so a new message doesn't just sit in an inbox.
- → Page views are tracked server-side, straight to GA4's Measurement Protocol — no analytics JavaScript ships to the browser at all.
-
→
A failed contact-notification job triggers a separate Slack alert once Hangfire's automatic retries are exhausted, via an
IApplyStateFilterreacting to the job's failed-state transition — not a try/catch inside the job itself, which would silently defeat the retries. - → The dark/light toggle in the nav is a real preference, persisted in the browser — light by default regardless of your system setting.
-
→
An xUnit integration test suite — real HTTP requests against real routes via
WebApplicationFactory, covering all of the above — fully isolated (in-memory SQLite, mocked Redis/Hangfire/HTTP), so it needs no real Redis, mail server, or Turnstile account to run.
Infrastructure
- → Fully Dockerized — Kestrel serves the app directly (a single process, no PHP-FPM-style manager needed), plus MySQL, Redis, and LocalStack as separate services, for local dev and production alike.
-
→
The CV download streams the PDF straight from S3 (LocalStack-backed in dev) via the AWS SDK's
IAmazonS3. - → Runs on the same Hetzner VPS as the Laravel version, isolated in its own Docker stack and reached via a Cloudflare Tunnel — no additional inbound port opened on the shared server.
Performance & accessibility
- → Critical CSS is inlined (via Beasties) and everything else deferred, below-the-fold sections use content-visibility: auto so the browser skips laying them out until scrolled into view, and fonts are self-hosted with metric-matched fallbacks (via fontaine) so nothing shifts when they load.
- → The site aims to pass WCAG checks with proper landmarks, a skip-to-content link, and verified color contrast.
- → Structured data (JSON-LD), and Open Graph tags — so search engines and link previews see something more useful than a bare title.
Delivery
-
→
Roslyn analyzers run at a strict, nullable-enabled level (pinned to a specific SDK analysis level for reproducibility) as the PHPStan-L9 equivalent, and
dotnet formatas the Pint-check equivalent, plus a custom check that fails the build over a strayConsole.WriteLineor other leftover debug output. -
→
dotnet list package --vulnerableand npm audit scan every dependency for known vulnerabilities, and gitleaks scans the full git history for accidentally-committed secrets — on every push, not just once. -
→
Before deploying, CI builds the actual production Docker image and boots it standalone — no MySQL, Redis, or S3 attached — then confirms the home and about pages return 200, the hashed CSS asset referenced in the HTML actually resolves, and critical CSS is inlined. Source-level tests can't catch build/packaging bugs like a missing manifest file or an asset silently dropped by
dotnet publish— this step exists because both of those really happened during development. - → GitHub Actions runs all of the above on every push to main, then deploys straight to the server over SSH (using a key scoped only to this app's own directory) only if every check is green — with a Slack notification either way. This page updates itself.
Visit #866, tracked in Redis. Yes, you can refresh.