Back to Code & Architecture

Document not found, overwrite this content with #not-found slot in <ContentDoc>.

This Portfolio Site

This is a meta write-up — documenting how the portfolio you're currently reading is built, deployed, and served.

Architecture Overview

Tech Stack

Framework & Libraries

LibraryPurpose
Nuxt 4Vue-based meta-framework, running in static generation mode (nuxt generate)
@nuxt/ui-proPre-built UI components with Tailwind CSS — cards, badges, navigation
@nuxt/contentMarkdown-driven content (like this page) with syntax highlighting
@nuxt/iconIcon system using Heroicons and MDI icon sets
MermaidDiagram rendering for architecture and flow charts
Tailwind CSSUtility-first styling via Nuxt UI

Why Nuxt Static?

No server required. The entire site pre-renders to static HTML at build time, which means:

  • Zero compute costs (no Lambda, no containers)
  • Sub-second page loads via CDN edge
  • No cold starts, no scaling concerns
  • Perfect Lighthouse scores out of the box

CI/CD Pipeline

The pipeline runs on GitLab CI and deploys on every push to main:

stages:
  - build
  - deploy

cache:
  key:
    files:
      - package-lock.json
  paths:
    - node_modules/
    - .nuxt/

build:
  stage: build
  image: node:24.11.1-alpine3.22
  script:
    - yarn install
    - yarn generate
  artifacts:
    paths:
      - .output/public/
    expire_in: 1 hour
  only:
    - main

deploy:
  stage: deploy
  image:
    name: amazon/aws-cli:latest
    entrypoint: [""]
  script:
    # Sync all assets with aggressive caching (immutable)
    - aws s3 sync .output/public/ s3://$S3_BUCKET/ --delete
        --cache-control "public, max-age=31536000, immutable"

    # Override HTML files with no-cache (always revalidate)
    - aws s3 cp s3://$S3_BUCKET/ s3://$S3_BUCKET/ --recursive
        --exclude "*" --include "*.html"
        --metadata-directive REPLACE
        --cache-control "public, max-age=0, must-revalidate"
        --content-type "text/html"

    # Bust the CDN cache
    - aws cloudfront create-invalidation
        --distribution-id $CLOUDFRONT_DISTRIBUTION_ID
        --paths "/*"
  only:
    - main

Pipeline Design Decisions

  • Two-stage separation — build produces artifacts, deploy consumes them. If deploy fails, you don't rebuild.
  • Immutable asset caching — JS/CSS bundles get hashed filenames, so max-age=31536000 is safe. The browser never re-downloads unchanged assets.
  • HTML always revalidatesmust-revalidate ensures users always get the latest page content after a deploy.
  • CloudFront invalidation — full /* invalidation on every deploy. For a personal portfolio with infrequent deploys, this is simpler than selective invalidation.
  • Node 24 Alpine — minimal image, fast install, matches local dev.

AWS Services

ServiceRole
S3Hosts the static site files. Bucket policy restricts access to CloudFront only (OAC).
CloudFrontCDN distribution with edge caching. Serves the site globally with low latency.
ACMManages the TLS certificate for HTTPS.
IAMScoped credentials for GitLab CI — s3:PutObject, s3:DeleteObject, cloudfront:CreateInvalidation.
CloudflareDNS for the .dev domain (Route 53 doesn't support .dev TLD registration), WAF for bot/abuse protection, and error rate monitoring via Cloudflare Events. Proxies to the CloudFront distribution.

Cost

Effectively $0/month for a personal portfolio. S3 storage is negligible (a few MB), CloudFront free tier covers 1TB/month of transfer, and there's no compute.

Local Development

# Install dependencies
yarn install

# Run dev server with hot reload
yarn dev

# Generate static site (same as CI)
yarn generate

# Preview the generated site locally
yarn preview

What I'd Add Next

  • CloudWatch alarms on origin error spikes as a secondary signal (Cloudflare Events is the primary monitoring)