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.
-
Local
Edit and commit
VS Code over Remote-SSH to a Linux host. Changes staged with
git add -pso every hunk gets looked at before it's committed. -
GitHub
Push to
mainThe 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.
-
Actions
Workflow runs
A hosted runner checks out the repo and invokes the
Azure/static-web-apps-deployaction. It authenticates with a deployment token held as a repository secret — scoped to this one Static Web App, not to the subscription. -
Azure
Content uploaded
The action uploads the contents of
app_locationand readsstaticwebapp.config.jsonfor routing, headers and error handling. Nothing is compiled — the files served are the files in the repo. -
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.
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.