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
| Library | Purpose |
|---|---|
| Nuxt 4 | Vue-based meta-framework, running in static generation mode (nuxt generate) |
| @nuxt/ui-pro | Pre-built UI components with Tailwind CSS — cards, badges, navigation |
| @nuxt/content | Markdown-driven content (like this page) with syntax highlighting |
| @nuxt/icon | Icon system using Heroicons and MDI icon sets |
| Mermaid | Diagram rendering for architecture and flow charts |
| Tailwind CSS | Utility-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=31536000is safe. The browser never re-downloads unchanged assets. - HTML always revalidates —
must-revalidateensures 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
| Service | Role |
|---|---|
| S3 | Hosts the static site files. Bucket policy restricts access to CloudFront only (OAC). |
| CloudFront | CDN distribution with edge caching. Serves the site globally with low latency. |
| ACM | Manages the TLS certificate for HTTPS. |
| IAM | Scoped credentials for GitLab CI — s3:PutObject, s3:DeleteObject, cloudfront:CreateInvalidation. |
| Cloudflare | DNS 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)