How billybunn.io is built

How this site is built and run: infrastructure as code, automated deploys, performance budgets, and security headers.

Role
Solo — infrastructure, CI/CD, frontend, content, deploy
Scope
Personal project, solo-operated
Stack
11ty, CloudFormation, S3, CloudFront, GitHub Actions

TL;DR

  • What it is: A personal site built and run like a real system — infrastructure as code, automated deploys, security headers, performance budgets.
  • My role: Solo. I own infrastructure, CI/CD, DNS, TLS, font loading, CSS pipeline, content, and deploy.
  • Constraints: One person, no budget, no backend. Everything runs on static files and managed AWS services.
  • Results:
    • ~2 minutes from push to live (GitHub Actions → S3 → CloudFront invalidation)
    • Zero infrastructure drift — all resources defined in CloudFormation
    • Page weight target: < 50 KB (not yet formally measured; Lighthouse CI is planned)

Context

Most personal sites start from a template and stay there. I wanted to treat mine as a real system — one where I make deliberate infrastructure, performance, and security decisions and document why.

This site is both a portfolio and a working example of how I think about building for the web: constraints first, then tradeoffs, then implementation.

Goals

  • Ship a multi-page static site with clean URLs, custom typography, and a warm visual identity.
  • Define all infrastructure in code. No manual console changes.
  • Automate deploys end-to-end: push to master → build → deploy → invalidate cache.
  • Treat performance and accessibility as constraints, not afterthoughts.
  • Keep the system simple enough for one person to operate and extend.

Constraints

  • One person. No team, no code review, no on-call rotation.
  • No budget. AWS free tier plus minimal S3/CloudFront costs. No paid CI, no paid monitoring.
  • No backend. Static files only. No application server, no database, no client-side framework.
  • Custom typography. The site uses Figure from Fort Foundry, which means managing font loading performance rather than falling back to system fonts.

Approach

Architecture

The site is a static Eleventy build deployed to AWS.

GitHubpush to masterGitHub Actionsbuild & deployS3static filesCloudFrontCDN + headersRoute 53DNSACMTLS certificateUserCloudFormationIaC

How it works:

  1. Push to master triggers GitHub Actions
  2. Actions builds the 11ty site and deploys the CloudFormation stack
  3. Built files sync to S3 with per-file-type cache headers
  4. CloudFront serves content over HTTP/2+3 with security headers
  5. A CloudFront Function rewrites clean URLs (/about/about/index.html)
  6. CloudFront cache is invalidated on every deploy

All infrastructure is defined in a single template.yaml — S3 buckets, CloudFront distributions, Route 53 records, ACM certificate, security headers policy, and the URL rewrite function. No manual console changes.

Performance

  • Minimal client-side JS. The only JavaScript is a ~30-line FOFT font loader with sessionStorage caching. Repeat visits skip straight to the full font.
  • CSS is inlined. All styles are inlined in <style> tags — no render-blocking external stylesheets. PurgeCSS removes unused rules per page at build time.
  • Font loading: FOFT (FOUT with Two Stage Render). A ~5 KB subset font (ASCII only) is preloaded and renders immediately. The full family (Regular, Bold, Italic, Bold Italic) loads in the background. Faux-bold bridges the gap.
  • Fluid type and spacing. Utopia clamp() values handle responsive sizing without breakpoints or JS.
  • HTTP/2+3. CloudFront is configured for http2and3, enabling QUIC for supporting clients.
  • Cache strategy:
    • HTML: Cache-Control: no-cache — browser always revalidates, gets 304 if unchanged
    • Fonts: Cache-Control: public, max-age=31536000, immutable — cached for 1 year at both edge and browser
  • CloudFront invalidation is scoped. Only HTML and robots.txt are invalidated on deploy. Fonts stay cached at edge.

