[{"data":1,"prerenderedAt":1592},["ShallowReactive",2],{"content-query-CbcetkYBh8":3},{"_path":4,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":8,"description":9,"tags":10,"body":17,"_type":1586,"_id":1587,"_source":1588,"_file":1589,"_stem":1590,"_extension":1591},"/codes/portfolio-site","codes",false,"","This Portfolio — Nuxt 4, GitLab CI, S3 + CloudFront","How this portfolio site is built and deployed — a static Nuxt 4 app with GitLab CI/CD, S3 hosting, and CloudFront CDN with cache invalidation.",[11,12,13,14,15,16],"nuxt","gitlab-ci","aws","s3","cloudfront","vue",{"type":18,"children":19,"toc":1571},"root",[20,29,35,42,156,162,169,302,308,313,338,344,357,956,962,1039,1045,1270,1410,1416,1428,1434,1546,1552,1565],{"type":21,"tag":22,"props":23,"children":25},"element","h1",{"id":24},"this-portfolio-site",[26],{"type":27,"value":28},"text","This Portfolio Site",{"type":21,"tag":30,"props":31,"children":32},"p",{},[33],{"type":27,"value":34},"This is a meta write-up — documenting how the portfolio you're currently reading is built, deployed, and served.",{"type":21,"tag":36,"props":37,"children":39},"h2",{"id":38},"architecture-overview",[40],{"type":27,"value":41},"Architecture Overview",{"type":21,"tag":43,"props":44,"children":48},"pre",{"className":45,"code":46,"language":47,"meta":7,"style":7},"language-mermaid shiki shiki-themes material-theme-lighter github-light github-dark","graph LR\n    A[GitLab Push] --> B[GitLab CI]\n    B --> C[yarn generate]\n    C --> D[Static HTML/JS/CSS]\n    D --> E[S3 Bucket]\n    E --> F[CloudFront CDN]\n    F --> G[User Browser]\n\n    style A fill:#FC6D26\n    style E fill:#FF9900\n    style F fill:#FF9900\n","mermaid",[49],{"type":21,"tag":50,"props":51,"children":52},"code",{"__ignoreMap":7},[53,65,74,83,92,101,110,119,129,138,147],{"type":21,"tag":54,"props":55,"children":58},"span",{"class":56,"line":57},"line",1,[59],{"type":21,"tag":54,"props":60,"children":62},{"style":61},"--shiki-light:#90A4AE;--shiki-default:#24292E;--shiki-dark:#E1E4E8",[63],{"type":27,"value":64},"graph LR\n",{"type":21,"tag":54,"props":66,"children":68},{"class":56,"line":67},2,[69],{"type":21,"tag":54,"props":70,"children":71},{"style":61},[72],{"type":27,"value":73},"    A[GitLab Push] --> B[GitLab CI]\n",{"type":21,"tag":54,"props":75,"children":77},{"class":56,"line":76},3,[78],{"type":21,"tag":54,"props":79,"children":80},{"style":61},[81],{"type":27,"value":82},"    B --> C[yarn generate]\n",{"type":21,"tag":54,"props":84,"children":86},{"class":56,"line":85},4,[87],{"type":21,"tag":54,"props":88,"children":89},{"style":61},[90],{"type":27,"value":91},"    C --> D[Static HTML/JS/CSS]\n",{"type":21,"tag":54,"props":93,"children":95},{"class":56,"line":94},5,[96],{"type":21,"tag":54,"props":97,"children":98},{"style":61},[99],{"type":27,"value":100},"    D --> E[S3 Bucket]\n",{"type":21,"tag":54,"props":102,"children":104},{"class":56,"line":103},6,[105],{"type":21,"tag":54,"props":106,"children":107},{"style":61},[108],{"type":27,"value":109},"    E --> F[CloudFront CDN]\n",{"type":21,"tag":54,"props":111,"children":113},{"class":56,"line":112},7,[114],{"type":21,"tag":54,"props":115,"children":116},{"style":61},[117],{"type":27,"value":118},"    F --> G[User Browser]\n",{"type":21,"tag":54,"props":120,"children":122},{"class":56,"line":121},8,[123],{"type":21,"tag":54,"props":124,"children":126},{"emptyLinePlaceholder":125},true,[127],{"type":27,"value":128},"\n",{"type":21,"tag":54,"props":130,"children":132},{"class":56,"line":131},9,[133],{"type":21,"tag":54,"props":134,"children":135},{"style":61},[136],{"type":27,"value":137},"    style A fill:#FC6D26\n",{"type":21,"tag":54,"props":139,"children":141},{"class":56,"line":140},10,[142],{"type":21,"tag":54,"props":143,"children":144},{"style":61},[145],{"type":27,"value":146},"    style E fill:#FF9900\n",{"type":21,"tag":54,"props":148,"children":150},{"class":56,"line":149},11,[151],{"type":21,"tag":54,"props":152,"children":153},{"style":61},[154],{"type":27,"value":155},"    style F fill:#FF9900\n",{"type":21,"tag":36,"props":157,"children":159},{"id":158},"tech-stack",[160],{"type":27,"value":161},"Tech Stack",{"type":21,"tag":163,"props":164,"children":166},"h3",{"id":165},"framework-libraries",[167],{"type":27,"value":168},"Framework & Libraries",{"type":21,"tag":170,"props":171,"children":172},"table",{},[173,192],{"type":21,"tag":174,"props":175,"children":176},"thead",{},[177],{"type":21,"tag":178,"props":179,"children":180},"tr",{},[181,187],{"type":21,"tag":182,"props":183,"children":184},"th",{},[185],{"type":27,"value":186},"Library",{"type":21,"tag":182,"props":188,"children":189},{},[190],{"type":27,"value":191},"Purpose",{"type":21,"tag":193,"props":194,"children":195},"tbody",{},[196,222,238,254,270,286],{"type":21,"tag":178,"props":197,"children":198},{},[199,209],{"type":21,"tag":200,"props":201,"children":202},"td",{},[203],{"type":21,"tag":204,"props":205,"children":206},"strong",{},[207],{"type":27,"value":208},"Nuxt 4",{"type":21,"tag":200,"props":210,"children":211},{},[212,214,220],{"type":27,"value":213},"Vue-based meta-framework, running in static generation mode (",{"type":21,"tag":50,"props":215,"children":217},{"className":216},[],[218],{"type":27,"value":219},"nuxt generate",{"type":27,"value":221},")",{"type":21,"tag":178,"props":223,"children":224},{},[225,233],{"type":21,"tag":200,"props":226,"children":227},{},[228],{"type":21,"tag":204,"props":229,"children":230},{},[231],{"type":27,"value":232},"@nuxt/ui-pro",{"type":21,"tag":200,"props":234,"children":235},{},[236],{"type":27,"value":237},"Pre-built UI components with Tailwind CSS — cards, badges, navigation",{"type":21,"tag":178,"props":239,"children":240},{},[241,249],{"type":21,"tag":200,"props":242,"children":243},{},[244],{"type":21,"tag":204,"props":245,"children":246},{},[247],{"type":27,"value":248},"@nuxt/content",{"type":21,"tag":200,"props":250,"children":251},{},[252],{"type":27,"value":253},"Markdown-driven content (like this page) with syntax highlighting",{"type":21,"tag":178,"props":255,"children":256},{},[257,265],{"type":21,"tag":200,"props":258,"children":259},{},[260],{"type":21,"tag":204,"props":261,"children":262},{},[263],{"type":27,"value":264},"@nuxt/icon",{"type":21,"tag":200,"props":266,"children":267},{},[268],{"type":27,"value":269},"Icon system using Heroicons and MDI icon sets",{"type":21,"tag":178,"props":271,"children":272},{},[273,281],{"type":21,"tag":200,"props":274,"children":275},{},[276],{"type":21,"tag":204,"props":277,"children":278},{},[279],{"type":27,"value":280},"Mermaid",{"type":21,"tag":200,"props":282,"children":283},{},[284],{"type":27,"value":285},"Diagram rendering for architecture and flow charts",{"type":21,"tag":178,"props":287,"children":288},{},[289,297],{"type":21,"tag":200,"props":290,"children":291},{},[292],{"type":21,"tag":204,"props":293,"children":294},{},[295],{"type":27,"value":296},"Tailwind CSS",{"type":21,"tag":200,"props":298,"children":299},{},[300],{"type":27,"value":301},"Utility-first styling via Nuxt UI",{"type":21,"tag":163,"props":303,"children":305},{"id":304},"why-nuxt-static",[306],{"type":27,"value":307},"Why Nuxt Static?",{"type":21,"tag":30,"props":309,"children":310},{},[311],{"type":27,"value":312},"No server required. The entire site pre-renders to static HTML at build time, which means:",{"type":21,"tag":314,"props":315,"children":316},"ul",{},[317,323,328,333],{"type":21,"tag":318,"props":319,"children":320},"li",{},[321],{"type":27,"value":322},"Zero compute costs (no Lambda, no containers)",{"type":21,"tag":318,"props":324,"children":325},{},[326],{"type":27,"value":327},"Sub-second page loads via CDN edge",{"type":21,"tag":318,"props":329,"children":330},{},[331],{"type":27,"value":332},"No cold starts, no scaling concerns",{"type":21,"tag":318,"props":334,"children":335},{},[336],{"type":27,"value":337},"Perfect Lighthouse scores out of the box",{"type":21,"tag":36,"props":339,"children":341},{"id":340},"cicd-pipeline",[342],{"type":27,"value":343},"CI/CD Pipeline",{"type":21,"tag":30,"props":345,"children":346},{},[347,349,355],{"type":27,"value":348},"The pipeline runs on GitLab CI and deploys on every push to ",{"type":21,"tag":50,"props":350,"children":352},{"className":351},[],[353],{"type":27,"value":354},"main",{"type":27,"value":356},":",{"type":21,"tag":43,"props":358,"children":362},{"className":359,"code":360,"language":361,"meta":7,"style":7},"language-yaml shiki shiki-themes material-theme-lighter github-light github-dark","stages:\n  - build\n  - deploy\n\ncache:\n  key:\n    files:\n      - package-lock.json\n  paths:\n    - node_modules/\n    - .nuxt/\n\nbuild:\n  stage: build\n  image: node:24.11.1-alpine3.22\n  script:\n    - yarn install\n    - yarn generate\n  artifacts:\n    paths:\n      - .output/public/\n    expire_in: 1 hour\n  only:\n    - main\n\ndeploy:\n  stage: deploy\n  image:\n    name: amazon/aws-cli:latest\n    entrypoint: [\"\"]\n  script:\n    # Sync all assets with aggressive caching (immutable)\n    - aws s3 sync .output/public/ s3://$S3_BUCKET/ --delete\n        --cache-control \"public, max-age=31536000, immutable\"\n\n    # Override HTML files with no-cache (always revalidate)\n    - aws s3 cp s3://$S3_BUCKET/ s3://$S3_BUCKET/ --recursive\n        --exclude \"*\" --include \"*.html\"\n        --metadata-directive REPLACE\n        --cache-control \"public, max-age=0, must-revalidate\"\n        --content-type \"text/html\"\n\n    # Bust the CDN cache\n    - aws cloudfront create-invalidation\n        --distribution-id $CLOUDFRONT_DISTRIBUTION_ID\n        --paths \"/*\"\n  only:\n    - main\n","yaml",[363],{"type":21,"tag":50,"props":364,"children":365},{"__ignoreMap":7},[366,381,395,407,414,426,438,450,463,475,488,500,508,521,538,556,569,582,595,608,621,634,652,665,678,686,699,715,727,745,774,786,796,809,818,826,835,848,857,866,875,884,892,901,914,923,932,944],{"type":21,"tag":54,"props":367,"children":368},{"class":56,"line":57},[369,375],{"type":21,"tag":54,"props":370,"children":372},{"style":371},"--shiki-light:#E53935;--shiki-default:#22863A;--shiki-dark:#85E89D",[373],{"type":27,"value":374},"stages",{"type":21,"tag":54,"props":376,"children":378},{"style":377},"--shiki-light:#39ADB5;--shiki-default:#24292E;--shiki-dark:#E1E4E8",[379],{"type":27,"value":380},":\n",{"type":21,"tag":54,"props":382,"children":383},{"class":56,"line":67},[384,389],{"type":21,"tag":54,"props":385,"children":386},{"style":377},[387],{"type":27,"value":388},"  -",{"type":21,"tag":54,"props":390,"children":392},{"style":391},"--shiki-light:#91B859;--shiki-default:#032F62;--shiki-dark:#9ECBFF",[393],{"type":27,"value":394}," build\n",{"type":21,"tag":54,"props":396,"children":397},{"class":56,"line":76},[398,402],{"type":21,"tag":54,"props":399,"children":400},{"style":377},[401],{"type":27,"value":388},{"type":21,"tag":54,"props":403,"children":404},{"style":391},[405],{"type":27,"value":406}," deploy\n",{"type":21,"tag":54,"props":408,"children":409},{"class":56,"line":85},[410],{"type":21,"tag":54,"props":411,"children":412},{"emptyLinePlaceholder":125},[413],{"type":27,"value":128},{"type":21,"tag":54,"props":415,"children":416},{"class":56,"line":94},[417,422],{"type":21,"tag":54,"props":418,"children":419},{"style":371},[420],{"type":27,"value":421},"cache",{"type":21,"tag":54,"props":423,"children":424},{"style":377},[425],{"type":27,"value":380},{"type":21,"tag":54,"props":427,"children":428},{"class":56,"line":103},[429,434],{"type":21,"tag":54,"props":430,"children":431},{"style":371},[432],{"type":27,"value":433},"  key",{"type":21,"tag":54,"props":435,"children":436},{"style":377},[437],{"type":27,"value":380},{"type":21,"tag":54,"props":439,"children":440},{"class":56,"line":112},[441,446],{"type":21,"tag":54,"props":442,"children":443},{"style":371},[444],{"type":27,"value":445},"    files",{"type":21,"tag":54,"props":447,"children":448},{"style":377},[449],{"type":27,"value":380},{"type":21,"tag":54,"props":451,"children":452},{"class":56,"line":121},[453,458],{"type":21,"tag":54,"props":454,"children":455},{"style":377},[456],{"type":27,"value":457},"      -",{"type":21,"tag":54,"props":459,"children":460},{"style":391},[461],{"type":27,"value":462}," package-lock.json\n",{"type":21,"tag":54,"props":464,"children":465},{"class":56,"line":131},[466,471],{"type":21,"tag":54,"props":467,"children":468},{"style":371},[469],{"type":27,"value":470},"  paths",{"type":21,"tag":54,"props":472,"children":473},{"style":377},[474],{"type":27,"value":380},{"type":21,"tag":54,"props":476,"children":477},{"class":56,"line":140},[478,483],{"type":21,"tag":54,"props":479,"children":480},{"style":377},[481],{"type":27,"value":482},"    -",{"type":21,"tag":54,"props":484,"children":485},{"style":391},[486],{"type":27,"value":487}," node_modules/\n",{"type":21,"tag":54,"props":489,"children":490},{"class":56,"line":149},[491,495],{"type":21,"tag":54,"props":492,"children":493},{"style":377},[494],{"type":27,"value":482},{"type":21,"tag":54,"props":496,"children":497},{"style":391},[498],{"type":27,"value":499}," .nuxt/\n",{"type":21,"tag":54,"props":501,"children":503},{"class":56,"line":502},12,[504],{"type":21,"tag":54,"props":505,"children":506},{"emptyLinePlaceholder":125},[507],{"type":27,"value":128},{"type":21,"tag":54,"props":509,"children":511},{"class":56,"line":510},13,[512,517],{"type":21,"tag":54,"props":513,"children":514},{"style":371},[515],{"type":27,"value":516},"build",{"type":21,"tag":54,"props":518,"children":519},{"style":377},[520],{"type":27,"value":380},{"type":21,"tag":54,"props":522,"children":524},{"class":56,"line":523},14,[525,530,534],{"type":21,"tag":54,"props":526,"children":527},{"style":371},[528],{"type":27,"value":529},"  stage",{"type":21,"tag":54,"props":531,"children":532},{"style":377},[533],{"type":27,"value":356},{"type":21,"tag":54,"props":535,"children":536},{"style":391},[537],{"type":27,"value":394},{"type":21,"tag":54,"props":539,"children":541},{"class":56,"line":540},15,[542,547,551],{"type":21,"tag":54,"props":543,"children":544},{"style":371},[545],{"type":27,"value":546},"  image",{"type":21,"tag":54,"props":548,"children":549},{"style":377},[550],{"type":27,"value":356},{"type":21,"tag":54,"props":552,"children":553},{"style":391},[554],{"type":27,"value":555}," node:24.11.1-alpine3.22\n",{"type":21,"tag":54,"props":557,"children":559},{"class":56,"line":558},16,[560,565],{"type":21,"tag":54,"props":561,"children":562},{"style":371},[563],{"type":27,"value":564},"  script",{"type":21,"tag":54,"props":566,"children":567},{"style":377},[568],{"type":27,"value":380},{"type":21,"tag":54,"props":570,"children":572},{"class":56,"line":571},17,[573,577],{"type":21,"tag":54,"props":574,"children":575},{"style":377},[576],{"type":27,"value":482},{"type":21,"tag":54,"props":578,"children":579},{"style":391},[580],{"type":27,"value":581}," yarn install\n",{"type":21,"tag":54,"props":583,"children":585},{"class":56,"line":584},18,[586,590],{"type":21,"tag":54,"props":587,"children":588},{"style":377},[589],{"type":27,"value":482},{"type":21,"tag":54,"props":591,"children":592},{"style":391},[593],{"type":27,"value":594}," yarn generate\n",{"type":21,"tag":54,"props":596,"children":598},{"class":56,"line":597},19,[599,604],{"type":21,"tag":54,"props":600,"children":601},{"style":371},[602],{"type":27,"value":603},"  artifacts",{"type":21,"tag":54,"props":605,"children":606},{"style":377},[607],{"type":27,"value":380},{"type":21,"tag":54,"props":609,"children":611},{"class":56,"line":610},20,[612,617],{"type":21,"tag":54,"props":613,"children":614},{"style":371},[615],{"type":27,"value":616},"    paths",{"type":21,"tag":54,"props":618,"children":619},{"style":377},[620],{"type":27,"value":380},{"type":21,"tag":54,"props":622,"children":624},{"class":56,"line":623},21,[625,629],{"type":21,"tag":54,"props":626,"children":627},{"style":377},[628],{"type":27,"value":457},{"type":21,"tag":54,"props":630,"children":631},{"style":391},[632],{"type":27,"value":633}," .output/public/\n",{"type":21,"tag":54,"props":635,"children":637},{"class":56,"line":636},22,[638,643,647],{"type":21,"tag":54,"props":639,"children":640},{"style":371},[641],{"type":27,"value":642},"    expire_in",{"type":21,"tag":54,"props":644,"children":645},{"style":377},[646],{"type":27,"value":356},{"type":21,"tag":54,"props":648,"children":649},{"style":391},[650],{"type":27,"value":651}," 1 hour\n",{"type":21,"tag":54,"props":653,"children":655},{"class":56,"line":654},23,[656,661],{"type":21,"tag":54,"props":657,"children":658},{"style":371},[659],{"type":27,"value":660},"  only",{"type":21,"tag":54,"props":662,"children":663},{"style":377},[664],{"type":27,"value":380},{"type":21,"tag":54,"props":666,"children":668},{"class":56,"line":667},24,[669,673],{"type":21,"tag":54,"props":670,"children":671},{"style":377},[672],{"type":27,"value":482},{"type":21,"tag":54,"props":674,"children":675},{"style":391},[676],{"type":27,"value":677}," main\n",{"type":21,"tag":54,"props":679,"children":681},{"class":56,"line":680},25,[682],{"type":21,"tag":54,"props":683,"children":684},{"emptyLinePlaceholder":125},[685],{"type":27,"value":128},{"type":21,"tag":54,"props":687,"children":689},{"class":56,"line":688},26,[690,695],{"type":21,"tag":54,"props":691,"children":692},{"style":371},[693],{"type":27,"value":694},"deploy",{"type":21,"tag":54,"props":696,"children":697},{"style":377},[698],{"type":27,"value":380},{"type":21,"tag":54,"props":700,"children":702},{"class":56,"line":701},27,[703,707,711],{"type":21,"tag":54,"props":704,"children":705},{"style":371},[706],{"type":27,"value":529},{"type":21,"tag":54,"props":708,"children":709},{"style":377},[710],{"type":27,"value":356},{"type":21,"tag":54,"props":712,"children":713},{"style":391},[714],{"type":27,"value":406},{"type":21,"tag":54,"props":716,"children":718},{"class":56,"line":717},28,[719,723],{"type":21,"tag":54,"props":720,"children":721},{"style":371},[722],{"type":27,"value":546},{"type":21,"tag":54,"props":724,"children":725},{"style":377},[726],{"type":27,"value":380},{"type":21,"tag":54,"props":728,"children":730},{"class":56,"line":729},29,[731,736,740],{"type":21,"tag":54,"props":732,"children":733},{"style":371},[734],{"type":27,"value":735},"    name",{"type":21,"tag":54,"props":737,"children":738},{"style":377},[739],{"type":27,"value":356},{"type":21,"tag":54,"props":741,"children":742},{"style":391},[743],{"type":27,"value":744}," amazon/aws-cli:latest\n",{"type":21,"tag":54,"props":746,"children":748},{"class":56,"line":747},30,[749,754,758,763,769],{"type":21,"tag":54,"props":750,"children":751},{"style":371},[752],{"type":27,"value":753},"    entrypoint",{"type":21,"tag":54,"props":755,"children":756},{"style":377},[757],{"type":27,"value":356},{"type":21,"tag":54,"props":759,"children":760},{"style":377},[761],{"type":27,"value":762}," [",{"type":21,"tag":54,"props":764,"children":766},{"style":765},"--shiki-light:#39ADB5;--shiki-default:#032F62;--shiki-dark:#9ECBFF",[767],{"type":27,"value":768},"\"\"",{"type":21,"tag":54,"props":770,"children":771},{"style":377},[772],{"type":27,"value":773},"]\n",{"type":21,"tag":54,"props":775,"children":777},{"class":56,"line":776},31,[778,782],{"type":21,"tag":54,"props":779,"children":780},{"style":371},[781],{"type":27,"value":564},{"type":21,"tag":54,"props":783,"children":784},{"style":377},[785],{"type":27,"value":380},{"type":21,"tag":54,"props":787,"children":789},{"class":56,"line":788},32,[790],{"type":21,"tag":54,"props":791,"children":793},{"style":792},"--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#6A737D;--shiki-default-font-style:inherit;--shiki-dark:#6A737D;--shiki-dark-font-style:inherit",[794],{"type":27,"value":795},"    # Sync all assets with aggressive caching (immutable)\n",{"type":21,"tag":54,"props":797,"children":799},{"class":56,"line":798},33,[800,804],{"type":21,"tag":54,"props":801,"children":802},{"style":377},[803],{"type":27,"value":482},{"type":21,"tag":54,"props":805,"children":806},{"style":391},[807],{"type":27,"value":808}," aws s3 sync .output/public/ s3://$S3_BUCKET/ --delete\n",{"type":21,"tag":54,"props":810,"children":812},{"class":56,"line":811},34,[813],{"type":21,"tag":54,"props":814,"children":815},{"style":391},[816],{"type":27,"value":817},"        --cache-control \"public, max-age=31536000, immutable\"\n",{"type":21,"tag":54,"props":819,"children":821},{"class":56,"line":820},35,[822],{"type":21,"tag":54,"props":823,"children":824},{"emptyLinePlaceholder":125},[825],{"type":27,"value":128},{"type":21,"tag":54,"props":827,"children":829},{"class":56,"line":828},36,[830],{"type":21,"tag":54,"props":831,"children":832},{"style":792},[833],{"type":27,"value":834},"    # Override HTML files with no-cache (always revalidate)\n",{"type":21,"tag":54,"props":836,"children":838},{"class":56,"line":837},37,[839,843],{"type":21,"tag":54,"props":840,"children":841},{"style":377},[842],{"type":27,"value":482},{"type":21,"tag":54,"props":844,"children":845},{"style":391},[846],{"type":27,"value":847}," aws s3 cp s3://$S3_BUCKET/ s3://$S3_BUCKET/ --recursive\n",{"type":21,"tag":54,"props":849,"children":851},{"class":56,"line":850},38,[852],{"type":21,"tag":54,"props":853,"children":854},{"style":391},[855],{"type":27,"value":856},"        --exclude \"*\" --include \"*.html\"\n",{"type":21,"tag":54,"props":858,"children":860},{"class":56,"line":859},39,[861],{"type":21,"tag":54,"props":862,"children":863},{"style":391},[864],{"type":27,"value":865},"        --metadata-directive REPLACE\n",{"type":21,"tag":54,"props":867,"children":869},{"class":56,"line":868},40,[870],{"type":21,"tag":54,"props":871,"children":872},{"style":391},[873],{"type":27,"value":874},"        --cache-control \"public, max-age=0, must-revalidate\"\n",{"type":21,"tag":54,"props":876,"children":878},{"class":56,"line":877},41,[879],{"type":21,"tag":54,"props":880,"children":881},{"style":391},[882],{"type":27,"value":883},"        --content-type \"text/html\"\n",{"type":21,"tag":54,"props":885,"children":887},{"class":56,"line":886},42,[888],{"type":21,"tag":54,"props":889,"children":890},{"emptyLinePlaceholder":125},[891],{"type":27,"value":128},{"type":21,"tag":54,"props":893,"children":895},{"class":56,"line":894},43,[896],{"type":21,"tag":54,"props":897,"children":898},{"style":792},[899],{"type":27,"value":900},"    # Bust the CDN cache\n",{"type":21,"tag":54,"props":902,"children":904},{"class":56,"line":903},44,[905,909],{"type":21,"tag":54,"props":906,"children":907},{"style":377},[908],{"type":27,"value":482},{"type":21,"tag":54,"props":910,"children":911},{"style":391},[912],{"type":27,"value":913}," aws cloudfront create-invalidation\n",{"type":21,"tag":54,"props":915,"children":917},{"class":56,"line":916},45,[918],{"type":21,"tag":54,"props":919,"children":920},{"style":391},[921],{"type":27,"value":922},"        --distribution-id $CLOUDFRONT_DISTRIBUTION_ID\n",{"type":21,"tag":54,"props":924,"children":926},{"class":56,"line":925},46,[927],{"type":21,"tag":54,"props":928,"children":929},{"style":391},[930],{"type":27,"value":931},"        --paths \"/*\"\n",{"type":21,"tag":54,"props":933,"children":935},{"class":56,"line":934},47,[936,940],{"type":21,"tag":54,"props":937,"children":938},{"style":371},[939],{"type":27,"value":660},{"type":21,"tag":54,"props":941,"children":942},{"style":377},[943],{"type":27,"value":380},{"type":21,"tag":54,"props":945,"children":947},{"class":56,"line":946},48,[948,952],{"type":21,"tag":54,"props":949,"children":950},{"style":377},[951],{"type":27,"value":482},{"type":21,"tag":54,"props":953,"children":954},{"style":391},[955],{"type":27,"value":677},{"type":21,"tag":163,"props":957,"children":959},{"id":958},"pipeline-design-decisions",[960],{"type":27,"value":961},"Pipeline Design Decisions",{"type":21,"tag":314,"props":963,"children":964},{},[965,975,993,1011,1029],{"type":21,"tag":318,"props":966,"children":967},{},[968,973],{"type":21,"tag":204,"props":969,"children":970},{},[971],{"type":27,"value":972},"Two-stage separation",{"type":27,"value":974}," — build produces artifacts, deploy consumes them. If deploy fails, you don't rebuild.",{"type":21,"tag":318,"props":976,"children":977},{},[978,983,985,991],{"type":21,"tag":204,"props":979,"children":980},{},[981],{"type":27,"value":982},"Immutable asset caching",{"type":27,"value":984}," — JS/CSS bundles get hashed filenames, so ",{"type":21,"tag":50,"props":986,"children":988},{"className":987},[],[989],{"type":27,"value":990},"max-age=31536000",{"type":27,"value":992}," is safe. The browser never re-downloads unchanged assets.",{"type":21,"tag":318,"props":994,"children":995},{},[996,1001,1003,1009],{"type":21,"tag":204,"props":997,"children":998},{},[999],{"type":27,"value":1000},"HTML always revalidates",{"type":27,"value":1002}," — ",{"type":21,"tag":50,"props":1004,"children":1006},{"className":1005},[],[1007],{"type":27,"value":1008},"must-revalidate",{"type":27,"value":1010}," ensures users always get the latest page content after a deploy.",{"type":21,"tag":318,"props":1012,"children":1013},{},[1014,1019,1021,1027],{"type":21,"tag":204,"props":1015,"children":1016},{},[1017],{"type":27,"value":1018},"CloudFront invalidation",{"type":27,"value":1020}," — full ",{"type":21,"tag":50,"props":1022,"children":1024},{"className":1023},[],[1025],{"type":27,"value":1026},"/*",{"type":27,"value":1028}," invalidation on every deploy. For a personal portfolio with infrequent deploys, this is simpler than selective invalidation.",{"type":21,"tag":318,"props":1030,"children":1031},{},[1032,1037],{"type":21,"tag":204,"props":1033,"children":1034},{},[1035],{"type":27,"value":1036},"Node 24 Alpine",{"type":27,"value":1038}," — minimal image, fast install, matches local dev.",{"type":21,"tag":36,"props":1040,"children":1042},{"id":1041},"aws-services",[1043],{"type":27,"value":1044},"AWS Services",{"type":21,"tag":43,"props":1046,"children":1048},{"className":45,"code":1047,"language":47,"meta":7,"style":7},"graph TB\n    subgraph \"DNS & Edge\"\n        CFL[Cloudflare\u003Cbr/>DNS for .dev]\n    end\n\n    subgraph \"AWS Infrastructure\"\n        CF[CloudFront\u003Cbr/>CDN Distribution]\n        S3[S3 Bucket\u003Cbr/>Static Hosting]\n        ACM[ACM Certificate\u003Cbr/>TLS/SSL]\n    end\n\n    subgraph \"CI/CD\"\n        GL[GitLab CI\u003Cbr/>Build & Deploy]\n        IAM[IAM Role\u003Cbr/>CI Credentials]\n    end\n\n    CFL -->|CNAME| CF\n    GL -->|aws s3 sync| S3\n    GL -->|create-invalidation| CF\n    IAM -->|AssumeRole| GL\n    CF -->|Origin| S3\n    ACM -->|TLS| CF\n\n    style CFL fill:#F6821F\n    style S3 fill:#FF9900\n    style CF fill:#FF9900\n    style ACM fill:#FF9900\n    style IAM fill:#FF9900\n",[1049],{"type":21,"tag":50,"props":1050,"children":1051},{"__ignoreMap":7},[1052,1060,1068,1076,1084,1091,1099,1107,1115,1123,1130,1137,1145,1153,1161,1168,1175,1183,1191,1199,1207,1215,1223,1230,1238,1246,1254,1262],{"type":21,"tag":54,"props":1053,"children":1054},{"class":56,"line":57},[1055],{"type":21,"tag":54,"props":1056,"children":1057},{"style":61},[1058],{"type":27,"value":1059},"graph TB\n",{"type":21,"tag":54,"props":1061,"children":1062},{"class":56,"line":67},[1063],{"type":21,"tag":54,"props":1064,"children":1065},{"style":61},[1066],{"type":27,"value":1067},"    subgraph \"DNS & Edge\"\n",{"type":21,"tag":54,"props":1069,"children":1070},{"class":56,"line":76},[1071],{"type":21,"tag":54,"props":1072,"children":1073},{"style":61},[1074],{"type":27,"value":1075},"        CFL[Cloudflare\u003Cbr/>DNS for .dev]\n",{"type":21,"tag":54,"props":1077,"children":1078},{"class":56,"line":85},[1079],{"type":21,"tag":54,"props":1080,"children":1081},{"style":61},[1082],{"type":27,"value":1083},"    end\n",{"type":21,"tag":54,"props":1085,"children":1086},{"class":56,"line":94},[1087],{"type":21,"tag":54,"props":1088,"children":1089},{"emptyLinePlaceholder":125},[1090],{"type":27,"value":128},{"type":21,"tag":54,"props":1092,"children":1093},{"class":56,"line":103},[1094],{"type":21,"tag":54,"props":1095,"children":1096},{"style":61},[1097],{"type":27,"value":1098},"    subgraph \"AWS Infrastructure\"\n",{"type":21,"tag":54,"props":1100,"children":1101},{"class":56,"line":112},[1102],{"type":21,"tag":54,"props":1103,"children":1104},{"style":61},[1105],{"type":27,"value":1106},"        CF[CloudFront\u003Cbr/>CDN Distribution]\n",{"type":21,"tag":54,"props":1108,"children":1109},{"class":56,"line":121},[1110],{"type":21,"tag":54,"props":1111,"children":1112},{"style":61},[1113],{"type":27,"value":1114},"        S3[S3 Bucket\u003Cbr/>Static Hosting]\n",{"type":21,"tag":54,"props":1116,"children":1117},{"class":56,"line":131},[1118],{"type":21,"tag":54,"props":1119,"children":1120},{"style":61},[1121],{"type":27,"value":1122},"        ACM[ACM Certificate\u003Cbr/>TLS/SSL]\n",{"type":21,"tag":54,"props":1124,"children":1125},{"class":56,"line":140},[1126],{"type":21,"tag":54,"props":1127,"children":1128},{"style":61},[1129],{"type":27,"value":1083},{"type":21,"tag":54,"props":1131,"children":1132},{"class":56,"line":149},[1133],{"type":21,"tag":54,"props":1134,"children":1135},{"emptyLinePlaceholder":125},[1136],{"type":27,"value":128},{"type":21,"tag":54,"props":1138,"children":1139},{"class":56,"line":502},[1140],{"type":21,"tag":54,"props":1141,"children":1142},{"style":61},[1143],{"type":27,"value":1144},"    subgraph \"CI/CD\"\n",{"type":21,"tag":54,"props":1146,"children":1147},{"class":56,"line":510},[1148],{"type":21,"tag":54,"props":1149,"children":1150},{"style":61},[1151],{"type":27,"value":1152},"        GL[GitLab CI\u003Cbr/>Build & Deploy]\n",{"type":21,"tag":54,"props":1154,"children":1155},{"class":56,"line":523},[1156],{"type":21,"tag":54,"props":1157,"children":1158},{"style":61},[1159],{"type":27,"value":1160},"        IAM[IAM Role\u003Cbr/>CI Credentials]\n",{"type":21,"tag":54,"props":1162,"children":1163},{"class":56,"line":540},[1164],{"type":21,"tag":54,"props":1165,"children":1166},{"style":61},[1167],{"type":27,"value":1083},{"type":21,"tag":54,"props":1169,"children":1170},{"class":56,"line":558},[1171],{"type":21,"tag":54,"props":1172,"children":1173},{"emptyLinePlaceholder":125},[1174],{"type":27,"value":128},{"type":21,"tag":54,"props":1176,"children":1177},{"class":56,"line":571},[1178],{"type":21,"tag":54,"props":1179,"children":1180},{"style":61},[1181],{"type":27,"value":1182},"    CFL -->|CNAME| CF\n",{"type":21,"tag":54,"props":1184,"children":1185},{"class":56,"line":584},[1186],{"type":21,"tag":54,"props":1187,"children":1188},{"style":61},[1189],{"type":27,"value":1190},"    GL -->|aws s3 sync| S3\n",{"type":21,"tag":54,"props":1192,"children":1193},{"class":56,"line":597},[1194],{"type":21,"tag":54,"props":1195,"children":1196},{"style":61},[1197],{"type":27,"value":1198},"    GL -->|create-invalidation| CF\n",{"type":21,"tag":54,"props":1200,"children":1201},{"class":56,"line":610},[1202],{"type":21,"tag":54,"props":1203,"children":1204},{"style":61},[1205],{"type":27,"value":1206},"    IAM -->|AssumeRole| GL\n",{"type":21,"tag":54,"props":1208,"children":1209},{"class":56,"line":623},[1210],{"type":21,"tag":54,"props":1211,"children":1212},{"style":61},[1213],{"type":27,"value":1214},"    CF -->|Origin| S3\n",{"type":21,"tag":54,"props":1216,"children":1217},{"class":56,"line":636},[1218],{"type":21,"tag":54,"props":1219,"children":1220},{"style":61},[1221],{"type":27,"value":1222},"    ACM -->|TLS| CF\n",{"type":21,"tag":54,"props":1224,"children":1225},{"class":56,"line":654},[1226],{"type":21,"tag":54,"props":1227,"children":1228},{"emptyLinePlaceholder":125},[1229],{"type":27,"value":128},{"type":21,"tag":54,"props":1231,"children":1232},{"class":56,"line":667},[1233],{"type":21,"tag":54,"props":1234,"children":1235},{"style":61},[1236],{"type":27,"value":1237},"    style CFL fill:#F6821F\n",{"type":21,"tag":54,"props":1239,"children":1240},{"class":56,"line":680},[1241],{"type":21,"tag":54,"props":1242,"children":1243},{"style":61},[1244],{"type":27,"value":1245},"    style S3 fill:#FF9900\n",{"type":21,"tag":54,"props":1247,"children":1248},{"class":56,"line":688},[1249],{"type":21,"tag":54,"props":1250,"children":1251},{"style":61},[1252],{"type":27,"value":1253},"    style CF fill:#FF9900\n",{"type":21,"tag":54,"props":1255,"children":1256},{"class":56,"line":701},[1257],{"type":21,"tag":54,"props":1258,"children":1259},{"style":61},[1260],{"type":27,"value":1261},"    style ACM fill:#FF9900\n",{"type":21,"tag":54,"props":1263,"children":1264},{"class":56,"line":717},[1265],{"type":21,"tag":54,"props":1266,"children":1267},{"style":61},[1268],{"type":27,"value":1269},"    style IAM fill:#FF9900\n",{"type":21,"tag":170,"props":1271,"children":1272},{},[1273,1289],{"type":21,"tag":174,"props":1274,"children":1275},{},[1276],{"type":21,"tag":178,"props":1277,"children":1278},{},[1279,1284],{"type":21,"tag":182,"props":1280,"children":1281},{},[1282],{"type":27,"value":1283},"Service",{"type":21,"tag":182,"props":1285,"children":1286},{},[1287],{"type":27,"value":1288},"Role",{"type":21,"tag":193,"props":1290,"children":1291},{},[1292,1308,1324,1340,1379],{"type":21,"tag":178,"props":1293,"children":1294},{},[1295,1303],{"type":21,"tag":200,"props":1296,"children":1297},{},[1298],{"type":21,"tag":204,"props":1299,"children":1300},{},[1301],{"type":27,"value":1302},"S3",{"type":21,"tag":200,"props":1304,"children":1305},{},[1306],{"type":27,"value":1307},"Hosts the static site files. Bucket policy restricts access to CloudFront only (OAC).",{"type":21,"tag":178,"props":1309,"children":1310},{},[1311,1319],{"type":21,"tag":200,"props":1312,"children":1313},{},[1314],{"type":21,"tag":204,"props":1315,"children":1316},{},[1317],{"type":27,"value":1318},"CloudFront",{"type":21,"tag":200,"props":1320,"children":1321},{},[1322],{"type":27,"value":1323},"CDN distribution with edge caching. Serves the site globally with low latency.",{"type":21,"tag":178,"props":1325,"children":1326},{},[1327,1335],{"type":21,"tag":200,"props":1328,"children":1329},{},[1330],{"type":21,"tag":204,"props":1331,"children":1332},{},[1333],{"type":27,"value":1334},"ACM",{"type":21,"tag":200,"props":1336,"children":1337},{},[1338],{"type":27,"value":1339},"Manages the TLS certificate for HTTPS.",{"type":21,"tag":178,"props":1341,"children":1342},{},[1343,1351],{"type":21,"tag":200,"props":1344,"children":1345},{},[1346],{"type":21,"tag":204,"props":1347,"children":1348},{},[1349],{"type":27,"value":1350},"IAM",{"type":21,"tag":200,"props":1352,"children":1353},{},[1354,1356,1362,1364,1370,1371,1377],{"type":27,"value":1355},"Scoped credentials for GitLab CI — ",{"type":21,"tag":50,"props":1357,"children":1359},{"className":1358},[],[1360],{"type":27,"value":1361},"s3:PutObject",{"type":27,"value":1363},", ",{"type":21,"tag":50,"props":1365,"children":1367},{"className":1366},[],[1368],{"type":27,"value":1369},"s3:DeleteObject",{"type":27,"value":1363},{"type":21,"tag":50,"props":1372,"children":1374},{"className":1373},[],[1375],{"type":27,"value":1376},"cloudfront:CreateInvalidation",{"type":27,"value":1378},".",{"type":21,"tag":178,"props":1380,"children":1381},{},[1382,1390],{"type":21,"tag":200,"props":1383,"children":1384},{},[1385],{"type":21,"tag":204,"props":1386,"children":1387},{},[1388],{"type":27,"value":1389},"Cloudflare",{"type":21,"tag":200,"props":1391,"children":1392},{},[1393,1395,1401,1403,1408],{"type":27,"value":1394},"DNS for the ",{"type":21,"tag":50,"props":1396,"children":1398},{"className":1397},[],[1399],{"type":27,"value":1400},".dev",{"type":27,"value":1402}," domain (Route 53 doesn't support ",{"type":21,"tag":50,"props":1404,"children":1406},{"className":1405},[],[1407],{"type":27,"value":1400},{"type":27,"value":1409}," TLD registration), WAF for bot/abuse protection, and error rate monitoring via Cloudflare Events. Proxies to the CloudFront distribution.",{"type":21,"tag":163,"props":1411,"children":1413},{"id":1412},"cost",[1414],{"type":27,"value":1415},"Cost",{"type":21,"tag":30,"props":1417,"children":1418},{},[1419,1421,1426],{"type":27,"value":1420},"Effectively ",{"type":21,"tag":204,"props":1422,"children":1423},{},[1424],{"type":27,"value":1425},"$0/month",{"type":27,"value":1427}," for a personal portfolio. S3 storage is negligible (a few MB), CloudFront free tier covers 1TB/month of transfer, and there's no compute.",{"type":21,"tag":36,"props":1429,"children":1431},{"id":1430},"local-development",[1432],{"type":27,"value":1433},"Local Development",{"type":21,"tag":43,"props":1435,"children":1439},{"className":1436,"code":1437,"language":1438,"meta":7,"style":7},"language-bash shiki shiki-themes material-theme-lighter github-light github-dark","# Install dependencies\nyarn install\n\n# Run dev server with hot reload\nyarn dev\n\n# Generate static site (same as CI)\nyarn generate\n\n# Preview the generated site locally\nyarn preview\n","bash",[1440],{"type":21,"tag":50,"props":1441,"children":1442},{"__ignoreMap":7},[1443,1451,1465,1472,1480,1492,1499,1507,1519,1526,1534],{"type":21,"tag":54,"props":1444,"children":1445},{"class":56,"line":57},[1446],{"type":21,"tag":54,"props":1447,"children":1448},{"style":792},[1449],{"type":27,"value":1450},"# Install dependencies\n",{"type":21,"tag":54,"props":1452,"children":1453},{"class":56,"line":67},[1454,1460],{"type":21,"tag":54,"props":1455,"children":1457},{"style":1456},"--shiki-light:#E2931D;--shiki-default:#6F42C1;--shiki-dark:#B392F0",[1458],{"type":27,"value":1459},"yarn",{"type":21,"tag":54,"props":1461,"children":1462},{"style":391},[1463],{"type":27,"value":1464}," install\n",{"type":21,"tag":54,"props":1466,"children":1467},{"class":56,"line":76},[1468],{"type":21,"tag":54,"props":1469,"children":1470},{"emptyLinePlaceholder":125},[1471],{"type":27,"value":128},{"type":21,"tag":54,"props":1473,"children":1474},{"class":56,"line":85},[1475],{"type":21,"tag":54,"props":1476,"children":1477},{"style":792},[1478],{"type":27,"value":1479},"# Run dev server with hot reload\n",{"type":21,"tag":54,"props":1481,"children":1482},{"class":56,"line":94},[1483,1487],{"type":21,"tag":54,"props":1484,"children":1485},{"style":1456},[1486],{"type":27,"value":1459},{"type":21,"tag":54,"props":1488,"children":1489},{"style":391},[1490],{"type":27,"value":1491}," dev\n",{"type":21,"tag":54,"props":1493,"children":1494},{"class":56,"line":103},[1495],{"type":21,"tag":54,"props":1496,"children":1497},{"emptyLinePlaceholder":125},[1498],{"type":27,"value":128},{"type":21,"tag":54,"props":1500,"children":1501},{"class":56,"line":112},[1502],{"type":21,"tag":54,"props":1503,"children":1504},{"style":792},[1505],{"type":27,"value":1506},"# Generate static site (same as CI)\n",{"type":21,"tag":54,"props":1508,"children":1509},{"class":56,"line":121},[1510,1514],{"type":21,"tag":54,"props":1511,"children":1512},{"style":1456},[1513],{"type":27,"value":1459},{"type":21,"tag":54,"props":1515,"children":1516},{"style":391},[1517],{"type":27,"value":1518}," generate\n",{"type":21,"tag":54,"props":1520,"children":1521},{"class":56,"line":131},[1522],{"type":21,"tag":54,"props":1523,"children":1524},{"emptyLinePlaceholder":125},[1525],{"type":27,"value":128},{"type":21,"tag":54,"props":1527,"children":1528},{"class":56,"line":140},[1529],{"type":21,"tag":54,"props":1530,"children":1531},{"style":792},[1532],{"type":27,"value":1533},"# Preview the generated site locally\n",{"type":21,"tag":54,"props":1535,"children":1536},{"class":56,"line":149},[1537,1541],{"type":21,"tag":54,"props":1538,"children":1539},{"style":1456},[1540],{"type":27,"value":1459},{"type":21,"tag":54,"props":1542,"children":1543},{"style":391},[1544],{"type":27,"value":1545}," preview\n",{"type":21,"tag":36,"props":1547,"children":1549},{"id":1548},"what-id-add-next",[1550],{"type":27,"value":1551},"What I'd Add Next",{"type":21,"tag":314,"props":1553,"children":1554},{},[1555],{"type":21,"tag":318,"props":1556,"children":1557},{},[1558,1563],{"type":21,"tag":204,"props":1559,"children":1560},{},[1561],{"type":27,"value":1562},"CloudWatch alarms",{"type":27,"value":1564}," on origin error spikes as a secondary signal (Cloudflare Events is the primary monitoring)",{"type":21,"tag":1566,"props":1567,"children":1568},"style",{},[1569],{"type":27,"value":1570},"html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":7,"searchDepth":67,"depth":67,"links":1572},[1573,1574,1578,1581,1584,1585],{"id":38,"depth":67,"text":41},{"id":158,"depth":67,"text":161,"children":1575},[1576,1577],{"id":165,"depth":76,"text":168},{"id":304,"depth":76,"text":307},{"id":340,"depth":67,"text":343,"children":1579},[1580],{"id":958,"depth":76,"text":961},{"id":1041,"depth":67,"text":1044,"children":1582},[1583],{"id":1412,"depth":76,"text":1415},{"id":1430,"depth":67,"text":1433},{"id":1548,"depth":67,"text":1551},"markdown","content:codes:portfolio-site.md","content","codes/portfolio-site.md","codes/portfolio-site","md",1782460598427]