Overview
The site you are reading. It started as a static page that fetched JSON in the browser and was rebuilt so the server does the work: every page is rendered with Razor Pages from the same data service that backs a small REST API, so crawlers, link previews and visitors without JavaScript get complete HTML. It runs on Azure App Service at faresm.dev over HTTPS, and every push to main goes through an automated build, test and deploy pipeline.
Architecture
Content lives in two JSON files (profile and projects) outside the web root. A singleton data service reads them, caches the parsed result in memory and evicts it when a file changes. Razor Pages render the home page and one detail page per project from that service, while API controllers expose the same data at /api/profile and /api/projects. A media service discovers screenshot folders on disk and reads image dimensions from file headers, so every image ships with explicit width and height. Deployment is a two-job GitHub Actions workflow: the build job builds the app, runs the xUnit test suite and publishes an artifact; the deploy job signs in to Azure with OpenID Connect and deploys that artifact to App Service.
Key Features
- Server-rendered pages: complete HTML for crawlers, link previews and visitors without JavaScript
- REST endpoints (/api/profile, /api/projects, /api/projects/{slug}) backed by the same data service as the pages
- Real 404 responses for unknown routes and project slugs
- Screenshot galleries discovered from the filesystem, with image dimensions read from file headers
- xUnit integration tests (WebApplicationFactory) that run in CI before every deploy
- Deployment authenticated with OIDC federated credentials instead of a stored publish profile or client secret
- Custom domain (faresm.dev) served over HTTPS with HSTS
Technical Challenges
- Moving from client-side to server-side rendering without changing the JSON API or the data format behind it
- Making sure the content files are only reachable through the API and the pages, not downloadable as raw static files
- Deploying to Azure from CI without keeping long-lived credentials in the repository
Engineering Decisions
- Rendered pages on the server with Razor Pages instead of adding a frontend framework, keeping JavaScript for progressive enhancement only
- Kept content in JSON files rather than a database, since it is small, changes rarely and is edited by hand; an in-memory cache with file-change invalidation keeps reads cheap
- Used OIDC federated credentials for the Azure login, so the workflow exchanges a short-lived GitHub token for Azure access instead of storing a deploy password
- Read image dimensions from file headers instead of adding an imaging library dependency
Technologies Used
C# on ASP.NET Core 10 (Razor Pages and API controllers), vanilla JavaScript, HTML and CSS on the front end, xUnit with WebApplicationFactory for tests, GitHub Actions for CI/CD, and Azure App Service for hosting.
Lessons Learned
- When the content already lives on the server, server-side rendering is the simplest fix for SEO and link previews
- Federated identity removes the need to store and rotate a deploy secret in CI/CD
- Integration tests against the real HTTP pipeline catch routing and status-code regressions that unit tests miss