Accessibility

  • Semantic HTML throughout. Headings follow a strict hierarchy. Landmarks (<main>, <footer>, <article>, <nav>) are used correctly.
  • Visible focus styles. All interactive elements have a 2px solid outline on :focus-visible, with outline-offset for breathing room.
  • Reduced motion. Link hover transitions are wrapped in prefers-reduced-motion: no-preference. No animations play for users who prefer reduced motion.
  • Native semantics over ARIA. The markup is simple enough that native elements handle accessibility. The architecture SVG diagram uses role="img" with a descriptive aria-label.
  • Color contrast. The warm-on-cool palette (cream text on dark green-tinted background) was chosen for comfortable reading. Heading and body text colors were tested for WCAG AA contrast ratios.

Security

  • HTTPS everywhere. Viewer protocol policy is redirect-to-https. HSTS is set with max-age=63072000 (2 years), includeSubdomains, and preload.
  • Security headers (via CloudFront Response Headers Policy):
    • X-Content-Type-Options: nosniff
    • X-Frame-Options: DENY
    • X-XSS-Protection: 1; mode=block
    • Referrer-Policy: strict-origin-when-cross-origin
  • S3 is private. The bucket blocks all public access. CloudFront accesses it via Origin Access Identity (OAI). No direct S3 URLs are exposed.
  • TLS 1.2 minimum. Certificate enforces TLSv1.2_2021.
  • No tracking, no analytics, no cookies. The site collects nothing. sessionStorage is used only for font-loading optimization and is never sent to a server.

Tradeoffs and decisions

Every system has tradeoffs. Here are the ones I made and why.

  • Static site over SPA. No client-side routing, no hydration, no bundle. The tradeoff: no dynamic content without a rebuild. For a portfolio site, that's the right call.
  • CloudFormation over CDK. CDK adds a build step, a dependency on aws-cdk-lib, and synthesized templates that are hard to read. Plain CloudFormation is verbose but predictable. For a single stack, the overhead of CDK isn't justified. (The old CDK stack is archived for reference.)
  • OAI over OAC. The S3 origin still uses the legacy Origin Access Identity instead of the newer Origin Access Control. OAC is the recommended path forward but requires a migration. This is deferred work — it functions correctly but isn't the modern approach.
  • Inline CSS over external stylesheet. Inlining eliminates a render-blocking request but means CSS can't be cached separately. For a site this small, the tradeoff favors inlining. PurgeCSS keeps the inline payload tight.
  • Custom font over system fonts. System fonts are faster (zero network cost), but Figure is part of the site's identity. FOFT loading minimizes the performance penalty — the subset is ~5 KB and preloaded.
  • No CSP yet. A Content Security Policy would tighten the security posture, but it requires auditing the inline font-loading script and styles. It's next on the roadmap.

Outcomes

  • Deploy pipeline works end-to-end. Push to master builds, deploys infrastructure, syncs files, sets cache headers, and invalidates the CDN — no manual steps.
  • Zero infrastructure drift. Every AWS resource is defined in template.yaml. Nothing was created in the console.
  • Security headers are in place. HSTS, X-Frame-Options, nosniff, XSS protection, and referrer policy are all set via CloudFront response headers policy.
  • Font loading is optimized. The FOFT strategy renders a ~5 KB subset immediately and loads the full family in the background. Repeat visitors get the full font from sessionStorage with no network cost.

I haven't yet set up formal performance measurement (Lighthouse CI is planned). I'd want to track first contentful paint, total page weight, and cumulative layout shift over time.

What I'd change next

  • Add Lighthouse CI to the pipeline. Automated performance and accessibility regression testing on every push. This would give me the FCP and page weight numbers the TL;DR is missing.
  • Add a Content Security Policy. Nonce-based CSP for the inline font-loading script, style-src 'self' for CSS.
  • Migrate OAI to OAC. Origin Access Control is the modern approach and supports additional features (KMS, PUT requests).
  • Structured data. Add JSON-LD for person/website schema.
  • OG images. Generate or create a social sharing image for each page.