diff --git a/.github/actions/install-cache-deps/action.yml b/.github/actions/install-cache-deps/action.yml index 9ed70cc22..d4ddd271b 100644 --- a/.github/actions/install-cache-deps/action.yml +++ b/.github/actions/install-cache-deps/action.yml @@ -34,7 +34,7 @@ runs: - name: Install pnpm shell: bash - run: npm install -g pnpm@9 + run: npm install -g pnpm@11 - name: Get pnpm store directory shell: bash @@ -77,4 +77,4 @@ runs: - name: ↧ Install Remaining Dependencies shell: bash working-directory: ${{ inputs.WORKSPACE_ROOT }} - run: pnpm install + run: pnpm install --frozen-lockfile diff --git a/.github/workflows/backstop-remote.yml b/.github/workflows/backstop-remote.yml index cf4f7c5c0..75fbfdea3 100644 --- a/.github/workflows/backstop-remote.yml +++ b/.github/workflows/backstop-remote.yml @@ -11,7 +11,7 @@ permissions: env: BRANCH_NAME: ${{ github.head_ref || github.ref_name }} - NODE_VERSION: 20 + NODE_VERSION: 24 jobs: backstop: diff --git a/.github/workflows/bd-fe.yml b/.github/workflows/bd-fe.yml index ec464b6af..03f30ff9c 100644 --- a/.github/workflows/bd-fe.yml +++ b/.github/workflows/bd-fe.yml @@ -68,7 +68,7 @@ permissions: env: BRANCH_NAME: ${{ github.head_ref || github.ref_name }} - NODE_VERSION: 20 + NODE_VERSION: 24 jobs: build_deploy: diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 945edd58e..7187f4323 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -12,7 +12,7 @@ on: env: BRANCH_NAME: ${{ github.head_ref || github.ref_name }} - NODE_VERSION: 20 + NODE_VERSION: 24 jobs: lint: diff --git a/.github/workflows/test-backstop.yml b/.github/workflows/test-backstop.yml index 364f7dd2a..32b141be8 100644 --- a/.github/workflows/test-backstop.yml +++ b/.github/workflows/test-backstop.yml @@ -30,7 +30,7 @@ permissions: env: BRANCH_NAME: ${{ github.head_ref || github.ref_name }} - NODE_VERSION: 20 + NODE_VERSION: 24 jobs: backstop: diff --git a/.github/workflows/test-lighthouse.yml b/.github/workflows/test-lighthouse.yml index c175ae778..3a7232f99 100644 --- a/.github/workflows/test-lighthouse.yml +++ b/.github/workflows/test-lighthouse.yml @@ -23,7 +23,7 @@ on: GH_WORKFLOW_TOKEN: required: true env: - NODE_VERSION: 20 + NODE_VERSION: 24 jobs: lighthouse: diff --git a/.github/workflows/test-psi.yml b/.github/workflows/test-psi.yml index 6fa42cf72..97a87b2c8 100644 --- a/.github/workflows/test-psi.yml +++ b/.github/workflows/test-psi.yml @@ -21,7 +21,7 @@ permissions: env: BRANCH_NAME: ${{ github.head_ref || github.ref_name }} - NODE_VERSION: 20 + NODE_VERSION: 24 jobs: psi: diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 000000000..a45fd52cc --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +24 diff --git a/_ci/backstop/package.json b/_ci/backstop/package.json index 8b2442c06..7935b5931 100644 --- a/_ci/backstop/package.json +++ b/_ci/backstop/package.json @@ -35,10 +35,10 @@ "super-simple-web-server": "github:dgrebb/super-simple-web-server#0.0.2" }, "engines": { - "node": ">=16.0.0 <=20.x.x", - "npm": ">=6.0.0", - "pnpm": ">= 8.14.1" + "node": ">=22.0.0", + "pnpm": ">=11.0.0" }, + "packageManager": "pnpm@11.10.0", "pnpm": { "overrides": { "braces@<3.0.3": ">=3.0.3", diff --git a/_ci/perf/package.json b/_ci/perf/package.json index d7aa9fe61..77d75c700 100644 --- a/_ci/perf/package.json +++ b/_ci/perf/package.json @@ -29,10 +29,10 @@ "npm-check-updates": "^16.14.18" }, "engines": { - "node": ">=16.0.0 <=20.x.x", - "npm": ">=6.0.0", - "pnpm": ">= 8.14.1" + "node": ">=22.0.0", + "pnpm": ">=11.0.0" }, + "packageManager": "pnpm@11.10.0", "pnpm": { "overrides": { "braces@<3.0.3": ">=3.0.3", diff --git a/_docker/back.Dockerfile b/_docker/back.Dockerfile index 203ec5e15..0a4420c30 100644 --- a/_docker/back.Dockerfile +++ b/_docker/back.Dockerfile @@ -1,5 +1,5 @@ # -------- Base -------- # -FROM node:20-alpine AS base +FROM node:24-alpine AS base # Installing libvips-dev for sharp Compatibility RUN apk update && apk add --no-cache build-base \ gcc autoconf automake zlib-dev libpng-dev \ @@ -23,7 +23,7 @@ ENV NODE_ENV=${NODE_ENV} RUN pnpm run build # -------- Run -------- # -FROM node:20-alpine +FROM node:24-alpine COPY --from=build /app /app WORKDIR /app ENV PNPM_HOME="/pnpm" diff --git a/back/package.json b/back/package.json index a0e25a8b5..2ce7c7904 100644 --- a/back/package.json +++ b/back/package.json @@ -30,10 +30,10 @@ "strapi-plugin-config-sync": "^1.2.5" }, "engines": { - "node": ">=16.0.0 <=20.x.x", - "npm": ">=6.0.0", - "pnpm": ">= 8.14.1" + "node": ">=22.0.0", + "pnpm": ">=11.0.0" }, + "packageManager": "pnpm@11.10.0", "strapi": {}, "devDependencies": { "prop-types": "^15.8.1" diff --git a/docs/CMS_EVALUATION.md b/docs/CMS_EVALUATION.md new file mode 100644 index 000000000..0161eea6d --- /dev/null +++ b/docs/CMS_EVALUATION.md @@ -0,0 +1,188 @@ +# CMS Evaluation: Strapi 5 vs Alternatives + +## Executive Summary + +This document evaluates CMS options for dgrebb.com with a focus on: +1. Cost reduction (eliminating RDS/load balancer) +2. SSG compatibility for GitHub Actions builds +3. Migration effort from Strapi 4.25.2 + +## Current Architecture + +- **CMS**: Strapi 4.25.2 +- **Database**: PostgreSQL 15.5 on AWS RDS +- **Infrastructure**: ECS/Fargate + ALB +- **Front-end**: SvelteKit with SSG (adapter-static) +- **Content Types**: 25 API endpoints + +## Option 1: Strapi 5 with SQLite + +### Overview +Upgrade to Strapi 5 and switch from PostgreSQL to SQLite for cost reduction. + +### Pros +- Minimal migration effort (same API structure) +- No database server required +- SQLite 3 fully supported in Strapi 5 +- Uses `better-sqlite3` package (modernized WAL mode) +- Eliminates RDS costs (~$15-30/month) + +### Cons +- **Not recommended for production** by Strapi +- SQLite limitations: + - No concurrent writes + - Single-writer model + - Risk of "database locked" errors + - No transaction isolation +- Still requires CMS server running (ECS/container costs) + +### Verdict +**Not recommended** for this use case. SQLite is for development/testing only. + +--- + +## Option 2: Git-Based CMS (TinaCMS, Keystatic, Decap) + +### Overview +Move content from database to Git repository as Markdown/JSON files. + +### Candidates + +| CMS | GitHub Stars | Strengths | SvelteKit Support | +|-----|-------------|-----------|-------------------| +| TinaCMS | ~9K | Visual in-context editing | Via API | +| Keystatic | ~4K | Best Astro integration, clean UI | Via API | +| Decap CMS | ~18K | Most mature, many backends | Via API | + +### Pros +- **Zero infrastructure cost** (content in Git) +- Perfect for SSG builds +- No API latency during build +- Version control for content +- Works entirely in GitHub Actions + +### Cons +- **Significant migration effort** - must convert 25 content types +- Complex content relations harder to model +- No real-time preview without development server +- Limited for non-technical editors + +### Migration Effort: **HIGH** +- Requires restructuring all content as files +- Rebuild data fetching layer +- Create new admin editing workflow + +--- + +## Option 3: Payload CMS (Next.js Native) + +### Overview +TypeScript-first CMS that integrates directly with Next.js (now owned by Figma). + +### Pros +- Code-first, type-safe schemas +- Native Next.js integration +- SQLite support via better-sqlite3 +- Can deploy to Vercel with database + +### Cons +- **Not compatible with SvelteKit** - designed for Next.js +- Would require frontend framework change +- Significant rewrite effort + +### Verdict +**Not suitable** - requires abandoning SvelteKit. + +--- + +## Option 4: Directus (Database-First) + +### Overview +Wraps any SQL database with instant API and admin panel. + +### Pros +- Can use existing PostgreSQL data +- Instant API generation +- Visual data studio +- SQLite support for low-traffic + +### Cons +- Still requires running server +- Migration from Strapi schema required +- Different content modeling approach + +### Migration Effort: **MEDIUM** + +--- + +## Option 5: Export-at-Build Architecture + +### Overview +Keep Strapi as content editing tool but generate static JSON at build time, eliminating runtime CMS dependency. + +### Architecture +``` +GitHub Actions Workflow: +1. Spin up temporary Strapi + SQLite container +2. Seed database from exported content +3. Fetch all content via API → static JSON files +4. Build SvelteKit with static JSON as data source +5. Deploy static build to S3 + CloudFront +6. Destroy Strapi container +``` + +### Pros +- **Minimal code changes** - keep Strapi content model +- **Zero runtime infrastructure** - only S3 + CloudFront +- Content can be version-controlled (exported JSON) +- SSG build reads local files, not API + +### Cons +- More complex build pipeline +- Content editing requires local Strapi +- Changes need rebuild/deploy cycle + +### Implementation +1. Add `strapi export` to CI pipeline +2. Create SvelteKit data adapter for static JSON +3. Store content exports in repo or artifact + +### Migration Effort: **LOW-MEDIUM** + +--- + +## Recommendation + +### Short-Term (Immediate Cost Reduction) +**Option 5: Export-at-Build Architecture** + +- Eliminates RDS (~$15-30/month) +- Eliminates ECS/ALB (~$50-100/month) +- Keeps familiar Strapi workflow +- Minimal code changes to SvelteKit + +### Long-Term (Best DX) +Consider **Git-Based CMS (Keystatic or TinaCMS)** when ready for larger refactor: +- Zero infrastructure +- Content versioned in Git +- Better for solo maintainer workflow + +--- + +## Cost Analysis + +| Architecture | Monthly Cost (Est.) | Infrastructure | +|--------------|-------------------|----------------| +| Current (Strapi + RDS + ECS) | $80-150 | RDS + ECS + ALB + S3 + CloudFront | +| Strapi on EC2/Lightsail | $20-40 | EC2/Lightsail + S3 + CloudFront | +| Export-at-Build | $5-15 | S3 + CloudFront only | +| Git-Based CMS | $5-15 | S3 + CloudFront only | + +--- + +## Next Steps + +1. Create GitHub Actions workflow for export-at-build pattern +2. Test Strapi 5 migration in local environment +3. Evaluate content editing workflow for solo maintainer +4. Consider Keystatic for future content architecture diff --git a/docs/SSG_MIGRATION_PLAN.md b/docs/SSG_MIGRATION_PLAN.md new file mode 100644 index 000000000..e806947cd --- /dev/null +++ b/docs/SSG_MIGRATION_PLAN.md @@ -0,0 +1,418 @@ +# SSG Migration Plan + +## Strategy Overview + +**Keep production CMS running** while we build the static pipeline. The current Strapi on ECS/RDS continues serving API endpoints for builds until we're fully cut over. + +``` +CURRENT STATE TARGET STATE +───────────────────────────────────────────────────────────────────── + +┌──────────────┐ ┌──────────────┐ +│ Route53 │ │ Route53 │ +│ www.dgrebb │ │ www.dgrebb │ +└──────┬───────┘ └──────┬───────┘ + │ │ + ▼ ▼ +┌──────────────┐ ┌──────────────┐ ┌──────────────┐ +│ CloudFront │ │ CloudFront │ │ CloudFront │ (single) +│ (www) │ │ (uploads) │ │ (www+assets)│ +└──────┬───────┘ └──────┬───────┘ └──────┬───────┘ + │ │ │ + ▼ ▼ ▼ +┌──────────────┐ ┌──────────────┐ ┌──────────────┐ +│ S3 (static) │ │ S3 (media) │ │ S3 (single) │ +└──────────────┘ └──────────────┘ │ HTML+assets │ + └──────────────┘ +┌─────────────────────────────────┐ +│ VPC │ ← DELETED +│ ALB → ECS Fargate → RDS │ +│ (Strapi) (PostgreSQL) │ +└─────────────────────────────────┘ + +Monthly: ~$50-85 Monthly: ~$3-6 +``` + +--- + +## What We're Keeping vs Deleting + +### KEEP +| Resource | Why | +|----------|-----| +| **S3** (1 bucket) | Static site + assets | +| **CloudFront** (1 distribution) | HTTPS, caching, security headers, video delivery | +| **Route53** | DNS | +| **ACM certs** | TLS | +| **Terraform state bucket** | State management | + +### DELETE (after cutover) +| Resource | Current Cost | +|----------|-------------| +| RDS PostgreSQL | ~$15-25/mo | +| ECS Fargate | ~$10-20/mo | +| ALB | ~$16-25/mo | +| VPC + subnets | minimal | +| Security groups | $0 | +| 2nd CloudFront (uploads) | ~$2-5/mo | +| 2nd S3 bucket (uploads) | ~$1/mo | + +--- + +## Work Streams + +### Stream A: Build Pipeline (SvelteKit → S3) + +Build the static generation pipeline that fetches from existing prod CMS. + +``` +GitHub Actions + │ + ├─► Checkout repo + ├─► Install deps (pnpm) + ├─► Build SvelteKit + │ └─► Fetches from https://cms.dgrebb.com (existing prod) + ├─► Sync build/ to S3 + └─► Invalidate CloudFront +``` + +**Tasks:** +- [ ] A1. Create new S3 bucket for beta site +- [ ] A2. Create CloudFront distribution for beta.dgrebb.com +- [ ] A3. Create ACM cert for beta.dgrebb.com +- [ ] A4. Create Route53 record for beta.dgrebb.com +- [ ] A5. Create GitHub Actions workflow for SSG deploy +- [ ] A6. Test: push triggers build, site appears at beta.dgrebb.com + +### Stream B: Asset Migration + +Get uploads/media into the static build. + +**Option 1: Bake into build** (recommended for your content volume) +``` +Build step: + 1. Fetch all media URLs from Strapi + 2. Download to front/static/uploads/ + 3. Rewrite content URLs: cdn.dgrebb.com/uploads/* → /uploads/* + 4. Build includes everything +``` + +**Option 2: Keep separate CDN for video** +``` +Large videos stay on cdn.dgrebb.com +Images bake into static build +``` + +**Tasks:** +- [ ] B1. Audit current uploads (count, total size, video vs images) +- [ ] B2. Create build script to download/sync uploads +- [ ] B3. Update URL rewriting in build process +- [ ] B4. Decide: video on separate CDN or baked in? + +### Stream C: DNS Cutover + +Swap www and apex to point at new static infrastructure. + +**Tasks:** +- [ ] C1. Validate beta site thoroughly (checklist below) +- [ ] C2. Update CloudFront aliases: add www.dgrebb.com, dgrebb.com +- [ ] C3. Update Route53 A records for www and apex +- [ ] C4. Invalidate CloudFront cache +- [ ] C5. Monitor for 24-48 hours + +### Stream D: Teardown Old Infrastructure + +After cutover is stable, remove old resources. + +**Tasks:** +- [ ] D1. Backup RDS (final snapshot) +- [ ] D2. Export Strapi content (backup for future) +- [ ] D3. Scale ECS service to 0 +- [ ] D4. terraform destroy containers module +- [ ] D5. terraform destroy database module +- [ ] D6. terraform destroy scaling module (ALB) +- [ ] D7. Clean up VPC resources +- [ ] D8. Remove old uploads CDN + bucket (if not keeping for video) + +--- + +## Detailed Steps + +### A1-A4: Beta Infrastructure (Terraform) + +Create `_tf/beta/` directory: + +```hcl +# _tf/beta/main.tf + +locals { + domain = "beta.dgrebb.com" + dashed_domain = "beta-dgrebb-com" +} + +# S3 Bucket +resource "aws_s3_bucket" "static" { + bucket = local.dashed_domain +} + +resource "aws_s3_bucket_policy" "static" { + bucket = aws_s3_bucket.static.id + policy = jsonencode({ + Version = "2012-10-17" + Statement = [{ + Sid = "CloudFrontAccess" + Effect = "Allow" + Principal = { Service = "cloudfront.amazonaws.com" } + Action = "s3:GetObject" + Resource = "${aws_s3_bucket.static.arn}/*" + Condition = { + StringEquals = { + "AWS:SourceArn" = aws_cloudfront_distribution.static.arn + } + } + }] + }) +} + +# CloudFront Origin Access Control +resource "aws_cloudfront_origin_access_control" "static" { + name = local.dashed_domain + origin_access_control_origin_type = "s3" + signing_behavior = "always" + signing_protocol = "sigv4" +} + +# ACM Certificate (must be in us-east-1) +resource "aws_acm_certificate" "beta" { + provider = aws.us_east_1 + domain_name = local.domain + validation_method = "DNS" + + lifecycle { + create_before_destroy = true + } +} + +# CloudFront Distribution +resource "aws_cloudfront_distribution" "static" { + enabled = true + default_root_object = "index.html" + aliases = [local.domain] + price_class = "PriceClass_100" + + origin { + domain_name = aws_s3_bucket.static.bucket_regional_domain_name + origin_id = "S3" + origin_access_control_id = aws_cloudfront_origin_access_control.static.id + } + + default_cache_behavior { + allowed_methods = ["GET", "HEAD", "OPTIONS"] + cached_methods = ["GET", "HEAD"] + target_origin_id = "S3" + viewer_protocol_policy = "redirect-to-https" + compress = true + + cache_policy_id = "658327ea-f89d-4fab-a63d-7e88639e58f6" # CachingOptimized + origin_request_policy_id = "88a5eaf4-2fd4-4709-b370-b4c650ea3fcf" # CORS-S3Origin + } + + # SPA fallback - try /path/index.html + custom_error_response { + error_code = 403 + response_code = 200 + response_page_path = "/index.html" + } + + custom_error_response { + error_code = 404 + response_code = 404 + response_page_path = "/404/index.html" + } + + viewer_certificate { + acm_certificate_arn = aws_acm_certificate.beta.arn + ssl_support_method = "sni-only" + minimum_protocol_version = "TLSv1.2_2021" + } + + restrictions { + geo_restriction { restriction_type = "none" } + } +} + +# Route53 Record +resource "aws_route53_record" "beta" { + zone_id = data.aws_route53_zone.main.zone_id + name = local.domain + type = "A" + + alias { + name = aws_cloudfront_distribution.static.domain_name + zone_id = aws_cloudfront_distribution.static.hosted_zone_id + evaluate_target_health = false + } +} + +output "cloudfront_distribution_id" { + value = aws_cloudfront_distribution.static.id +} + +output "s3_bucket" { + value = aws_s3_bucket.static.id +} +``` + +### A5: GitHub Actions Deploy Workflow + +```yaml +# .github/workflows/deploy-ssg.yml +name: 🚀 Deploy SSG + +on: + push: + branches: [main] + workflow_dispatch: + +env: + NODE_VERSION: 24 + PNPM_VERSION: 11 + # Point at existing prod CMS during transition + API_URL: https://cms.dgrebb.com + +jobs: + build-deploy: + runs-on: ubuntu-latest + environment: beta # or production later + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: ${{ env.PNPM_VERSION }} + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: pnpm + cache-dependency-path: front/pnpm-lock.yaml + + - name: Install dependencies + working-directory: front + run: pnpm install --frozen-lockfile + + - name: Build + working-directory: front + run: pnpm build + env: + API_URL: ${{ env.API_URL }} + + - name: Configure AWS + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: us-east-1 + + - name: Deploy to S3 + run: | + # Immutable assets (hashed filenames) + aws s3 sync front/build/_app s3://${{ vars.S3_BUCKET }}/_app \ + --delete \ + --cache-control "max-age=31536000,immutable" + + # HTML and other files + aws s3 sync front/build s3://${{ vars.S3_BUCKET }} \ + --delete \ + --cache-control "max-age=0,must-revalidate" \ + --exclude "_app/*" + + - name: Invalidate CloudFront + run: | + aws cloudfront create-invalidation \ + --distribution-id ${{ vars.CLOUDFRONT_DISTRIBUTION_ID }} \ + --paths "/*" +``` + +--- + +## Validation Checklist (Stream C1) + +Before DNS cutover, verify at beta.dgrebb.com: + +### Functionality +- [ ] Homepage loads +- [ ] All navigation links work +- [ ] Blog posts render with content +- [ ] CV/portfolio pages render +- [ ] Images load correctly +- [ ] Videos play (if applicable) +- [ ] 404 page works +- [ ] Category/tag pages work + +### Technical +- [ ] HTTPS working (valid cert) +- [ ] Security headers present (CSP, HSTS, etc.) +- [ ] No mixed content warnings +- [ ] No console errors +- [ ] Lighthouse score acceptable +- [ ] Mobile responsive + +### SEO +- [ ] Meta tags present +- [ ] sitemap.xml accessible +- [ ] robots.txt correct + +--- + +## Timeline + +| Stream | Task | Dependency | Effort | +|--------|------|------------|--------| +| A | Beta infrastructure | None | 2-3 hours | +| A | GHA workflow | A infra | 1-2 hours | +| B | Asset migration | A workflow | 2-4 hours | +| C | Validation | A+B complete | 1-2 hours | +| C | DNS cutover | Validation pass | 30 min | +| D | Teardown | C stable 24-48h | 1-2 hours | + +**Total: ~8-14 hours of work, spread over a few sessions** + +--- + +## Future: CMS Strategy + +Once static pipeline is working, decide on CMS future: + +| Option | Pros | Cons | +|--------|------|------| +| **Keep Strapi on cheap hosting** (Fly.io, Railway) | Familiar UI, minimal changes | Still paying ~$5-15/mo | +| **Run Strapi locally only** | Free, full control | Must have laptop to edit | +| **Export to JSON, edit files** | Free, version controlled | No UI for relationships | +| **Move to git-based CMS** (Keystatic, TinaCMS) | UI + git + free | Migration effort | + +This decision can wait until after the static pipeline is proven. + +--- + +## Quick Reference: What's What + +``` +CURRENT INFRA (keep running during migration) +├── cms.dgrebb.com → ALB → ECS Fargate → RDS +├── cdn.dgrebb.com → CloudFront → S3 (uploads) +├── www.dgrebb.com → CloudFront → S3 (static site) +└── reports.dgrebb.com → CloudFront → S3 (reports) + +NEW INFRA (build this) +└── beta.dgrebb.com → CloudFront → S3 (SSG + assets) + +AFTER CUTOVER (delete old) +├── ALB, ECS, RDS, VPC → DELETE +├── cdn.dgrebb.com → DELETE (or keep for video) +└── www.dgrebb.com → Point to new S3 +``` diff --git a/docs/TERRAFORM_OUTLINE.md b/docs/TERRAFORM_OUTLINE.md new file mode 100644 index 000000000..dd31bb68a --- /dev/null +++ b/docs/TERRAFORM_OUTLINE.md @@ -0,0 +1,233 @@ +# Terraform Infrastructure Outline + +> ⚠️ **IMPORTANT**: This document is for **planning purposes only**. Do not apply any Terraform changes without proper state synchronization. Modifying Terraform without syncing state risks corrupting the infrastructure. + +## Current Infrastructure Overview + +### Architecture Diagram +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ AWS Infrastructure │ +├─────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ CloudFront │ │ CloudFront │ │ CloudFront │ │ +│ │ (www) │ │ (uploads) │ │ (reports) │ │ +│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │ +│ │ │ │ │ +│ ▼ ▼ ▼ │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ S3 Bucket │ │ S3 Bucket │ │ S3 Bucket │ │ +│ │ (static) │ │ (media) │ │ (reports) │ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ │ +│ │ +│ ┌─────────────────────────────────────────────────────────────┐ │ +│ │ VPC │ │ +│ │ ┌─────────────┐ │ │ +│ │ │ ALB │ ◄── HTTPS (443) │ │ +│ │ └──────┬──────┘ │ │ +│ │ │ │ │ +│ │ ▼ │ │ +│ │ ┌─────────────┐ ┌─────────────┐ │ │ +│ │ │ ECS Fargate │───►│ RDS │ │ │ +│ │ │ (Strapi) │ │ PostgreSQL │ │ │ +│ │ │ 256 CPU │ │ db.t3.micro │ │ │ +│ │ │ 512 MB │ │ postgres15 │ │ │ +│ │ └─────────────┘ └─────────────┘ │ │ +│ └─────────────────────────────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +### Modules Inventory + +| Module | Purpose | Files | +|--------|---------|-------| +| `cdn` | CloudFront distributions | cdn.tf, inputs.tf, outputs.tf | +| `containers` | ECR + ECS Fargate | containers.tf, inputs.tf | +| `database` | RDS PostgreSQL | database.tf, inputs.tf | +| `management` | IAM policies | management.tf, inputs.tf | +| `network` | VPC, subnets, DNS | network.tf, cdn-dns/, inputs.tf, outputs.tf | +| `reports` | Reports S3 bucket | reports.tf, inputs.tf, outputs.tf | +| `scaling` | ALB + target groups | scaling.tf, inputs.tf, outputs.tf | +| `security` | Security groups | security.tf, inputs.tf, outputs.tf | +| `state` | Terraform state bucket | state.tf, inputs.tf | +| `storage` | S3 buckets | storage.tf, defaults/, inputs.tf, outputs.tf | + +### Environments + +- **Production** (`_tf/prd/`): Full infrastructure +- **Staging** (`_tf/stg/`): Mirrors production + +--- + +## Required Updates (DO NOT APPLY) + +### 1. Database Version Update + +**Current State:** +```hcl +# _tf/modules/database/database.tf +engine_version = "15.5" +family = "postgres15" +``` + +**Recommended Update:** +```hcl +engine_version = "16.4" # Latest PostgreSQL 16 +family = "postgres16" +``` + +**Notes:** +- PostgreSQL 15.5 is outdated (released 2023) +- PostgreSQL 16 is current stable with better performance +- Requires RDS instance restart for major version upgrade + +--- + +### 2. Cost Reduction: Remove ALB + ECS + RDS + +**Current Monthly Costs (Estimated):** +| Service | Cost | +|---------|------| +| RDS db.t3.micro | $15-25 | +| ECS Fargate (256 CPU/512 MB) | $10-20 | +| ALB | $16-25 | +| CloudFront + S3 | $5-15 | +| **Total** | **$46-85** | + +**Proposed Architecture (SSG-Only):** +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ Minimal AWS Infrastructure │ +├─────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ CloudFront │ │ CloudFront │ │ CloudFront │ │ +│ │ (www) │ │ (uploads) │ │ (reports) │ │ +│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │ +│ │ │ │ │ +│ ▼ ▼ ▼ │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ S3 Bucket │ │ S3 Bucket │ │ S3 Bucket │ │ +│ │ (static) │ │ (media) │ │ (reports) │ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ │ +│ │ +│ [NO VPC, NO ALB, NO ECS, NO RDS] │ +│ │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +**Estimated Monthly Cost: $5-15** + +**Modules to Remove (in order):** +1. `scaling` - ALB, target groups, listeners +2. `containers` - ECS cluster, task definitions, ECR +3. `database` - RDS instance, parameter groups +4. Parts of `network` - VPC components (keep DNS) +5. Parts of `security` - Security groups + +**Modules to Keep:** +- `cdn` - CloudFront distributions +- `storage` - S3 buckets +- `reports` - Reports bucket +- `state` - Terraform state +- Parts of `network` - Route53 DNS records + +--- + +### 3. Module Dependencies + +Before removing any module, understand dependencies: + +``` +cdn ◄─── network (certificates, DNS) + └── storage (buckets) + +containers ◄── scaling (ALB target group) + ├── network (subnets) + └── security (security groups) + +database ◄── network (subnet group) + └── security (DB security group) + +scaling ◄── network (VPC, subnets) + └── security (LB security group) +``` + +**Safe Removal Order:** +1. Remove `containers` module +2. Remove `database` module +3. Remove `scaling` module +4. Remove unused `security` resources +5. Remove unused `network` resources (keep DNS) + +--- + +## Migration Plan (Read-Only) + +### Phase 1: Content Export +1. Export all Strapi content to JSON +2. Commit content to repository +3. Update SvelteKit to read from static JSON + +### Phase 2: Infrastructure Teardown +1. Backup RDS database +2. Remove ECS service (scale to 0) +3. Remove ALB +4. Remove RDS instance +5. Clean up VPC if unused + +### Phase 3: Terraform State Update +1. `terraform state rm` for removed resources +2. Update `main.tf` to remove modules +3. Verify remaining infrastructure + +--- + +## Current Resource Specifications + +### Database (RDS) +```hcl +instance_class = "db.t3.micro" +engine = "postgres" +engine_version = "15.5" +allocated_storage = 5 +multi_az = false +backup_retention_period = 3 +``` + +### Container (ECS Fargate) +```hcl +cpu = 256 +memory = 512 +desired_count = 1 +launch_type = "FARGATE" +``` + +### Load Balancer (ALB) +```hcl +load_balancer_type = "application" +ssl_policy = "ELBSecurityPolicy-2016-08" +``` + +--- + +## Warnings + +1. **DO NOT** run `terraform apply` without syncing state +2. **DO NOT** modify resources that may affect production traffic +3. **ALWAYS** backup RDS before any changes +4. **ALWAYS** test in staging first + +--- + +## Related Issues + +- #1338 - Terraform Infrastructure Outline + +--- + +## Contact + +For infrastructure changes, coordinate with the repository owner to ensure Terraform state is properly synchronized. diff --git a/front/.npmrc b/front/.npmrc index 08ca9deb4..078ce6e59 100644 --- a/front/.npmrc +++ b/front/.npmrc @@ -1,5 +1,4 @@ engine-strict=true resolution-mode=highest shamefully-hoist=true -hoist-pattern[]=*highlight.js* -enable-pre-post-scripts=true \ No newline at end of file +hoist-pattern[]=*highlight.js* \ No newline at end of file diff --git a/front/eslint.config.js b/front/eslint.config.js index c9c7eb372..45c6f86de 100644 --- a/front/eslint.config.js +++ b/front/eslint.config.js @@ -24,6 +24,10 @@ export default [ }, rules: { 'svelte/no-at-html-tags': 'off', + 'svelte/no-navigation-without-resolve': 'off', + 'svelte/require-each-key': 'warn', + 'svelte/valid-prop-names-in-kit-pages': 'off', + 'no-useless-assignment': 'warn', }, }, { diff --git a/front/package.json b/front/package.json index d31896469..1a4f92b48 100644 --- a/front/package.json +++ b/front/package.json @@ -33,53 +33,54 @@ }, "devDependencies": { "@accuser/svelte-plausible-analytics": "dgrebb/svelte-plausible-analytics#downstream/feature-support-custom-pageview-properties", - "@iconify-json/entypo-social": "^1.1.9", - "@iconify-json/gg": "^1.1.10", - "@iconify-json/icomoon-free": "^1.1.9", - "@iconify-json/icon-park-outline": "^1.1.16", - "@iconify-json/mdi": "^1.1.68", - "@iconify-json/ph": "^1.1.14", - "@iconify-json/simple-icons": "^1.1.114", - "@iconify-json/typcn": "^1.1.10", - "@playwright/test": "^1.46.1", - "@sveltejs/adapter-static": "^3.0.4", - "@sveltejs/kit": "^2.5.24", - "@sveltejs/vite-plugin-svelte": "^3.1.2", - "autoprefixer": "^10.4.20", - "eslint": "^9.9.1", - "eslint-config-prettier": "^9.1.0", - "eslint-plugin-svelte": "^2.43.0", - "highlight.js": "^11.10.0", + "@iconify-json/entypo-social": "^1.2.0", + "@iconify-json/gg": "^1.2.0", + "@iconify-json/icomoon-free": "^1.2.0", + "@iconify-json/icon-park-outline": "^1.2.0", + "@iconify-json/mdi": "^1.2.0", + "@iconify-json/ph": "^1.2.0", + "@iconify-json/simple-icons": "^1.2.0", + "@iconify-json/typcn": "^1.2.0", + "@playwright/test": "^1.61.0", + "@sveltejs/adapter-static": "^3.0.10", + "@sveltejs/kit": "^2.69.2", + "@sveltejs/vite-plugin-svelte": "^7.2.0", + "autoprefixer": "^10.5.0", + "@eslint/js": "^10.0.0", + "eslint": "^10.6.0", + "eslint-config-prettier": "^10.1.0", + "eslint-plugin-svelte": "^3.20.0", + "globals": "^17.7.0", + "highlight.js": "^11.11.0", "html-minifier-terser": "^7.2.0", - "marked": "^13.0.3", - "marked-alert": "^2.0.1", + "marked": "^15.0.0", + "marked-alert": "^2.1.0", "patch-package": "^8.0.0", - "postcss": "^8.4.41", + "postcss": "^8.5.0", "postcss-import": "^16.1.0", - "postcss-nested": "^6.2.0", + "postcss-nested": "^7.0.0", "postcss-nesting": "^13.0.0", - "prettier": "^3.3.3", - "prettier-plugin-css-order": "^2.1.2", - "prettier-plugin-svelte": "^3.2.6", - "rollup-plugin-bundle-stats": "^4.14.2", - "rollup-plugin-visualizer": "^5.12.0", + "prettier": "^3.9.0", + "prettier-plugin-css-order": "^2.2.0", + "prettier-plugin-svelte": "^4.1.0", + "rollup-plugin-bundle-stats": "^4.22.0", + "rollup-plugin-visualizer": "^5.14.0", "slugger": "^1.0.1", - "svelte": "^4.2.19", - "svelte-check": "^3.8.6", + "svelte": "^5.56.4", + "svelte-check": "^4.7.0", "svelte-highlight": "7.6.1", - "svelte-meta-tags": "^3.1.3", - "svelte-sitemap": "^2.6.0", - "typescript": "^5.5.4", - "unplugin-icons": "^0.19.2", - "vite": "^5.4.2", - "vite-plugin-progress": "^0.0.7", - "vitest": "^2.0.5" + "svelte-meta-tags": "^3.1.4", + "svelte-sitemap": "^2.8.0", + "typescript": "^5.9.0", + "unplugin-icons": "^23.0.0", + "vite": "^8.1.0", + "vitest": "^4.1.0" }, "engines": { - "node": ">=16.0.0 <=20.x.x", - "npm": ">=6.0.0", - "pnpm": ">= 8.14.1" + "node": ">=22.0.0", + "pnpm": ">=11.0.0" }, + "packageManager": "pnpm@11.10.0", "type": "module", "pnpm": { "patchedDependencies": { diff --git a/front/src/lib/components/cv/Award.svelte b/front/src/lib/components/cv/Award.svelte index 4c574b764..3809d22fb 100644 --- a/front/src/lib/components/cv/Award.svelte +++ b/front/src/lib/components/cv/Award.svelte @@ -2,8 +2,8 @@ import { PATHS } from '$lib/CONSTANTS'; const { experience: exPath, project: projPath } = PATHS.one; - export let content; - let { + let { content } = $props(); + const { name, body, experiences: { data: experiences }, diff --git a/front/src/lib/components/cv/Certification.svelte b/front/src/lib/components/cv/Certification.svelte index 40e8853b6..886449bf0 100644 --- a/front/src/lib/components/cv/Certification.svelte +++ b/front/src/lib/components/cv/Certification.svelte @@ -1,8 +1,8 @@ -
-
+{#snippet headerSnippet()} +

Professional Experience

{name} @@ -32,8 +32,10 @@ {/if}

+{/snippet} -
+{/snippet} + +
diff --git a/front/src/lib/components/cv/Industry.svelte b/front/src/lib/components/cv/Industry.svelte index c70de7d00..4178771d0 100644 --- a/front/src/lib/components/cv/Industry.svelte +++ b/front/src/lib/components/cv/Industry.svelte @@ -7,8 +7,8 @@ skill: skillPath, } = PATHS.one; - export let content; - let { + let { content } = $props(); + const { name, skills: { data: skills }, organizations: { data: organizations }, diff --git a/front/src/lib/components/cv/Organization.svelte b/front/src/lib/components/cv/Organization.svelte index 9580ba986..cacba88e8 100644 --- a/front/src/lib/components/cv/Organization.svelte +++ b/front/src/lib/components/cv/Organization.svelte @@ -6,8 +6,8 @@ project: projPath, skill: skillPath, } = PATHS.one; - export let content; - let { + let { content } = $props(); + const { name, body, industries: { data: industries }, diff --git a/front/src/lib/components/cv/Project.svelte b/front/src/lib/components/cv/Project.svelte index 3cdd13a44..515d7753d 100644 --- a/front/src/lib/components/cv/Project.svelte +++ b/front/src/lib/components/cv/Project.svelte @@ -4,9 +4,9 @@ import AsideGroup from '@components/general/Aside/AsideGroup.svelte'; import '@styles/pages/skills.css'; - export let content; - let collection = 'projects'; - let { + let { content } = $props(); + const collection = 'projects'; + const { name, body, artifacts, @@ -17,13 +17,15 @@ } = content.project; -
-
+{#snippet headerSnippet()} +

Projects

{name}

+{/snippet} -
+{/snippet} + +
diff --git a/front/src/lib/components/cv/Skill.svelte b/front/src/lib/components/cv/Skill.svelte index 866eebca0..e23e29fd4 100644 --- a/front/src/lib/components/cv/Skill.svelte +++ b/front/src/lib/components/cv/Skill.svelte @@ -10,9 +10,9 @@ classification: classPath, certification: certPath, } = PATHS.one; - export let content; + let { content } = $props(); - let { + const { name, body, hero, diff --git a/front/src/lib/components/cv/SkillBreakdown/SkillBreakdown.svelte b/front/src/lib/components/cv/SkillBreakdown/SkillBreakdown.svelte index 1799bcf61..caafe964a 100644 --- a/front/src/lib/components/cv/SkillBreakdown/SkillBreakdown.svelte +++ b/front/src/lib/components/cv/SkillBreakdown/SkillBreakdown.svelte @@ -3,7 +3,7 @@ import SkillIcon from '@components/icons/SkillIcon.svelte'; import { getHighContrastHexColor, getRGBFromHex } from '@utils'; - export let skills; + let { skills } = $props();
diff --git a/front/src/lib/components/cv/Timeline.svelte b/front/src/lib/components/cv/Timeline.svelte index 5c31f93df..6ae0800af 100644 --- a/front/src/lib/components/cv/Timeline.svelte +++ b/front/src/lib/components/cv/Timeline.svelte @@ -5,7 +5,7 @@ import Calendar from '@components/icons/Calendar.svelte'; import '@styles/pages/cv.css'; - export let items; + let { items } = $props(); const currentYear = new Date().getFullYear(); diff --git a/front/src/lib/components/general/Aside/Aside.svelte b/front/src/lib/components/general/Aside/Aside.svelte index bf92a8bd1..27ce1bb10 100644 --- a/front/src/lib/components/general/Aside/Aside.svelte +++ b/front/src/lib/components/general/Aside/Aside.svelte @@ -1,12 +1,13 @@