This site

Static HTML and CSS, deployed to Azure Static Web Apps by GitHub Actions on every push to main. No server, no framework, no build step — which turned out to be the one thing the platform didn't expect.

Updated September 2026 · Source on GitHub

Why it exists

A resume has room for one line per project. That line has to describe what the thing is, which leaves no room for why it's built that way or what went wrong while building it — the parts that actually show whether someone can do the work.

So this site is an appendix rather than a second resume. The employment history stays on the PDF. Each page here takes one project and covers the architecture, the decisions I'd have to defend in a review, and the failures that only surfaced once something was running.

The site is also the first project in the list, because the deployment pipeline is a reasonable first piece of cloud work in its own right: source control triggering a build agent, a scoped credential, and a hosted artefact — the same shape as a much larger deployment, small enough to understand end to end.

How a change reaches the internet

Everything below is triggered by one git push. Nothing is deployed by hand, and there's no state anywhere that didn't come from the repository.

  1. Local

    Edit and commit

    VS Code over Remote-SSH to a Linux host. Changes staged with git add -p so every hunk gets looked at before it's committed.

  2. GitHub

    Push to main

    The push is the only trigger. A pull request instead of a push produces a preview environment on its own URL, torn down when the PR closes.

  3. Actions

    Workflow runs

    A hosted runner checks out the repo and invokes the Azure/static-web-apps-deploy action. It authenticates with a deployment token held as a repository secret — scoped to this one Static Web App, not to the subscription.

  4. Azure

    Content uploaded

    The action uploads the contents of app_location and reads staticwebapp.config.json for routing, headers and error handling. Nothing is compiled — the files served are the files in the repo.

  5. Edge

    Served over HTTPS

    Static Web Apps distributes the content and manages the certificate. There is no origin server to patch, restart or pay for while idle.

One push, five stages, no manual step in between.

How the repository is laid out

One stylesheet, one page per project, one folder per page:

.
├── index.html                      landing page and project list
├── styles.css                      the entire stylesheet
├── 404.html
├── staticwebapp.config.json        routing, headers, error handling
├── projects/
│   ├── this-site/
│   │   └── index.html              this page
│   ├── homelab/
│   │   └── index.html
│   └── shortener/
│       └── index.html
└── .github/
    └── workflows/
        └── azure-static-web-apps.yml

index.html is the default document, so projects/homelab/index.html is served at /projects/homelab/. That keeps the URLs clean enough to paste into a cover letter, and gives each project a directory to put diagrams or screenshots in later without a shared assets pile.

Every asset reference is root-relative — /styles.css, not styles.css. A relative path resolves against the current directory, so from /projects/homelab/ it would look for a stylesheet that doesn't exist and the page would render unstyled. This is the kind of thing that works locally and breaks only once deployed.

Decisions and tradeoffs

No framework

This is text and links. A React or Next build would mean shipping a runtime to render paragraphs, plus a dependency tree to keep patched for a site that changes a few times a year. Hand-written HTML costs nothing to maintain and stays readable in the repo. The tradeoff is real — the navigation and the <head> are duplicated across pages, and at around a dozen pages a small static generator like Eleventy starts to earn its place.

No contact form

An email address in the HTML gets scraped; a contact form means an API endpoint, a mail-sending credential, and abuse protection to stop it becoming an open relay. Neither is worth it when the resume already carries my contact details and reaches the employer directly. The site links to GitHub and LinkedIn and nothing else, so there's nothing here to harvest and no endpoint to defend.

A real 404 rather than a fallback

Static Web Apps defaults to rewriting unmatched routes to /index.html, which is correct for a single-page app and wrong here. A stale or mistyped project link would silently return the homepage with a 200, so a broken link would look like a working one. The config overrides it to serve a real 404.html with a 404 status.

Free tier, deliberately

Custom domains and managed certificates are included, and there's no compute to leave running. The limits worth knowing are the storage cap and the lack of a per-app SLA — neither of which matters for a personal site, both of which would for anything real.

What broke: a build step for a site with no build

The first few deployments failed in the Actions log during the build stage, before anything was uploaded.

When Azure creates a Static Web App from a GitHub repository, it writes the workflow file for you and the deploy action runs Oryx, a build-detection tool that inspects the repository and infers the right toolchain. For a Node project it finds package.json and runs an install and build. For plain HTML there is nothing to detect, and rather than skipping the stage it failed on it.

The fix was to tell the action there is no build:

- uses: Azure/static-web-apps-deploy@v1
  with:
    azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN }}
    action: upload
    app_location: "/"
    output_location: ""
    skip_app_build: true

The part worth keeping is how it was found. The failure surfaced as a deployment error, which invites you to start changing deployment settings. Reading the job log instead showed the run never reached the upload step — it stopped inside build detection. That moves the question from "why won't it deploy" to "why is it building at all", and the answer is that a convenience default was doing work this repository didn't ask for.

A pattern worth naming: scaffolding generated on your behalf encodes assumptions about your project. When it fails, the useful question is usually which assumption is wrong, not which setting to change.

Still to do

  • Custom domain, once nameservers move to a provider with a DNS API.
  • A content security policy and the rest of the security headers, set in staticwebapp.config.json.
  • A link checker in the workflow, so a renamed project folder fails the build instead of quietly producing a dead link.