[{"data":1,"prerenderedAt":2169},["ShallowReactive",2],{"page:\u002Fproduction-ready-deployment-cicd-workflows\u002Fgithub-actions-for-automated-ssg-builds\u002Fdeploying-to-multiple-environments-from-one-workflow":3,"all-docs-nav":1926},{"id":4,"title":5,"body":6,"breadcrumb":1901,"dateModified":1911,"datePublished":1911,"description":1912,"extension":1913,"faq":1914,"meta":1920,"navigation":314,"path":1921,"seo":1922,"slug":12,"stem":1923,"type":1924,"__hash__":1925},"content\u002Fproduction-ready-deployment-cicd-workflows\u002Fgithub-actions-for-automated-ssg-builds\u002Fdeploying-to-multiple-environments-from-one-workflow\u002Findex.md","Deploying to Multiple Environments From One Workflow",{"type":7,"value":8,"toc":1881},"minimark",[9,13,17,31,36,49,53,232,236,977,984,988,991,1369,1384,1482,1486,1489,1497,1505,1509,1512,1522,1528,1534,1591,1595,1598,1675,1678,1682,1685,1688,1735,1738,1741,1745,1787,1791,1796,1800,1805,1808,1812,1815,1819,1822,1826,1829,1833,1836,1840,1877],[10,11,5],"h1",{"id":12},"deploying-to-multiple-environments-from-one-workflow",[14,15,16],"p",{},"A static site's deploy pipeline usually starts as one workflow that builds and ships to production. Then a staging site appears, then per-branch previews, and before long there are three workflows that were copied from one another and have quietly diverged — different Node versions, different build flags, and a staging site that no longer resembles what production will be.",[14,18,19,20,24,25,30],{},"The fix is a single workflow that builds ",[21,22,23],"strong",{},"one"," artifact and promotes it through environments, with configuration injected at deploy time rather than baked in per build. This guide builds that, including environment-scoped secrets and approvals. It sits under ",[26,27,29],"a",{"href":28},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fgithub-actions-for-automated-ssg-builds\u002F","GitHub Actions for Automated SSG Builds",".",[32,33,35],"h2",{"id":34},"prerequisites","Prerequisites",[37,38,39,43,46],"ul",{},[40,41,42],"li",{},"A build that produces a self-contained output directory.",[40,44,45],{},"Deploy credentials per target, stored as environment-scoped secrets rather than repository-wide ones.",[40,47,48],{},"Relative internal links, or a plan for the handful of absolute URLs that need rewriting.",[32,50,52],{"id":51},"build-once-promote-the-same-bytes","Build Once, Promote the Same Bytes",[54,55,56,228],"figure",{},[57,58,65,66,65,70,65,74,65,81,65,214],"svg",{"viewBox":59,"role":60,"ariaLabelledBy":61,"xmlns":64},"0 0 760 300","img",[62,63],"me-promote-title","me-promote-desc","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","\n  ",[67,68,69],"title",{"id":62},"Build-once promotion versus rebuild per environment",[71,72,73],"desc",{"id":63},"In the build-once model one build produces one artifact that is deployed unchanged to preview, staging and production, so all three run identical bytes. In the rebuild model each environment runs its own build, producing three different artifacts, so what was tested is not what ships.",[75,76],"rect",{"x":77,"y":77,"width":78,"height":79,"fill":80},"0","760","300","#ffffff",[82,83,85,86,85,94,85,101,85,110,85,115,85,120,85,129,85,134,85,137,85,141,85,145,85,149,85,153,85,158,85,164,85,168,85,172,85,176,85,181,85,185,85,189,85,194,85,198,65],"g",{"style":84},"font-family:system-ui, sans-serif;font-size:12px","\n    ",[87,88,93],"text",{"x":89,"y":90,"fill":91,"style":92},"380","28","#1f2937","font-size:16px;font-weight:700;text-anchor:middle","The artifact you tested is the artifact you ship",[87,95,100],{"x":96,"y":97,"fill":98,"style":99},"40","76","#5a8a16","font-size:12px;font-weight:700","Build once",[75,102],{"x":103,"y":104,"width":103,"height":105,"rx":106,"fill":107,"opacity":108,"stroke":98,"style":109},"150","56","46","8","#8ac926","0.18","stroke-width:1.5px",[87,111,114],{"x":112,"y":97,"fill":91,"style":113},"225","font-size:12px;text-anchor:middle","one build",[87,116,119],{"x":112,"y":117,"fill":98,"style":118},"93","font-size:11px;text-anchor:middle","artifact a91f3c2",[75,121],{"x":122,"y":105,"width":123,"height":124,"rx":125,"fill":126,"opacity":127,"stroke":128,"style":109},"340","120","30","6","#d9e2ef","0.55","#556071",[87,130,133],{"x":131,"y":132,"fill":91,"style":118},"400","66","preview",[75,135],{"x":122,"y":136,"width":123,"height":124,"rx":125,"fill":126,"opacity":127,"stroke":128,"style":109},"82",[87,138,140],{"x":131,"y":139,"fill":91,"style":118},"102","staging",[75,142],{"x":122,"y":143,"width":123,"height":124,"rx":125,"fill":107,"opacity":144,"stroke":98,"style":109},"118","0.2",[87,146,148],{"x":131,"y":147,"fill":91,"style":118},"138","production",[87,150,152],{"x":151,"y":139,"fill":98,"style":99},"490","identical bytes everywhere",[87,154,157],{"x":96,"y":155,"fill":156,"style":99},"206","#d83b41","Rebuild each",[75,159],{"x":103,"y":160,"width":103,"height":124,"rx":125,"fill":161,"opacity":162,"stroke":163,"style":109},"176","#ffca3a","0.3","#a97b00",[87,165,167],{"x":112,"y":166,"fill":91,"style":118},"196","build → preview",[75,169],{"x":103,"y":170,"width":103,"height":124,"rx":125,"fill":161,"opacity":171,"stroke":163,"style":109},"212","0.26",[87,173,175],{"x":112,"y":174,"fill":91,"style":118},"232","build → staging",[75,177],{"x":103,"y":178,"width":103,"height":124,"rx":125,"fill":179,"opacity":180,"stroke":156,"style":109},"248","#ff595e","0.14",[87,182,184],{"x":112,"y":183,"fill":91,"style":118},"268","build → production",[87,186,188],{"x":122,"y":187,"fill":156,"style":99},"216","three artifacts, three sets of bytes",[87,190,193],{"x":122,"y":191,"fill":128,"style":192},"240","font-size:11px","a dependency resolved differently, a date stamped differently,",[87,195,197],{"x":122,"y":196,"fill":128,"style":192},"258","and staging no longer predicts production",[82,199,202,203,202,208,202,211,85],{"stroke":128,"fill":200,"style":201},"none","stroke-width:2px","\n      ",[204,205],"path",{"d":206,"style":207},"M302 70 L338 62","marker-end:url(#me-arrow)",[204,209],{"d":210,"style":207},"M302 82 L338 96",[204,212],{"d":213,"style":207},"M302 94 L338 130",[215,216,85,217,65],"defs",{},[218,219,202,225,85],"marker",{"id":220,"viewBox":221,"refX":106,"refY":222,"markerWidth":223,"markerHeight":223,"orient":224},"me-arrow","0 0 10 10","5","7","auto-start-reverse",[204,226],{"d":227,"fill":128},"M0 0 L10 5 L0 10 z",[229,230,231],"figcaption",{},"Rebuilding per environment reintroduces the exact uncertainty that testing on staging was supposed to remove. It also triples the build minutes for no benefit.",[32,233,235],{"id":234},"the-workflow","The Workflow",[237,238,243],"pre",{"className":239,"code":240,"language":241,"meta":242,"style":242},"language-yaml shiki shiki-themes github-light github-dark","# .github\u002Fworkflows\u002Fdeploy.yml\nname: Build and deploy\non:\n  pull_request:\n  push: { branches: [main] }\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions\u002Fcheckout@v4\n      - uses: actions\u002Fsetup-node@v4\n        with: { node-version: 20, cache: npm }\n      - run: npm ci\n      - run: npm run build\n      - name: Content gates\n        run: npm run check          # links, schema, spelling, terminology\n      - uses: actions\u002Fupload-artifact@v4\n        with: { name: site-${{ github.sha }}, path: dist, retention-days: 30 }\n\n  preview:\n    if: github.event_name == 'pull_request'\n    needs: build\n    runs-on: ubuntu-latest\n    environment: { name: preview }\n    steps:\n      - uses: actions\u002Fdownload-artifact@v4\n        with: { name: site-${{ github.sha }}, path: dist }\n      - run: node scripts\u002Fapply-env-config.mjs preview\n      - run: npx wrangler versions upload\n        env: { CLOUDFLARE_API_TOKEN: '${{ secrets.CF_TOKEN }}' }\n\n  staging:\n    if: github.ref == 'refs\u002Fheads\u002Fmain'\n    needs: build\n    runs-on: ubuntu-latest\n    environment: { name: staging, url: 'https:\u002F\u002Fstaging.example.com' }\n    steps:\n      - uses: actions\u002Fdownload-artifact@v4\n        with: { name: site-${{ github.sha }}, path: dist }\n      - run: node scripts\u002Fapply-env-config.mjs staging\n      - run: npx wrangler deploy --env staging\n        env: { CLOUDFLARE_API_TOKEN: '${{ secrets.CF_TOKEN }}' }\n      - run: node scripts\u002Fsmoke.mjs https:\u002F\u002Fstaging.example.com\n\n  production:\n    needs: staging\n    runs-on: ubuntu-latest\n    environment: { name: production, url: 'https:\u002F\u002Fexample.com' }   # required reviewer\n    steps:\n      - uses: actions\u002Fdownload-artifact@v4\n        with: { name: site-${{ github.sha }}, path: dist }\n      - run: node scripts\u002Fapply-env-config.mjs production\n      - run: npx wrangler deploy --env production\n        env: { CLOUDFLARE_API_TOKEN: '${{ secrets.CF_TOKEN }}' }\n      - run: node scripts\u002Fsmoke.mjs https:\u002F\u002Fexample.com\n","yaml","",[244,245,246,255,270,280,288,309,316,324,332,343,351,365,377,407,420,432,444,458,470,511,516,524,535,546,555,571,578,590,617,629,641,659,664,672,682,691,700,725,732,743,770,782,794,809,821,826,834,844,853,881,888,899,926,938,950,965],"code",{"__ignoreMap":242},[247,248,251],"span",{"class":249,"line":250},"line",1,[247,252,254],{"class":253},"sJ8bj","# .github\u002Fworkflows\u002Fdeploy.yml\n",[247,256,258,262,266],{"class":249,"line":257},2,[247,259,261],{"class":260},"s9eBZ","name",[247,263,265],{"class":264},"sVt8B",": ",[247,267,269],{"class":268},"sZZnC","Build and deploy\n",[247,271,273,277],{"class":249,"line":272},3,[247,274,276],{"class":275},"sj4cs","on",[247,278,279],{"class":264},":\n",[247,281,283,286],{"class":249,"line":282},4,[247,284,285],{"class":260},"  pull_request",[247,287,279],{"class":264},[247,289,291,294,297,300,303,306],{"class":249,"line":290},5,[247,292,293],{"class":260},"  push",[247,295,296],{"class":264},": { ",[247,298,299],{"class":260},"branches",[247,301,302],{"class":264},": [",[247,304,305],{"class":268},"main",[247,307,308],{"class":264},"] }\n",[247,310,312],{"class":249,"line":311},6,[247,313,315],{"emptyLinePlaceholder":314},true,"\n",[247,317,319,322],{"class":249,"line":318},7,[247,320,321],{"class":260},"jobs",[247,323,279],{"class":264},[247,325,327,330],{"class":249,"line":326},8,[247,328,329],{"class":260},"  build",[247,331,279],{"class":264},[247,333,335,338,340],{"class":249,"line":334},9,[247,336,337],{"class":260},"    runs-on",[247,339,265],{"class":264},[247,341,342],{"class":268},"ubuntu-latest\n",[247,344,346,349],{"class":249,"line":345},10,[247,347,348],{"class":260},"    steps",[247,350,279],{"class":264},[247,352,354,357,360,362],{"class":249,"line":353},11,[247,355,356],{"class":264},"      - ",[247,358,359],{"class":260},"uses",[247,361,265],{"class":264},[247,363,364],{"class":268},"actions\u002Fcheckout@v4\n",[247,366,368,370,372,374],{"class":249,"line":367},12,[247,369,356],{"class":264},[247,371,359],{"class":260},[247,373,265],{"class":264},[247,375,376],{"class":268},"actions\u002Fsetup-node@v4\n",[247,378,380,383,385,388,390,393,396,399,401,404],{"class":249,"line":379},13,[247,381,382],{"class":260},"        with",[247,384,296],{"class":264},[247,386,387],{"class":260},"node-version",[247,389,265],{"class":264},[247,391,392],{"class":275},"20",[247,394,395],{"class":264},", ",[247,397,398],{"class":260},"cache",[247,400,265],{"class":264},[247,402,403],{"class":268},"npm",[247,405,406],{"class":264}," }\n",[247,408,410,412,415,417],{"class":249,"line":409},14,[247,411,356],{"class":264},[247,413,414],{"class":260},"run",[247,416,265],{"class":264},[247,418,419],{"class":268},"npm ci\n",[247,421,423,425,427,429],{"class":249,"line":422},15,[247,424,356],{"class":264},[247,426,414],{"class":260},[247,428,265],{"class":264},[247,430,431],{"class":268},"npm run build\n",[247,433,435,437,439,441],{"class":249,"line":434},16,[247,436,356],{"class":264},[247,438,261],{"class":260},[247,440,265],{"class":264},[247,442,443],{"class":268},"Content gates\n",[247,445,447,450,452,455],{"class":249,"line":446},17,[247,448,449],{"class":260},"        run",[247,451,265],{"class":264},[247,453,454],{"class":268},"npm run check",[247,456,457],{"class":253},"          # links, schema, spelling, terminology\n",[247,459,461,463,465,467],{"class":249,"line":460},18,[247,462,356],{"class":264},[247,464,359],{"class":260},[247,466,265],{"class":264},[247,468,469],{"class":268},"actions\u002Fupload-artifact@v4\n",[247,471,473,475,477,479,481,484,487,490,493,495,497,500,502,505,507,509],{"class":249,"line":472},19,[247,474,382],{"class":260},[247,476,296],{"class":264},[247,478,261],{"class":260},[247,480,265],{"class":264},[247,482,483],{"class":268},"site-$",[247,485,486],{"class":264},"{{ ",[247,488,489],{"class":268},"github.sha",[247,491,492],{"class":264}," }}, ",[247,494,204],{"class":260},[247,496,265],{"class":264},[247,498,499],{"class":268},"dist",[247,501,395],{"class":264},[247,503,504],{"class":260},"retention-days",[247,506,265],{"class":264},[247,508,124],{"class":275},[247,510,406],{"class":264},[247,512,514],{"class":249,"line":513},20,[247,515,315],{"emptyLinePlaceholder":314},[247,517,519,522],{"class":249,"line":518},21,[247,520,521],{"class":260},"  preview",[247,523,279],{"class":264},[247,525,527,530,532],{"class":249,"line":526},22,[247,528,529],{"class":260},"    if",[247,531,265],{"class":264},[247,533,534],{"class":268},"github.event_name == 'pull_request'\n",[247,536,538,541,543],{"class":249,"line":537},23,[247,539,540],{"class":260},"    needs",[247,542,265],{"class":264},[247,544,545],{"class":268},"build\n",[247,547,549,551,553],{"class":249,"line":548},24,[247,550,337],{"class":260},[247,552,265],{"class":264},[247,554,342],{"class":268},[247,556,558,561,563,565,567,569],{"class":249,"line":557},25,[247,559,560],{"class":260},"    environment",[247,562,296],{"class":264},[247,564,261],{"class":260},[247,566,265],{"class":264},[247,568,133],{"class":268},[247,570,406],{"class":264},[247,572,574,576],{"class":249,"line":573},26,[247,575,348],{"class":260},[247,577,279],{"class":264},[247,579,581,583,585,587],{"class":249,"line":580},27,[247,582,356],{"class":264},[247,584,359],{"class":260},[247,586,265],{"class":264},[247,588,589],{"class":268},"actions\u002Fdownload-artifact@v4\n",[247,591,593,595,597,599,601,603,605,607,609,611,613,615],{"class":249,"line":592},28,[247,594,382],{"class":260},[247,596,296],{"class":264},[247,598,261],{"class":260},[247,600,265],{"class":264},[247,602,483],{"class":268},[247,604,486],{"class":264},[247,606,489],{"class":268},[247,608,492],{"class":264},[247,610,204],{"class":260},[247,612,265],{"class":264},[247,614,499],{"class":268},[247,616,406],{"class":264},[247,618,620,622,624,626],{"class":249,"line":619},29,[247,621,356],{"class":264},[247,623,414],{"class":260},[247,625,265],{"class":264},[247,627,628],{"class":268},"node scripts\u002Fapply-env-config.mjs preview\n",[247,630,632,634,636,638],{"class":249,"line":631},30,[247,633,356],{"class":264},[247,635,414],{"class":260},[247,637,265],{"class":264},[247,639,640],{"class":268},"npx wrangler versions upload\n",[247,642,644,647,649,652,654,657],{"class":249,"line":643},31,[247,645,646],{"class":260},"        env",[247,648,296],{"class":264},[247,650,651],{"class":260},"CLOUDFLARE_API_TOKEN",[247,653,265],{"class":264},[247,655,656],{"class":268},"'${{ secrets.CF_TOKEN }}'",[247,658,406],{"class":264},[247,660,662],{"class":249,"line":661},32,[247,663,315],{"emptyLinePlaceholder":314},[247,665,667,670],{"class":249,"line":666},33,[247,668,669],{"class":260},"  staging",[247,671,279],{"class":264},[247,673,675,677,679],{"class":249,"line":674},34,[247,676,529],{"class":260},[247,678,265],{"class":264},[247,680,681],{"class":268},"github.ref == 'refs\u002Fheads\u002Fmain'\n",[247,683,685,687,689],{"class":249,"line":684},35,[247,686,540],{"class":260},[247,688,265],{"class":264},[247,690,545],{"class":268},[247,692,694,696,698],{"class":249,"line":693},36,[247,695,337],{"class":260},[247,697,265],{"class":264},[247,699,342],{"class":268},[247,701,703,705,707,709,711,713,715,718,720,723],{"class":249,"line":702},37,[247,704,560],{"class":260},[247,706,296],{"class":264},[247,708,261],{"class":260},[247,710,265],{"class":264},[247,712,140],{"class":268},[247,714,395],{"class":264},[247,716,717],{"class":260},"url",[247,719,265],{"class":264},[247,721,722],{"class":268},"'https:\u002F\u002Fstaging.example.com'",[247,724,406],{"class":264},[247,726,728,730],{"class":249,"line":727},38,[247,729,348],{"class":260},[247,731,279],{"class":264},[247,733,735,737,739,741],{"class":249,"line":734},39,[247,736,356],{"class":264},[247,738,359],{"class":260},[247,740,265],{"class":264},[247,742,589],{"class":268},[247,744,746,748,750,752,754,756,758,760,762,764,766,768],{"class":249,"line":745},40,[247,747,382],{"class":260},[247,749,296],{"class":264},[247,751,261],{"class":260},[247,753,265],{"class":264},[247,755,483],{"class":268},[247,757,486],{"class":264},[247,759,489],{"class":268},[247,761,492],{"class":264},[247,763,204],{"class":260},[247,765,265],{"class":264},[247,767,499],{"class":268},[247,769,406],{"class":264},[247,771,773,775,777,779],{"class":249,"line":772},41,[247,774,356],{"class":264},[247,776,414],{"class":260},[247,778,265],{"class":264},[247,780,781],{"class":268},"node scripts\u002Fapply-env-config.mjs staging\n",[247,783,785,787,789,791],{"class":249,"line":784},42,[247,786,356],{"class":264},[247,788,414],{"class":260},[247,790,265],{"class":264},[247,792,793],{"class":268},"npx wrangler deploy --env staging\n",[247,795,797,799,801,803,805,807],{"class":249,"line":796},43,[247,798,646],{"class":260},[247,800,296],{"class":264},[247,802,651],{"class":260},[247,804,265],{"class":264},[247,806,656],{"class":268},[247,808,406],{"class":264},[247,810,812,814,816,818],{"class":249,"line":811},44,[247,813,356],{"class":264},[247,815,414],{"class":260},[247,817,265],{"class":264},[247,819,820],{"class":268},"node scripts\u002Fsmoke.mjs https:\u002F\u002Fstaging.example.com\n",[247,822,824],{"class":249,"line":823},45,[247,825,315],{"emptyLinePlaceholder":314},[247,827,829,832],{"class":249,"line":828},46,[247,830,831],{"class":260},"  production",[247,833,279],{"class":264},[247,835,837,839,841],{"class":249,"line":836},47,[247,838,540],{"class":260},[247,840,265],{"class":264},[247,842,843],{"class":268},"staging\n",[247,845,847,849,851],{"class":249,"line":846},48,[247,848,337],{"class":260},[247,850,265],{"class":264},[247,852,342],{"class":268},[247,854,856,858,860,862,864,866,868,870,872,875,878],{"class":249,"line":855},49,[247,857,560],{"class":260},[247,859,296],{"class":264},[247,861,261],{"class":260},[247,863,265],{"class":264},[247,865,148],{"class":268},[247,867,395],{"class":264},[247,869,717],{"class":260},[247,871,265],{"class":264},[247,873,874],{"class":268},"'https:\u002F\u002Fexample.com'",[247,876,877],{"class":264}," }   ",[247,879,880],{"class":253},"# required reviewer\n",[247,882,884,886],{"class":249,"line":883},50,[247,885,348],{"class":260},[247,887,279],{"class":264},[247,889,891,893,895,897],{"class":249,"line":890},51,[247,892,356],{"class":264},[247,894,359],{"class":260},[247,896,265],{"class":264},[247,898,589],{"class":268},[247,900,902,904,906,908,910,912,914,916,918,920,922,924],{"class":249,"line":901},52,[247,903,382],{"class":260},[247,905,296],{"class":264},[247,907,261],{"class":260},[247,909,265],{"class":264},[247,911,483],{"class":268},[247,913,486],{"class":264},[247,915,489],{"class":268},[247,917,492],{"class":264},[247,919,204],{"class":260},[247,921,265],{"class":264},[247,923,499],{"class":268},[247,925,406],{"class":264},[247,927,929,931,933,935],{"class":249,"line":928},53,[247,930,356],{"class":264},[247,932,414],{"class":260},[247,934,265],{"class":264},[247,936,937],{"class":268},"node scripts\u002Fapply-env-config.mjs production\n",[247,939,941,943,945,947],{"class":249,"line":940},54,[247,942,356],{"class":264},[247,944,414],{"class":260},[247,946,265],{"class":264},[247,948,949],{"class":268},"npx wrangler deploy --env production\n",[247,951,953,955,957,959,961,963],{"class":249,"line":952},55,[247,954,646],{"class":260},[247,956,296],{"class":264},[247,958,651],{"class":260},[247,960,265],{"class":264},[247,962,656],{"class":268},[247,964,406],{"class":264},[247,966,968,970,972,974],{"class":249,"line":967},56,[247,969,356],{"class":264},[247,971,414],{"class":260},[247,973,265],{"class":264},[247,975,976],{"class":268},"node scripts\u002Fsmoke.mjs https:\u002F\u002Fexample.com\n",[14,978,979,980,983],{},"The same ",[244,981,982],{},"secrets.CF_TOKEN"," name appears three times and resolves to three different values, because each is scoped to its environment. A job running against staging cannot obtain the production token — the scoping is what makes a single workflow safe rather than convenient.",[32,985,987],{"id":986},"inject-configuration-at-deploy-time","Inject Configuration at Deploy Time",[14,989,990],{},"The one thing that genuinely differs per environment is a handful of absolute URLs and flags. Rewrite them in the artifact rather than rebuilding:",[237,992,996],{"className":993,"code":994,"language":995,"meta":242,"style":242},"language-javascript shiki shiki-themes github-light github-dark","\u002F\u002F scripts\u002Fapply-env-config.mjs \u003Cenvironment>\nimport { readFile, writeFile } from 'node:fs\u002Fpromises';\nimport { globSync } from 'node:fs';\n\nconst ENVS = {\n  preview:    { base: 'https:\u002F\u002Fpreview.example.com', robots: 'noindex, nofollow', analytics: 'off' },\n  staging:    { base: 'https:\u002F\u002Fstaging.example.com', robots: 'noindex, nofollow', analytics: 'off' },\n  production: { base: 'https:\u002F\u002Fexample.com',         robots: 'index, follow',     analytics: 'on'  },\n};\n\nconst env = ENVS[process.argv[2]];\nif (!env) { console.error(`unknown environment: ${process.argv[2]}`); process.exit(2); }\n\nfor (const file of globSync('dist\u002F**\u002F*.{html,xml,txt}')) {\n  const before = await readFile(file, 'utf8');\n  const after = before\n    .replaceAll('__SITE_BASE__', env.base)\n    .replaceAll('__ROBOTS__', env.robots)\n    .replaceAll('__ANALYTICS__', env.analytics);\n  if (after !== before) await writeFile(file, after);\n}\nconsole.log(`applied ${process.argv[2]} config`);\n","javascript",[244,997,998,1003,1021,1035,1039,1053,1076,1093,1115,1120,1124,1144,1200,1204,1230,1255,1267,1283,1297,1311,1334,1339],{"__ignoreMap":242},[247,999,1000],{"class":249,"line":250},[247,1001,1002],{"class":253},"\u002F\u002F scripts\u002Fapply-env-config.mjs \u003Cenvironment>\n",[247,1004,1005,1009,1012,1015,1018],{"class":249,"line":257},[247,1006,1008],{"class":1007},"szBVR","import",[247,1010,1011],{"class":264}," { readFile, writeFile } ",[247,1013,1014],{"class":1007},"from",[247,1016,1017],{"class":268}," 'node:fs\u002Fpromises'",[247,1019,1020],{"class":264},";\n",[247,1022,1023,1025,1028,1030,1033],{"class":249,"line":272},[247,1024,1008],{"class":1007},[247,1026,1027],{"class":264}," { globSync } ",[247,1029,1014],{"class":1007},[247,1031,1032],{"class":268}," 'node:fs'",[247,1034,1020],{"class":264},[247,1036,1037],{"class":249,"line":282},[247,1038,315],{"emptyLinePlaceholder":314},[247,1040,1041,1044,1047,1050],{"class":249,"line":290},[247,1042,1043],{"class":1007},"const",[247,1045,1046],{"class":275}," ENVS",[247,1048,1049],{"class":1007}," =",[247,1051,1052],{"class":264}," {\n",[247,1054,1055,1058,1061,1064,1067,1070,1073],{"class":249,"line":311},[247,1056,1057],{"class":264},"  preview:    { base: ",[247,1059,1060],{"class":268},"'https:\u002F\u002Fpreview.example.com'",[247,1062,1063],{"class":264},", robots: ",[247,1065,1066],{"class":268},"'noindex, nofollow'",[247,1068,1069],{"class":264},", analytics: ",[247,1071,1072],{"class":268},"'off'",[247,1074,1075],{"class":264}," },\n",[247,1077,1078,1081,1083,1085,1087,1089,1091],{"class":249,"line":318},[247,1079,1080],{"class":264},"  staging:    { base: ",[247,1082,722],{"class":268},[247,1084,1063],{"class":264},[247,1086,1066],{"class":268},[247,1088,1069],{"class":264},[247,1090,1072],{"class":268},[247,1092,1075],{"class":264},[247,1094,1095,1098,1100,1103,1106,1109,1112],{"class":249,"line":326},[247,1096,1097],{"class":264},"  production: { base: ",[247,1099,874],{"class":268},[247,1101,1102],{"class":264},",         robots: ",[247,1104,1105],{"class":268},"'index, follow'",[247,1107,1108],{"class":264},",     analytics: ",[247,1110,1111],{"class":268},"'on'",[247,1113,1114],{"class":264},"  },\n",[247,1116,1117],{"class":249,"line":334},[247,1118,1119],{"class":264},"};\n",[247,1121,1122],{"class":249,"line":345},[247,1123,315],{"emptyLinePlaceholder":314},[247,1125,1126,1128,1131,1133,1135,1138,1141],{"class":249,"line":353},[247,1127,1043],{"class":1007},[247,1129,1130],{"class":275}," env",[247,1132,1049],{"class":1007},[247,1134,1046],{"class":275},[247,1136,1137],{"class":264},"[process.argv[",[247,1139,1140],{"class":275},"2",[247,1142,1143],{"class":264},"]];\n",[247,1145,1146,1149,1152,1155,1158,1162,1165,1168,1171,1173,1176,1179,1181,1184,1187,1190,1193,1195,1197],{"class":249,"line":367},[247,1147,1148],{"class":1007},"if",[247,1150,1151],{"class":264}," (",[247,1153,1154],{"class":1007},"!",[247,1156,1157],{"class":264},"env) { console.",[247,1159,1161],{"class":1160},"sScJk","error",[247,1163,1164],{"class":264},"(",[247,1166,1167],{"class":268},"`unknown environment: ${",[247,1169,1170],{"class":264},"process",[247,1172,30],{"class":268},[247,1174,1175],{"class":264},"argv",[247,1177,1178],{"class":268},"[",[247,1180,1140],{"class":275},[247,1182,1183],{"class":268},"]",[247,1185,1186],{"class":268},"}`",[247,1188,1189],{"class":264},"); process.",[247,1191,1192],{"class":1160},"exit",[247,1194,1164],{"class":264},[247,1196,1140],{"class":275},[247,1198,1199],{"class":264},"); }\n",[247,1201,1202],{"class":249,"line":379},[247,1203,315],{"emptyLinePlaceholder":314},[247,1205,1206,1209,1211,1213,1216,1219,1222,1224,1227],{"class":249,"line":409},[247,1207,1208],{"class":1007},"for",[247,1210,1151],{"class":264},[247,1212,1043],{"class":1007},[247,1214,1215],{"class":275}," file",[247,1217,1218],{"class":1007}," of",[247,1220,1221],{"class":1160}," globSync",[247,1223,1164],{"class":264},[247,1225,1226],{"class":268},"'dist\u002F**\u002F*.{html,xml,txt}'",[247,1228,1229],{"class":264},")) {\n",[247,1231,1232,1235,1238,1240,1243,1246,1249,1252],{"class":249,"line":422},[247,1233,1234],{"class":1007},"  const",[247,1236,1237],{"class":275}," before",[247,1239,1049],{"class":1007},[247,1241,1242],{"class":1007}," await",[247,1244,1245],{"class":1160}," readFile",[247,1247,1248],{"class":264},"(file, ",[247,1250,1251],{"class":268},"'utf8'",[247,1253,1254],{"class":264},");\n",[247,1256,1257,1259,1262,1264],{"class":249,"line":434},[247,1258,1234],{"class":1007},[247,1260,1261],{"class":275}," after",[247,1263,1049],{"class":1007},[247,1265,1266],{"class":264}," before\n",[247,1268,1269,1272,1275,1277,1280],{"class":249,"line":446},[247,1270,1271],{"class":264},"    .",[247,1273,1274],{"class":1160},"replaceAll",[247,1276,1164],{"class":264},[247,1278,1279],{"class":268},"'__SITE_BASE__'",[247,1281,1282],{"class":264},", env.base)\n",[247,1284,1285,1287,1289,1291,1294],{"class":249,"line":460},[247,1286,1271],{"class":264},[247,1288,1274],{"class":1160},[247,1290,1164],{"class":264},[247,1292,1293],{"class":268},"'__ROBOTS__'",[247,1295,1296],{"class":264},", env.robots)\n",[247,1298,1299,1301,1303,1305,1308],{"class":249,"line":472},[247,1300,1271],{"class":264},[247,1302,1274],{"class":1160},[247,1304,1164],{"class":264},[247,1306,1307],{"class":268},"'__ANALYTICS__'",[247,1309,1310],{"class":264},", env.analytics);\n",[247,1312,1313,1316,1319,1322,1325,1328,1331],{"class":249,"line":513},[247,1314,1315],{"class":1007},"  if",[247,1317,1318],{"class":264}," (after ",[247,1320,1321],{"class":1007},"!==",[247,1323,1324],{"class":264}," before) ",[247,1326,1327],{"class":1007},"await",[247,1329,1330],{"class":1160}," writeFile",[247,1332,1333],{"class":264},"(file, after);\n",[247,1335,1336],{"class":249,"line":518},[247,1337,1338],{"class":264},"}\n",[247,1340,1341,1344,1347,1349,1352,1354,1356,1358,1360,1362,1364,1367],{"class":249,"line":526},[247,1342,1343],{"class":264},"console.",[247,1345,1346],{"class":1160},"log",[247,1348,1164],{"class":264},[247,1350,1351],{"class":268},"`applied ${",[247,1353,1170],{"class":264},[247,1355,30],{"class":268},[247,1357,1175],{"class":264},[247,1359,1178],{"class":268},[247,1361,1140],{"class":275},[247,1363,1183],{"class":268},[247,1365,1366],{"class":268},"} config`",[247,1368,1254],{"class":264},[14,1370,1371,1372,1375,1376,1379,1380,1383],{},"Placeholders keep the build environment-agnostic and make the substitution auditable — a ",[244,1373,1374],{},"grep"," for ",[244,1377,1378],{},"__SITE_BASE__"," in a deployed artifact is a one-line check that the step ran. The ",[244,1381,1382],{},"robots"," value is the important one: a staging site indexed by search engines is a real and surprisingly common incident.",[54,1385,1386,1479],{},[57,1387,65,1392,65,1395,65,1398,65,1401],{"viewBox":1388,"role":60,"ariaLabelledBy":1389,"xmlns":64},"0 0 760 290",[1390,1391],"me-scope-title","me-scope-desc",[67,1393,1394],{"id":1390},"Environment-scoped secrets and approvals",[71,1396,1397],{"id":1391},"Three environments with different properties. Preview has deploy credentials scoped to preview only, no approval and noindex. Staging has staging credentials, no approval and noindex. Production has production credentials, a required reviewer, and index follow. The same secret name resolves differently in each.",[75,1399],{"x":77,"y":77,"width":78,"height":1400,"fill":80},"290",[82,1402,85,1403,85,1406,85,1412,85,1415,85,1421,85,1424,85,1428,85,1432,85,1435,85,1439,85,1442,85,1446,85,1448,85,1450,85,1452,85,1455,85,1460,85,1463,85,1467,85,1470,85,1473,85,1476,65],{"style":84},[87,1404,1405],{"x":89,"y":90,"fill":91,"style":92},"One secret name, three scoped values",[75,1407],{"x":96,"y":104,"width":1408,"height":1409,"rx":1410,"fill":126,"opacity":1411,"stroke":128,"style":109},"220","180","12","0.5",[87,1413,133],{"x":103,"y":136,"fill":91,"style":1414},"font-size:13px;font-weight:700;text-anchor:middle",[87,1416,1420],{"x":1417,"y":1418,"fill":91,"style":1419},"62","112","font-size:12px","token: preview scope",[87,1422,1423],{"x":1417,"y":147,"fill":91,"style":1419},"approval: none",[87,1425,1427],{"x":1417,"y":1426,"fill":91,"style":1419},"164","robots: noindex",[87,1429,1431],{"x":1417,"y":1430,"fill":91,"style":1419},"190","analytics: off",[87,1433,1434],{"x":1417,"y":187,"fill":128,"style":192},"one per pull request",[75,1436],{"x":1437,"y":104,"width":1408,"height":1409,"rx":1410,"fill":161,"opacity":1438,"stroke":163,"style":109},"280","0.24",[87,1440,140],{"x":1441,"y":136,"fill":91,"style":1414},"390",[87,1443,1445],{"x":1444,"y":1418,"fill":91,"style":1419},"302","token: staging scope",[87,1447,1423],{"x":1444,"y":147,"fill":91,"style":1419},[87,1449,1427],{"x":1444,"y":1426,"fill":91,"style":1419},[87,1451,1431],{"x":1444,"y":1430,"fill":91,"style":1419},[87,1453,1454],{"x":1444,"y":187,"fill":128,"style":192},"every merge to main",[75,1456],{"x":1457,"y":104,"width":1458,"height":1409,"rx":1410,"fill":107,"opacity":1459,"stroke":98,"style":109},"520","200","0.16",[87,1461,148],{"x":1462,"y":136,"fill":91,"style":1414},"620",[87,1464,1466],{"x":1465,"y":1418,"fill":91,"style":1419},"542","token: prod scope",[87,1468,1469],{"x":1465,"y":147,"fill":91,"style":99},"approval: required",[87,1471,1472],{"x":1465,"y":1426,"fill":91,"style":1419},"robots: index",[87,1474,1475],{"x":1465,"y":1430,"fill":91,"style":1419},"analytics: on",[87,1477,1478],{"x":1465,"y":187,"fill":128,"style":192},"promoted, never rebuilt",[229,1480,1481],{},"Scoping is the security property that matters: a compromised preview job holds credentials that can only write to preview.",[32,1483,1485],{"id":1484},"gate-promotion-do-not-gate-the-build","Gate Promotion, Do Not Gate the Build",[14,1487,1488],{},"Approvals belong on the production deploy, not on the build or on staging. Gating earlier slows every change and buys nothing, because nothing has reached readers yet.",[14,1490,1491,1492,1496],{},"Configure the production environment with a required reviewer and, if useful, a wait timer. The deploy then pauses at the promotion step with the artifact already built and staging already verified, so the approver's decision is about ",[1493,1494,1495],"em",{},"shipping"," rather than about code — a distinction that makes the approval fast rather than ceremonial.",[14,1498,1499,1500,1504],{},"For content-only changes, consider auto-approving. A pipeline where prose deploys continuously and template changes require a reviewer matches the risk tiers described in ",[26,1501,1503],{"href":1502},"\u002Fproduction-ready-deployment-cicd-workflows\u002Frollbacks-and-deploy-safety-for-static-sites\u002F","Rollbacks and Deploy Safety for Static Sites",", and it keeps the approval meaningful by making it rare.",[32,1506,1508],{"id":1507},"what-each-environment-is-actually-for","What Each Environment Is Actually For",[14,1510,1511],{},"Environments accumulate when nobody has written down what each one answers. Three questions, three environments, and anything that does not answer a distinct question should not exist.",[14,1513,1514,1517,1518,30],{},[21,1515,1516],{},"Preview answers \"is this change right?\""," It is per pull request, ephemeral, indexed by nobody, and its audience is the reviewer. Its lifetime is the lifetime of the branch — which is why cleaning them up matters, as covered in ",[26,1519,1521],{"href":1520},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fpreview-environments-for-pull-requests\u002Fcleaning-up-stale-preview-deployments\u002F","Cleaning Up Stale Preview Deployments",[14,1523,1524,1527],{},[21,1525,1526],{},"Staging answers \"does this artifact work in a production-shaped environment?\""," It runs the same bytes production will run, against the same host configuration, with the same headers and redirects. If staging differs from production in any way other than its hostname and its robots policy, it has stopped answering the question.",[14,1529,1530,1533],{},[21,1531,1532],{},"Production answers nothing"," — it is the thing itself. The only decision at that boundary is whether to promote, which is why it is the only place an approval belongs.",[54,1535,1536,1588],{},[57,1537,65,1542,65,1545,65,1548,65,1550],{"viewBox":1538,"role":60,"ariaLabelledBy":1539,"xmlns":64},"0 0 760 280",[1540,1541],"me-purpose-title","me-purpose-desc",[67,1543,1544],{"id":1540},"One question per environment",[71,1546,1547],{"id":1541},"Three environments with the question each answers and its audience. Preview asks whether the change is right, for a reviewer, and is ephemeral. Staging asks whether the artifact works in a production-shaped environment, for the team, and lives as long as main. Production is the site itself, for readers, and only accepts promoted artifacts.",[75,1549],{"x":77,"y":77,"width":78,"height":1437,"fill":80},[82,1551,85,1552,85,1555,85,1560,85,1564,85,1567,85,1570,85,1574,85,1578,85,1581,85,1584,65],{"style":84},[87,1553,1554],{"x":89,"y":90,"fill":91,"style":92},"If two environments answer the same question, delete one",[75,1556],{"x":96,"y":104,"width":1557,"height":1558,"rx":1559,"fill":126,"opacity":1411,"stroke":128,"style":109},"680","60","10",[87,1561,1563],{"x":89,"y":1562,"fill":91,"style":1414},"80","preview — \"is this change right?\"",[87,1565,1566],{"x":89,"y":139,"fill":128,"style":118},"audience: the reviewer · lifetime: the branch · robots: noindex",[75,1568],{"x":96,"y":1569,"width":1557,"height":1558,"rx":1559,"fill":161,"opacity":1438,"stroke":163,"style":109},"124",[87,1571,1573],{"x":89,"y":1572,"fill":91,"style":1414},"148","staging — \"does this artifact work where production works?\"",[87,1575,1577],{"x":89,"y":1576,"fill":128,"style":118},"170","audience: the team · same bytes, same headers, same redirects · robots: noindex",[75,1579],{"x":96,"y":1580,"width":1557,"height":1558,"rx":1559,"fill":107,"opacity":1459,"stroke":98,"style":109},"192",[87,1582,1583],{"x":89,"y":187,"fill":91,"style":1414},"production — the site",[87,1585,1587],{"x":89,"y":1586,"fill":128,"style":118},"238","audience: readers · accepts promoted artifacts only · the one approval gate",[229,1589,1590],{},"Written down, the list also explains what to delete: a fourth environment is almost always a duplicate of staging with a different name.",[32,1592,1594],{"id":1593},"measured-impact","Measured Impact",[14,1596,1597],{},"A documentation site with three environments, before and after consolidating three workflows into one:",[1599,1600,1601,1617],"table",{},[1602,1603,1604],"thead",{},[1605,1606,1607,1611,1614],"tr",{},[1608,1609,1610],"th",{},"Measure",[1608,1612,1613],{},"Three workflows",[1608,1615,1616],{},"One workflow, promoted artifact",[1618,1619,1620,1632,1642,1653,1664],"tbody",{},[1605,1621,1622,1626,1629],{},[1623,1624,1625],"td",{},"Build minutes per merge",[1623,1627,1628],{},"6.9 min (3 builds)",[1623,1630,1631],{},"2.3 min (1 build)",[1605,1633,1634,1637,1640],{},[1623,1635,1636],{},"Config drift incidents per quarter",[1623,1638,1639],{},"4",[1623,1641,77],{},[1605,1643,1644,1647,1650],{},[1623,1645,1646],{},"Staging\u002Fproduction artifact identical",[1623,1648,1649],{},"No",[1623,1651,1652],{},"Yes",[1605,1654,1655,1658,1661],{},[1623,1656,1657],{},"Repository-wide secrets",[1623,1659,1660],{},"3",[1623,1662,1663],{},"0 (all environment-scoped)",[1605,1665,1666,1669,1672],{},[1623,1667,1668],{},"Time from merge to production",[1623,1670,1671],{},"11 min",[1623,1673,1674],{},"5 min + approval",[14,1676,1677],{},"The drift row is the one that motivated the change. Two of those four incidents were a Node version difference between the staging and production workflows, producing a staging site that had been green for a week while production shipped a different dependency tree.",[32,1679,1681],{"id":1680},"keep-the-artifact-traceable","Keep the Artifact Traceable",[14,1683,1684],{},"Because the same bytes move through three environments, you need to be able to say which artifact is where. Two cheap habits cover it.",[14,1686,1687],{},"Name the artifact after the commit, as the workflow above does, so the download step in each deploy job is unambiguous. And stamp the same SHA into the built HTML, so the deployed site can be asked directly:",[237,1689,1693],{"className":1690,"code":1691,"language":1692,"meta":242,"style":242},"language-bash shiki shiki-themes github-light github-dark","curl -s https:\u002F\u002Fstaging.example.com\u002F | grep -o '\u003Cmeta name=\"build\"[^>]*>'\ncurl -s https:\u002F\u002Fexample.com\u002F | grep -o '\u003Cmeta name=\"build\"[^>]*>'\n","bash",[244,1694,1695,1718],{"__ignoreMap":242},[247,1696,1697,1700,1703,1706,1709,1712,1715],{"class":249,"line":250},[247,1698,1699],{"class":1160},"curl",[247,1701,1702],{"class":275}," -s",[247,1704,1705],{"class":268}," https:\u002F\u002Fstaging.example.com\u002F",[247,1707,1708],{"class":1007}," |",[247,1710,1711],{"class":1160}," grep",[247,1713,1714],{"class":275}," -o",[247,1716,1717],{"class":268}," '\u003Cmeta name=\"build\"[^>]*>'\n",[247,1719,1720,1722,1724,1727,1729,1731,1733],{"class":249,"line":257},[247,1721,1699],{"class":1160},[247,1723,1702],{"class":275},[247,1725,1726],{"class":268}," https:\u002F\u002Fexample.com\u002F",[247,1728,1708],{"class":1007},[247,1730,1711],{"class":1160},[247,1732,1714],{"class":275},[247,1734,1717],{"class":268},[14,1736,1737],{},"When those two differ, you know exactly what is pending promotion. When they match, staging has nothing left to tell you. It is also the first command in any incident, which is why the same stamp appears in the rollback runbook.",[14,1739,1740],{},"Retain artifacts for at least as long as your rollback window — thirty days is a reasonable default — so promoting an older build is always possible without a rebuild.",[32,1742,1744],{"id":1743},"pitfalls-rollback","Pitfalls & Rollback",[37,1746,1747,1753,1759,1769,1775,1781],{},[40,1748,1749,1752],{},[21,1750,1751],{},"Rebuilding per environment."," Different bytes mean staging stops predicting production.",[40,1754,1755,1758],{},[21,1756,1757],{},"Repository-wide deploy secrets."," Any workflow can use them, including one triggered by a fork's pull request.",[40,1760,1761,1768],{},[21,1762,1763,1764,1767],{},"Forgetting ",[244,1765,1766],{},"noindex"," on non-production."," A staging site in search results is a real incident, and cleaning it up takes weeks.",[40,1770,1771,1774],{},[21,1772,1773],{},"Approvals on the build."," They slow everything and protect nothing; gate the promotion instead.",[40,1776,1777,1780],{},[21,1778,1779],{},"Environment-specific code paths."," If the artifact behaves differently per environment beyond configuration, you no longer have one artifact.",[40,1782,1783,1786],{},[21,1784,1785],{},"Rollback:"," the previous production deployment is untouched, so reverting is the same promotion in reverse. The workflow itself can be restored from history without affecting what is currently deployed.",[32,1788,1790],{"id":1789},"conclusion","Conclusion",[14,1792,1793,1794,30],{},"One workflow, one artifact, three environments that differ only in injected configuration and scoped credentials. Build and gate once, promote through preview and staging, and put the only approval on the production step. Everything else — the drift, the wasted build minutes, the staging site that was never quite production — follows from rebuilding when you could have promoted. The wider pipeline is in ",[26,1795,29],{"href":28},[32,1797,1799],{"id":1798},"faq","FAQ",[1801,1802,1804],"h3",{"id":1803},"should-i-build-once-and-promote-or-rebuild-per-environment","Should I build once and promote, or rebuild per environment?",[14,1806,1807],{},"Build once and promote whenever you can. A rebuilt artifact is a different artifact, so the thing you tested in staging is not the thing you shipped. Rebuild only when the build genuinely bakes in environment-specific values you cannot inject afterwards.",[1801,1809,1811],{"id":1810},"how-do-i-handle-a-base-url-that-differs-per-environment","How do I handle a base URL that differs per environment?",[14,1813,1814],{},"Prefer relative URLs so the same artifact works anywhere. Where an absolute URL is unavoidable — canonical tags, sitemap entries, Open Graph URLs — inject it at deploy time by rewriting a small placeholder, or accept a per-environment build for those files only.",[1801,1816,1818],{"id":1817},"what-is-the-point-of-environment-protection-rules","What is the point of environment protection rules?",[14,1820,1821],{},"They scope secrets and gate promotion. Production credentials live only in the production environment, so a workflow running against staging cannot use them even if it is compromised, and a required reviewer turns promotion into a deliberate act.",[1801,1823,1825],{"id":1824},"do-preview-deployments-need-their-own-environment","Do preview deployments need their own environment?",[14,1827,1828],{},"Usually not a protected one, but a named environment is still useful because it gives previews their own credentials and their own URL history. Keep those credentials scoped so a preview cannot write to production.",[1801,1830,1832],{"id":1831},"how-many-environments-is-too-many","How many environments is too many?",[14,1834,1835],{},"More than three is usually a sign that something else is wrong. Preview, staging and production cover the real cases for a static site; additional environments tend to be either unmaintained or duplicates of one another.",[32,1837,1839],{"id":1838},"related","Related",[37,1841,1842,1851,1858,1865,1870],{},[40,1843,1844,1847,1848,1850],{},[21,1845,1846],{},"Parent:"," ",[26,1849,29],{"href":28}," — the build this promotes.",[40,1852,1853,1857],{},[26,1854,1856],{"href":1855},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fgithub-actions-for-automated-ssg-builds\u002Fhow-to-set-up-github-actions-for-hugo-deployments\u002F","Securing Deploy Credentials in GitHub Actions"," — token scoping in practice.",[40,1859,1860,1864],{},[26,1861,1863],{"href":1862},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fpreview-environments-for-pull-requests\u002F","Preview Environments for Pull Requests"," — the first environment in the chain.",[40,1866,1867,1869],{},[26,1868,1503],{"href":1502}," — what promotion makes reversible.",[40,1871,1872,1876],{},[26,1873,1875],{"href":1874},"\u002Fproduction-ready-deployment-cicd-workflows\u002Frollbacks-and-deploy-safety-for-static-sites\u002Frunning-smoke-tests-against-a-preview-url\u002F","Running Smoke Tests Against a Preview URL"," — the check each environment runs.",[1878,1879,1880],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}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);}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}",{"title":242,"searchDepth":257,"depth":257,"links":1882},[1883,1884,1885,1886,1887,1888,1889,1890,1891,1892,1893,1900],{"id":34,"depth":257,"text":35},{"id":51,"depth":257,"text":52},{"id":234,"depth":257,"text":235},{"id":986,"depth":257,"text":987},{"id":1484,"depth":257,"text":1485},{"id":1507,"depth":257,"text":1508},{"id":1593,"depth":257,"text":1594},{"id":1680,"depth":257,"text":1681},{"id":1743,"depth":257,"text":1744},{"id":1789,"depth":257,"text":1790},{"id":1798,"depth":257,"text":1799,"children":1894},[1895,1896,1897,1898,1899],{"id":1803,"depth":272,"text":1804},{"id":1810,"depth":272,"text":1811},{"id":1817,"depth":272,"text":1818},{"id":1824,"depth":272,"text":1825},{"id":1831,"depth":272,"text":1832},{"id":1838,"depth":257,"text":1839},[1902,1905,1908,1909],{"name":1903,"item":1904},"Home","\u002F",{"name":1906,"item":1907},"Production-Ready Deployment & CI\u002FCD Workflows","\u002Fproduction-ready-deployment-cicd-workflows\u002F",{"name":29,"item":28},{"name":5,"item":1910},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fgithub-actions-for-automated-ssg-builds\u002Fdeploying-to-multiple-environments-from-one-workflow\u002F","2026-08-01","Build the artifact once and promote it to preview, staging and production with environment-scoped secrets, approvals and per-environment config injected at deploy time.","md",[1915,1916,1917,1918,1919],{"q":1804,"a":1807},{"q":1811,"a":1814},{"q":1818,"a":1821},{"q":1825,"a":1828},{"q":1832,"a":1835},{},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fgithub-actions-for-automated-ssg-builds\u002Fdeploying-to-multiple-environments-from-one-workflow",{"title":5,"description":1912},"production-ready-deployment-cicd-workflows\u002Fgithub-actions-for-automated-ssg-builds\u002Fdeploying-to-multiple-environments-from-one-workflow\u002Findex","article","rl4WH0_sWwYFp7z-BLuROpsDQODNUmTCtgkiobQhO0U",[1927,1930,1933,1936,1939,1942,1945,1948,1951,1954,1957,1960,1963,1966,1969,1972,1975,1978,1981,1984,1987,1990,1993,1996,1999,2002,2005,2008,2011,2014,2017,2020,2023,2026,2029,2032,2035,2038,2041,2044,2047,2050,2053,2056,2059,2062,2065,2068,2071,2074,2077,2080,2083,2086,2089,2092,2095,2098,2101,2104,2107,2110,2113,2116,2117,2120,2122,2125,2128,2131,2134,2137,2140,2143,2146,2149,2152,2155,2157,2159,2162,2164,2167],{"path":1928,"title":1929},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fastro-vs-eleventy-for-documentation-sites\u002Fchoosing-between-astro-and-eleventy-for-large-docs","Astro vs Eleventy for Large Docs (1000+ Pages)",{"path":1931,"title":1932},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fastro-vs-eleventy-for-documentation-sites\u002Fcontent-collections-vs-eleventy-data-cascade","Content Collections vs the Eleventy Data Cascade",{"path":1934,"title":1935},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fastro-vs-eleventy-for-documentation-sites","Astro vs Eleventy for Documentation Sites",{"path":1937,"title":1938},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fhugo-build-times-for-large-repositories\u002Fhow-to-benchmark-hugo-vs-astro-build-speeds","How to Benchmark Hugo vs Astro Build Speeds",{"path":1940,"title":1941},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fhugo-build-times-for-large-repositories","Hugo Build Times for Large Repositories",{"path":1943,"title":1944},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fhugo-build-times-for-large-repositories\u002Fprofiling-hugo-templates-with-template-metrics","Profiling Hugo Templates With Template Metrics",{"path":1946,"title":1947},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fhugo-build-times-for-large-repositories\u002Fspeeding-up-hugo-builds-with-render-hooks-and-caching","Speeding Up Hugo Builds with Render Hooks & Caching",{"path":1949,"title":1950},"\u002Fchoosing-the-right-static-site-generator-for-production","Choosing the Right Static Site Generator for Production",{"path":1952,"title":1953},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fjekyll-plugin-ecosystem\u002Feleventy-vs-jekyll-for-markdown-heavy-blogs","Eleventy vs Jekyll for Markdown-Heavy Blogs",{"path":1955,"title":1956},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fjekyll-plugin-ecosystem","Jekyll Plugin Ecosystem",{"path":1958,"title":1959},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fjekyll-plugin-ecosystem\u002Freplacing-jekyll-plugins-when-migrating-to-eleventy","Replacing Jekyll Plugins When Migrating to Eleventy",{"path":1961,"title":1962},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fjekyll-plugin-ecosystem\u002Frunning-jekyll-on-github-pages-without-plugins","Running Jekyll on GitHub Pages Without Plugins",{"path":1964,"title":1965},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fmigrating-between-static-site-generators","Migrating Between Static Site Generators",{"path":1967,"title":1968},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fmigrating-between-static-site-generators\u002Fkeeping-redirects-working-after-an-ssg-migration","Keeping Redirects Working After an SSG Migration",{"path":1970,"title":1971},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fmigrating-between-static-site-generators\u002Fmigrating-a-docs-site-from-jekyll-to-hugo","Migrating a Docs Site From Jekyll to Hugo",{"path":1973,"title":1974},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fmigrating-between-static-site-generators\u002Fmigrating-from-hugo-to-astro-without-breaking-urls","Migrating From Hugo to Astro Without Breaking URLs",{"path":1976,"title":1977},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fmigrating-between-static-site-generators\u002Fporting-shortcodes-and-includes-between-generators","Porting Shortcodes and Includes Between Generators",{"path":1979,"title":1980},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fnextjs-static-export-for-content-sites\u002Fhandling-dynamic-routes-in-nextjs-static-export","Handling Dynamic Routes in Next.js Static Export",{"path":1982,"title":1983},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fnextjs-static-export-for-content-sites","Next.js Static Export for Content Sites",{"path":1985,"title":1986},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fnextjs-static-export-for-content-sites\u002Fmigrating-from-gatsby-to-nextjs-static-export","Migrating from Gatsby to Next.js Static Export",{"path":1988,"title":1989},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fnextjs-static-export-for-content-sites\u002Fnextjs-static-export-vs-astro-for-marketing-sites","Next.js Static Export vs Astro for Marketing",{"path":1991,"title":1992},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fssg-framework-selection-matrix\u002Fbest-ssg-for-technical-writers-without-coding-experience","Best SSG for Non-Developer Technical Writers",{"path":1994,"title":1995},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fssg-framework-selection-matrix\u002Fchoosing-an-ssg-for-api-reference-documentation","Choosing an SSG for API Reference Documentation",{"path":1997,"title":1998},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fssg-framework-selection-matrix","SSG Framework Selection Matrix",{"path":2000,"title":2001},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fssg-framework-selection-matrix\u002Fpicking-an-ssg-for-a-multi-language-documentation-site","Picking an SSG for a Multi-Language Docs Site",{"path":2003,"title":2004},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fssg-framework-selection-matrix\u002Fssg-selection-checklist-for-engineering-teams","SSG Selection Checklist for Engineering Teams",{"path":2006,"title":2007},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fcdn-caching-rules-for-ssgs","CDN Caching Rules for SSGs",{"path":2009,"title":2010},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fcdn-caching-rules-for-ssgs\u002Fpurging-the-cdn-cache-after-a-static-deploy","Purging the CDN Cache After a Static Deploy",{"path":2012,"title":2013},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fcdn-caching-rules-for-ssgs\u002Fsetting-cache-control-headers-on-cloudflare-pages","Cache-Control Headers on Cloudflare Pages",{"path":2015,"title":2016},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fcdn-caching-rules-for-ssgs\u002Fsetting-up-proper-cache-headers-on-netlify","Proper Cache Headers on Netlify for SSGs",{"path":2018,"title":2019},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fcumulative-layout-shift-fixes-for-static-sites\u002Feliminating-layout-shift-from-web-fonts","Eliminating Layout Shift From Web Fonts",{"path":2021,"title":2022},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fcumulative-layout-shift-fixes-for-static-sites\u002Ffixing-cls-from-late-loading-embeds","Fixing CLS From Late-Loading Embeds",{"path":2024,"title":2025},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fcumulative-layout-shift-fixes-for-static-sites","Cumulative Layout Shift Fixes for Static Sites",{"path":2027,"title":2028},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fcumulative-layout-shift-fixes-for-static-sites\u002Fmeasuring-cls-in-the-field-with-web-vitals-js","Measuring CLS in the Field With web-vitals.js",{"path":2030,"title":2031},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fcumulative-layout-shift-fixes-for-static-sites\u002Freserving-space-for-images-and-embeds-to-stop-layout-shift","Reserving Space for Images and Embeds",{"path":2033,"title":2034},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Ffont-loading-strategies-for-static-sites","Font Loading Strategies for Static Sites",{"path":2036,"title":2037},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Ffont-loading-strategies-for-static-sites\u002Fself-hosting-google-fonts-to-eliminate-layout-shift","Self-Host Google Fonts to Eliminate Layout Shift",{"path":2039,"title":2040},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Ffont-loading-strategies-for-static-sites\u002Fsubsetting-variable-fonts-for-faster-first-render","Subsetting Variable Fonts for Faster First Render",{"path":2042,"title":2043},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fimage-optimization-pipelines-in-astro\u002Fbuilding-an-image-cdn-pipeline-for-static-sites","Building an Image CDN Pipeline for Static Sites",{"path":2045,"title":2046},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fimage-optimization-pipelines-in-astro","Image Optimization Pipelines in Astro",{"path":2048,"title":2049},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fimage-optimization-pipelines-in-astro\u002Foptimizing-webp-images-in-hugo-without-plugins","Optimizing WebP Images in Hugo Without Plugins",{"path":2051,"title":2052},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fimage-optimization-pipelines-in-astro\u002Fserving-avif-with-fallbacks-on-static-sites","Serving AVIF With Fallbacks on Static Sites",{"path":2054,"title":2055},"\u002Fperformance-optimization-core-web-vitals-for-ssgs","Core Web Vitals Optimization for SSGs",{"path":2057,"title":2058},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fjavascript-hydration-partial-rendering\u002Fastro-islands-vs-full-hydration-performance","Astro Islands vs Full Hydration Performance",{"path":2060,"title":2061},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fjavascript-hydration-partial-rendering\u002Fhow-to-reduce-bundle-size-in-eleventy-builds","How to Reduce Bundle Size in Eleventy Builds",{"path":2063,"title":2064},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fjavascript-hydration-partial-rendering","JavaScript Hydration & Partial Rendering",{"path":2066,"title":2067},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fjavascript-hydration-partial-rendering\u002Fmeasuring-inp-on-static-sites-with-real-user-monitoring","Measuring INP on Static Sites with RUM",{"path":2069,"title":2070},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Flargest-contentful-paint-optimization-for-static-sites\u002Feliminating-render-blocking-css-on-static-sites","Eliminating Render-Blocking CSS on Static Sites",{"path":2072,"title":2073},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Flargest-contentful-paint-optimization-for-static-sites","Largest Contentful Paint Optimization for Static Sites",{"path":2075,"title":2076},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Flargest-contentful-paint-optimization-for-static-sites\u002Foptimizing-lcp-on-astro-with-priority-hints","Optimizing LCP on Astro with Priority Hints",{"path":2078,"title":2079},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Flargest-contentful-paint-optimization-for-static-sites\u002Freducing-lcp-from-hero-images-on-static-sites","Reducing LCP from Hero Images on Static Sites",{"path":2081,"title":2082},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fthird-party-script-performance-on-static-sites\u002Fauditing-third-party-scripts-with-lighthouse","Auditing Third-Party Scripts With Lighthouse",{"path":2084,"title":2085},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fthird-party-script-performance-on-static-sites","Third-Party Script Performance on Static Sites",{"path":2087,"title":2088},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fthird-party-script-performance-on-static-sites\u002Flazy-loading-youtube-embeds-on-static-sites","Lazy-Loading YouTube Embeds on Static Sites",{"path":2090,"title":2091},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fthird-party-script-performance-on-static-sites\u002Fself-hosting-analytics-to-cut-third-party-requests","Self-Hosting Analytics to Cut Third-Party Requests",{"path":2093,"title":2094},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fcloudflare-pages-edge-caching-setup\u002Fautomating-eleventy-deployments-with-cloudflare-pages","Automating Eleventy Deployments on Cloudflare Pages",{"path":2096,"title":2097},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fcloudflare-pages-edge-caching-setup\u002Fdeploying-hugo-to-cloudflare-pages-and-workers","Deploying Hugo to Cloudflare Pages and Workers",{"path":2099,"title":2100},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fcloudflare-pages-edge-caching-setup","Cloudflare Pages Edge Caching Setup",{"path":2102,"title":2103},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fcontent-workflows-for-documentation-teams\u002Fdocs-as-code-review-workflow-for-writers","Docs-as-Code Review Workflow for Writers",{"path":2105,"title":2106},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fcontent-workflows-for-documentation-teams","Content Workflows for Documentation Teams",{"path":2108,"title":2109},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fcontent-workflows-for-documentation-teams\u002Fscheduling-content-publication-with-cron-triggered-builds","Scheduling Content Publication With Cron-Triggered Builds",{"path":2111,"title":2112},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fcontent-workflows-for-documentation-teams\u002Fwiring-a-headless-cms-to-a-static-build","Wiring a Headless CMS to a Static Build",{"path":2114,"title":2115},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fgithub-actions-for-automated-ssg-builds\u002Fcaching-node-modules-in-github-actions-for-faster-ssg-builds","Caching node_modules in GitHub Actions",{"path":1921,"title":5},{"path":2118,"title":2119},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fgithub-actions-for-automated-ssg-builds\u002Fhow-to-set-up-github-actions-for-hugo-deployments","GitHub Actions for Hugo Deployments",{"path":2121,"title":29},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fgithub-actions-for-automated-ssg-builds",{"path":2123,"title":2124},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fincremental-builds-and-build-caching-for-ssgs\u002Fcaching-hugo-builds-in-github-actions","Caching Hugo Builds in GitHub Actions",{"path":2126,"title":2127},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fincremental-builds-and-build-caching-for-ssgs\u002Fenabling-incremental-builds-in-eleventy","Enabling Incremental Builds in Eleventy",{"path":2129,"title":2130},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fincremental-builds-and-build-caching-for-ssgs","Incremental Builds and Build Caching for SSGs",{"path":2132,"title":2133},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fincremental-builds-and-build-caching-for-ssgs\u002Fmeasuring-build-time-regressions-in-ci","Measuring Build-Time Regressions in CI",{"path":2135,"title":2136},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fincremental-builds-and-build-caching-for-ssgs\u002Fsharing-build-cache-across-ci-runners","Sharing Build Cache Across CI Runners",{"path":2138,"title":2139},"\u002Fproduction-ready-deployment-cicd-workflows","Production-Ready Deployment & CI\u002FCD for SSGs",{"path":2141,"title":2142},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fnetlify-vs-vercel-deployment-strategies","Netlify vs Vercel Deployment Strategies",{"path":2144,"title":2145},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fnetlify-vs-vercel-deployment-strategies\u002Fnetlify-build-hooks-for-content-updates","Netlify Build Hooks for Content Updates",{"path":2147,"title":2148},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fnetlify-vs-vercel-deployment-strategies\u002Fsetting-up-deploy-previews-on-netlify-for-every-pull-request","Netlify Deploy Previews for Every Pull Request",{"path":2150,"title":2151},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fnetlify-vs-vercel-deployment-strategies\u002Fvercel-isr-vs-static-generation-for-ssgs","Vercel ISR vs Static Generation for SSGs",{"path":2153,"title":2154},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fpreview-environments-for-pull-requests\u002Fautomating-preview-deploy-pipelines-with-github-actions","Automating Preview Deploy Pipelines with GitHub Actions",{"path":2156,"title":1521},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fpreview-environments-for-pull-requests\u002Fcleaning-up-stale-preview-deployments",{"path":2158,"title":1863},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fpreview-environments-for-pull-requests",{"path":2160,"title":2161},"\u002Fproduction-ready-deployment-cicd-workflows\u002Frollbacks-and-deploy-safety-for-static-sites\u002Fatomic-deploys-vs-incremental-uploads","Atomic Deploys vs Incremental Uploads",{"path":2163,"title":1503},"\u002Fproduction-ready-deployment-cicd-workflows\u002Frollbacks-and-deploy-safety-for-static-sites",{"path":2165,"title":2166},"\u002Fproduction-ready-deployment-cicd-workflows\u002Frollbacks-and-deploy-safety-for-static-sites\u002Frolling-back-a-bad-static-deploy-in-under-a-minute","Rolling Back a Bad Static Deploy in Under a Minute",{"path":2168,"title":1875},"\u002Fproduction-ready-deployment-cicd-workflows\u002Frollbacks-and-deploy-safety-for-static-sites\u002Frunning-smoke-tests-against-a-preview-url",1785611671120]