How this site is built
Static HTML, no client framework, deployed from a git push. The interesting part is the deploy path and the headers, so that is most of what is below.
Stack
Astro in static output mode. Every page is rendered to HTML at build time and no framework runtime is shipped to the browser. The three scripts on the site are small plain files served from the same origin: one assembles the contact address, one callswindow.print() on the resume page, and one fades sections in on scroll. There is no bundler runtime, no hydration, and no third-party JavaScript.
The build output is served by Cloudflare Workers static assets. wrangler.jsonc points the asset directory atdist and sets not_found_handling to the 404 page, so unknown paths get the site's own 404 rather than a generic error.
Deploys
Cloudflare Workers Builds is connected to the repository, so a push is the whole deploy process. Workers Builds checks the commit out, runs npm run build, and then runs the deploy command. On main that iswrangler deploy, which promotes the new version to production. On any other branch it iswrangler versions upload, which uploads the version and returns a preview URL without touching production, so I can open a branch and look at the rendered result before merging.
The commit SHA in the footer comes from WORKERS_CI_COMMIT_SHA, which Workers Builds injects into the build environment. Locally it falls back to git rev-parse --short HEAD, and if neither is available it rendersdev. Build metadata is never worth failing a build over.
Headers
Security headers live in public/_headers, which Workers static assets applies at the edge. The content security policy is the one below, copied into this page from that file at build time:
default-src 'self'style-src 'self' 'unsafe-inline'img-src 'self' data:script-src 'self'base-uri 'self'form-action 'none'frame-ancestors 'none'
script-src 'self' means no inline scripts and no third-party scripts, so every behaviour on the site has to be a file in the repository. form-action 'none' and frame-ancestors 'none' are there because this site has no forms and should never be framed. Inline styles are allowed because Astro emits a few.
The same file sets X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Permissions-Policy, and gives hashed asset paths under/_astro/ a one year immutable cache header.
Scan the live headers on securityheaders.com ↗
Content and data
src/data/resume.json is the single source for my name, title, summary, stats, experience, skills, and certifications. The homepage hero, the stat row, the certification strip, the resume page, and the printable view all read from it, so there is one place to edit and no copy to keep in sync. The resume page has a print stylesheet, which means the PDF version is the same HTML rendered for paper rather than a separate document that quietly goes stale.
Projects are markdown files in src/content/projects with a typed schema. Setting draft: true keeps a write-up out of the build entirely, which is how unfinished notes stay unpublished.
Contact address
My email address does not appear in the served HTML. The local part and the domain are separate data attributes, and a small same-origin script joins them with an @ when you click the button. It is not a security control, it just keeps the address out of the cheapest scrapers. If you have scripts disabled, the page tells you the local part in plain words instead.