[{"data":1,"prerenderedAt":2179},["ShallowReactive",2],{"page:\u002Fproduction-ready-deployment-cicd-workflows\u002Fpreview-environments-for-pull-requests\u002Fcleaning-up-stale-preview-deployments":3,"all-docs-nav":1934},{"id":4,"title":5,"body":6,"breadcrumb":1905,"dateModified":1915,"datePublished":1915,"description":1916,"extension":1917,"faq":1918,"meta":1928,"navigation":458,"path":1929,"seo":1930,"slug":12,"stem":1931,"type":1932,"__hash__":1933},"content\u002Fproduction-ready-deployment-cicd-workflows\u002Fpreview-environments-for-pull-requests\u002Fcleaning-up-stale-preview-deployments\u002Findex.md","Cleaning Up Stale Preview Deployments",{"type":7,"value":8,"toc":1885},"minimark",[9,13,17,26,31,44,48,168,172,175,185,379,389,393,401,647,953,956,960,963,1404,1407,1543,1547,1554,1557,1563,1621,1624,1628,1631,1708,1711,1715,1718,1724,1727,1731,1777,1781,1789,1793,1798,1801,1805,1808,1812,1821,1825,1831,1835,1838,1842,1881],[10,11,5],"h1",{"id":12},"cleaning-up-stale-preview-deployments",[14,15,16],"p",{},"Per-pull-request previews are one of the best things about static hosting: every change gets a URL, reviewers see the rendered page, and nothing has to be merged to be seen. They also accumulate. A repository with a year of history can have several hundred live preview deployments, each a complete copy of the site at some past moment.",[14,18,19,20,25],{},"Most of the harm is invisible until it is not. Previews get crawled and indexed. They hold whatever configuration was current when they were built, including tokens that may since have been rotated for a reason. And on some hosts they count against storage or deployment quotas. This guide tears them down properly. It is part of ",[21,22,24],"a",{"href":23},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fpreview-environments-for-pull-requests\u002F","Preview Environments for Pull Requests",".",[27,28,30],"h2",{"id":29},"prerequisites","Prerequisites",[32,33,34,38,41],"ul",{},[35,36,37],"li",{},"Preview deployments created per pull request, with a predictable naming scheme.",[35,39,40],{},"API credentials that can list and delete deployments, scoped to the preview environment.",[35,42,43],{},"The ability to set response headers on preview hostnames.",[27,45,47],{"id":46},"what-stale-previews-actually-cost","What Stale Previews Actually Cost",[49,50,51,164],"figure",{},[52,53,60,61,60,65,60,69,60,76],"svg",{"viewBox":54,"role":55,"ariaLabelledBy":56,"xmlns":59},"0 0 760 300","img",[57,58],"cp-cost-title","cp-cost-desc","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","\n  ",[62,63,64],"title",{"id":57},"Four costs of leaving previews live",[66,67,68],"desc",{"id":58},"Four categories. Search: preview URLs get crawled and indexed, creating duplicate content under an unmonitored hostname. Secrets: an old preview holds the configuration current when it was built. Quota: deployments and storage accumulate against host limits. Confusion: a stale preview URL shared in chat outlives the branch and misleads readers.",[70,71],"rect",{"x":72,"y":72,"width":73,"height":74,"fill":75},"0","760","300","#ffffff",[77,78,80,81,80,89,80,100,80,106,80,112,80,118,80,125,80,129,80,133,80,136,80,140,80,144,80,148,80,152,80,155,80,158,80,161,60],"g",{"style":79},"font-family:system-ui, sans-serif;font-size:12px","\n    ",[82,83,88],"text",{"x":84,"y":85,"fill":86,"style":87},"380","28","#1f2937","font-size:16px;font-weight:700;text-anchor:middle","The costs nobody notices until an audit",[70,90],{"x":91,"y":92,"width":93,"height":94,"rx":95,"fill":96,"opacity":97,"stroke":98,"style":99},"40","56","330","96","12","#ff595e","0.12","#d83b41","stroke-width:1.5px",[82,101,105],{"x":102,"y":103,"fill":86,"style":104},"205","82","font-size:13px;font-weight:700;text-anchor:middle","Indexed by search engines",[82,107,111],{"x":108,"y":109,"fill":86,"style":110},"62","110","font-size:12px","duplicate content, unfinished pages",[82,113,117],{"x":108,"y":114,"fill":115,"style":116},"134","#556071","font-size:11px","on a hostname nobody monitors",[70,119],{"x":120,"y":92,"width":121,"height":94,"rx":95,"fill":122,"opacity":123,"stroke":124,"style":99},"400","320","#ffca3a","0.26","#a97b00",[82,126,128],{"x":127,"y":103,"fill":86,"style":104},"560","Frozen configuration",[82,130,132],{"x":131,"y":109,"fill":86,"style":110},"424","endpoints and keys from build time",[82,134,135],{"x":131,"y":114,"fill":115,"style":116},"still live after a rotation",[70,137],{"x":91,"y":138,"width":93,"height":94,"rx":95,"fill":139,"opacity":97,"stroke":139,"style":99},"168","#1982c4",[82,141,143],{"x":102,"y":142,"fill":86,"style":104},"194","Quota and storage",[82,145,147],{"x":108,"y":146,"fill":86,"style":110},"222","hundreds of full site copies",[82,149,151],{"x":108,"y":150,"fill":115,"style":116},"246","deployment limits, slower listings",[70,153],{"x":120,"y":138,"width":121,"height":94,"rx":95,"fill":154,"opacity":97,"stroke":154,"style":99},"#6a4c93",[82,156,157],{"x":127,"y":142,"fill":86,"style":104},"Stale links",[82,159,160],{"x":131,"y":146,"fill":86,"style":110},"a URL shared in chat outlives the branch",[82,162,163],{"x":131,"y":150,"fill":115,"style":116},"readers see a version that never shipped",[165,166,167],"figcaption",{},"The indexing cost is the one that reaches readers. The others are hygiene — but hygiene that a security review will eventually ask about.",[27,169,171],{"id":170},"keep-them-out-of-the-index-from-the-start","Keep Them Out of the Index From the Start",[14,173,174],{},"Cleanup is the second line of defence. The first is making sure a preview cannot be indexed while it lives, and that has to happen at the host or edge layer so it covers assets and error pages too:",[176,177,182],"pre",{"className":178,"code":180,"language":82,"meta":181},[179],"language-text","# public\u002F_headers — applied only on preview deployments\nhttps:\u002F\u002F:project.preview.example.com\u002F*\n  X-Robots-Tag: noindex, nofollow\n  Cache-Control: no-store\n","",[183,184,180],"code",{"__ignoreMap":181},[176,186,190],{"className":187,"code":188,"language":189,"meta":181,"style":181},"language-javascript shiki shiki-themes github-light github-dark","\u002F\u002F Or at the edge, for hosts without a headers file\nexport default {\n  async fetch(request, env) {\n    const res = await env.ASSETS.fetch(request);\n    if (new URL(request.url).hostname.endsWith('.preview.example.com')) {\n      const out = new Response(res.body, res);\n      out.headers.set('x-robots-tag', 'noindex, nofollow');\n      return out;\n    }\n    return res;\n  },\n};\n","javascript",[183,191,192,201,215,241,271,301,321,343,352,358,367,373],{"__ignoreMap":181},[193,194,197],"span",{"class":195,"line":196},"line",1,[193,198,200],{"class":199},"sJ8bj","\u002F\u002F Or at the edge, for hosts without a headers file\n",[193,202,204,208,211],{"class":195,"line":203},2,[193,205,207],{"class":206},"szBVR","export",[193,209,210],{"class":206}," default",[193,212,214],{"class":213},"sVt8B"," {\n",[193,216,218,221,225,228,232,235,238],{"class":195,"line":217},3,[193,219,220],{"class":206},"  async",[193,222,224],{"class":223},"sScJk"," fetch",[193,226,227],{"class":213},"(",[193,229,231],{"class":230},"s4XuR","request",[193,233,234],{"class":213},", ",[193,236,237],{"class":230},"env",[193,239,240],{"class":213},") {\n",[193,242,244,247,251,254,257,260,263,265,268],{"class":195,"line":243},4,[193,245,246],{"class":206},"    const",[193,248,250],{"class":249},"sj4cs"," res",[193,252,253],{"class":206}," =",[193,255,256],{"class":206}," await",[193,258,259],{"class":213}," env.",[193,261,262],{"class":249},"ASSETS",[193,264,25],{"class":213},[193,266,267],{"class":223},"fetch",[193,269,270],{"class":213},"(request);\n",[193,272,274,277,280,283,286,289,292,294,298],{"class":195,"line":273},5,[193,275,276],{"class":206},"    if",[193,278,279],{"class":213}," (",[193,281,282],{"class":206},"new",[193,284,285],{"class":223}," URL",[193,287,288],{"class":213},"(request.url).hostname.",[193,290,291],{"class":223},"endsWith",[193,293,227],{"class":213},[193,295,297],{"class":296},"sZZnC","'.preview.example.com'",[193,299,300],{"class":213},")) {\n",[193,302,304,307,310,312,315,318],{"class":195,"line":303},6,[193,305,306],{"class":206},"      const",[193,308,309],{"class":249}," out",[193,311,253],{"class":206},[193,313,314],{"class":206}," new",[193,316,317],{"class":223}," Response",[193,319,320],{"class":213},"(res.body, res);\n",[193,322,324,327,330,332,335,337,340],{"class":195,"line":323},7,[193,325,326],{"class":213},"      out.headers.",[193,328,329],{"class":223},"set",[193,331,227],{"class":213},[193,333,334],{"class":296},"'x-robots-tag'",[193,336,234],{"class":213},[193,338,339],{"class":296},"'noindex, nofollow'",[193,341,342],{"class":213},");\n",[193,344,346,349],{"class":195,"line":345},8,[193,347,348],{"class":206},"      return",[193,350,351],{"class":213}," out;\n",[193,353,355],{"class":195,"line":354},9,[193,356,357],{"class":213},"    }\n",[193,359,361,364],{"class":195,"line":360},10,[193,362,363],{"class":206},"    return",[193,365,366],{"class":213}," res;\n",[193,368,370],{"class":195,"line":369},11,[193,371,372],{"class":213},"  },\n",[193,374,376],{"class":195,"line":375},12,[193,377,378],{"class":213},"};\n",[14,380,381,382,385,386,388],{},"Serve a blanket-disallow ",[183,383,384],{},"robots.txt"," on preview hostnames as well. The header is what actually keeps a page out of the index — ",[183,387,384],{}," only prevents crawling, and a URL linked from elsewhere can still be indexed without being crawled — so use both.",[27,390,392],{"id":391},"delete-on-close-not-only-on-merge","Delete on Close, Not Only on Merge",[14,394,395,396,400],{},"The teardown trigger should be the pull request ",[397,398,399],"em",{},"closing",", whether it was merged or abandoned. Abandoned branches are the larger population on most repositories.",[176,402,406],{"className":403,"code":404,"language":405,"meta":181,"style":181},"language-yaml shiki shiki-themes github-light github-dark","# .github\u002Fworkflows\u002Fpreview-cleanup.yml\nname: Tear down preview\non:\n  pull_request:\n    types: [closed]\n\njobs:\n  teardown:\n    runs-on: ubuntu-latest\n    environment: { name: preview }\n    steps:\n      - uses: actions\u002Fcheckout@v4\n      - name: Delete the preview deployment\n        run: node scripts\u002Fdelete-preview.mjs \"pr-${{ github.event.number }}\"\n        env:\n          CF_API_TOKEN: ${{ secrets.CF_TOKEN }}\n          CF_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }}\n      - name: Note it on the pull request\n        uses: actions\u002Fgithub-script@v7\n        with:\n          script: |\n            github.rest.issues.createComment({\n              issue_number: context.issue.number,\n              owner: context.repo.owner, repo: context.repo.repo,\n              body: 'Preview deployment removed.'\n            })\n","yaml",[183,407,408,413,425,433,440,454,460,467,474,484,502,509,522,534,545,553,564,575,587,598,606,617,623,629,635,641],{"__ignoreMap":181},[193,409,410],{"class":195,"line":196},[193,411,412],{"class":199},"# .github\u002Fworkflows\u002Fpreview-cleanup.yml\n",[193,414,415,419,422],{"class":195,"line":203},[193,416,418],{"class":417},"s9eBZ","name",[193,420,421],{"class":213},": ",[193,423,424],{"class":296},"Tear down preview\n",[193,426,427,430],{"class":195,"line":217},[193,428,429],{"class":249},"on",[193,431,432],{"class":213},":\n",[193,434,435,438],{"class":195,"line":243},[193,436,437],{"class":417},"  pull_request",[193,439,432],{"class":213},[193,441,442,445,448,451],{"class":195,"line":273},[193,443,444],{"class":417},"    types",[193,446,447],{"class":213},": [",[193,449,450],{"class":296},"closed",[193,452,453],{"class":213},"]\n",[193,455,456],{"class":195,"line":303},[193,457,459],{"emptyLinePlaceholder":458},true,"\n",[193,461,462,465],{"class":195,"line":323},[193,463,464],{"class":417},"jobs",[193,466,432],{"class":213},[193,468,469,472],{"class":195,"line":345},[193,470,471],{"class":417},"  teardown",[193,473,432],{"class":213},[193,475,476,479,481],{"class":195,"line":354},[193,477,478],{"class":417},"    runs-on",[193,480,421],{"class":213},[193,482,483],{"class":296},"ubuntu-latest\n",[193,485,486,489,492,494,496,499],{"class":195,"line":360},[193,487,488],{"class":417},"    environment",[193,490,491],{"class":213},": { ",[193,493,418],{"class":417},[193,495,421],{"class":213},[193,497,498],{"class":296},"preview",[193,500,501],{"class":213}," }\n",[193,503,504,507],{"class":195,"line":369},[193,505,506],{"class":417},"    steps",[193,508,432],{"class":213},[193,510,511,514,517,519],{"class":195,"line":375},[193,512,513],{"class":213},"      - ",[193,515,516],{"class":417},"uses",[193,518,421],{"class":213},[193,520,521],{"class":296},"actions\u002Fcheckout@v4\n",[193,523,525,527,529,531],{"class":195,"line":524},13,[193,526,513],{"class":213},[193,528,418],{"class":417},[193,530,421],{"class":213},[193,532,533],{"class":296},"Delete the preview deployment\n",[193,535,537,540,542],{"class":195,"line":536},14,[193,538,539],{"class":417},"        run",[193,541,421],{"class":213},[193,543,544],{"class":296},"node scripts\u002Fdelete-preview.mjs \"pr-${{ github.event.number }}\"\n",[193,546,548,551],{"class":195,"line":547},15,[193,549,550],{"class":417},"        env",[193,552,432],{"class":213},[193,554,556,559,561],{"class":195,"line":555},16,[193,557,558],{"class":417},"          CF_API_TOKEN",[193,560,421],{"class":213},[193,562,563],{"class":296},"${{ secrets.CF_TOKEN }}\n",[193,565,567,570,572],{"class":195,"line":566},17,[193,568,569],{"class":417},"          CF_ACCOUNT_ID",[193,571,421],{"class":213},[193,573,574],{"class":296},"${{ secrets.CF_ACCOUNT_ID }}\n",[193,576,578,580,582,584],{"class":195,"line":577},18,[193,579,513],{"class":213},[193,581,418],{"class":417},[193,583,421],{"class":213},[193,585,586],{"class":296},"Note it on the pull request\n",[193,588,590,593,595],{"class":195,"line":589},19,[193,591,592],{"class":417},"        uses",[193,594,421],{"class":213},[193,596,597],{"class":296},"actions\u002Fgithub-script@v7\n",[193,599,601,604],{"class":195,"line":600},20,[193,602,603],{"class":417},"        with",[193,605,432],{"class":213},[193,607,609,612,614],{"class":195,"line":608},21,[193,610,611],{"class":417},"          script",[193,613,421],{"class":213},[193,615,616],{"class":206},"|\n",[193,618,620],{"class":195,"line":619},22,[193,621,622],{"class":296},"            github.rest.issues.createComment({\n",[193,624,626],{"class":195,"line":625},23,[193,627,628],{"class":296},"              issue_number: context.issue.number,\n",[193,630,632],{"class":195,"line":631},24,[193,633,634],{"class":296},"              owner: context.repo.owner, repo: context.repo.repo,\n",[193,636,638],{"class":195,"line":637},25,[193,639,640],{"class":296},"              body: 'Preview deployment removed.'\n",[193,642,644],{"class":195,"line":643},26,[193,645,646],{"class":296},"            })\n",[176,648,650],{"className":187,"code":649,"language":189,"meta":181,"style":181},"\u002F\u002F scripts\u002Fdelete-preview.mjs \u003Calias>\nconst alias = process.argv[2];\nconst base = `https:\u002F\u002Fapi.cloudflare.com\u002Fclient\u002Fv4\u002Faccounts\u002F${process.env.CF_ACCOUNT_ID}\u002Fpages\u002Fprojects\u002Fdocs`;\nconst headers = { authorization: `Bearer ${process.env.CF_API_TOKEN}` };\n\nconst list = await (await fetch(`${base}\u002Fdeployments?env=preview`, { headers })).json();\nconst targets = (list.result ?? []).filter((d) => d.deployment_trigger?.metadata?.branch === alias);\n\nfor (const d of targets) {\n  const res = await fetch(`${base}\u002Fdeployments\u002F${d.id}?force=true`, { method: 'DELETE', headers });\n  console.log(`${d.id} → ${res.status}`);\n}\nconsole.log(`deleted ${targets.length} deployment(s) for ${alias}`);\n",[183,651,652,657,676,706,738,742,780,822,826,844,885,918,923],{"__ignoreMap":181},[193,653,654],{"class":195,"line":196},[193,655,656],{"class":199},"\u002F\u002F scripts\u002Fdelete-preview.mjs \u003Calias>\n",[193,658,659,662,665,667,670,673],{"class":195,"line":203},[193,660,661],{"class":206},"const",[193,663,664],{"class":249}," alias",[193,666,253],{"class":206},[193,668,669],{"class":213}," process.argv[",[193,671,672],{"class":249},"2",[193,674,675],{"class":213},"];\n",[193,677,678,680,683,685,688,691,693,695,697,700,703],{"class":195,"line":217},[193,679,661],{"class":206},[193,681,682],{"class":249}," base",[193,684,253],{"class":206},[193,686,687],{"class":296}," `https:\u002F\u002Fapi.cloudflare.com\u002Fclient\u002Fv4\u002Faccounts\u002F${",[193,689,690],{"class":213},"process",[193,692,25],{"class":296},[193,694,237],{"class":213},[193,696,25],{"class":296},[193,698,699],{"class":249},"CF_ACCOUNT_ID",[193,701,702],{"class":296},"}\u002Fpages\u002Fprojects\u002Fdocs`",[193,704,705],{"class":213},";\n",[193,707,708,710,713,715,718,721,723,725,727,729,732,735],{"class":195,"line":243},[193,709,661],{"class":206},[193,711,712],{"class":249}," headers",[193,714,253],{"class":206},[193,716,717],{"class":213}," { authorization: ",[193,719,720],{"class":296},"`Bearer ${",[193,722,690],{"class":213},[193,724,25],{"class":296},[193,726,237],{"class":213},[193,728,25],{"class":296},[193,730,731],{"class":249},"CF_API_TOKEN",[193,733,734],{"class":296},"}`",[193,736,737],{"class":213}," };\n",[193,739,740],{"class":195,"line":273},[193,741,459],{"emptyLinePlaceholder":458},[193,743,744,746,749,751,753,755,758,760,762,765,768,771,774,777],{"class":195,"line":303},[193,745,661],{"class":206},[193,747,748],{"class":249}," list",[193,750,253],{"class":206},[193,752,256],{"class":206},[193,754,279],{"class":213},[193,756,757],{"class":206},"await",[193,759,224],{"class":223},[193,761,227],{"class":213},[193,763,764],{"class":296},"`${",[193,766,767],{"class":213},"base",[193,769,770],{"class":296},"}\u002Fdeployments?env=preview`",[193,772,773],{"class":213},", { headers })).",[193,775,776],{"class":223},"json",[193,778,779],{"class":213},"();\n",[193,781,782,784,787,789,792,795,798,801,804,807,810,813,816,819],{"class":195,"line":323},[193,783,661],{"class":206},[193,785,786],{"class":249}," targets",[193,788,253],{"class":206},[193,790,791],{"class":213}," (list.result ",[193,793,794],{"class":206},"??",[193,796,797],{"class":213}," []).",[193,799,800],{"class":223},"filter",[193,802,803],{"class":213},"((",[193,805,806],{"class":230},"d",[193,808,809],{"class":213},") ",[193,811,812],{"class":206},"=>",[193,814,815],{"class":213}," d.deployment_trigger?.metadata?.branch ",[193,817,818],{"class":206},"===",[193,820,821],{"class":213}," alias);\n",[193,823,824],{"class":195,"line":345},[193,825,459],{"emptyLinePlaceholder":458},[193,827,828,831,833,835,838,841],{"class":195,"line":354},[193,829,830],{"class":206},"for",[193,832,279],{"class":213},[193,834,661],{"class":206},[193,836,837],{"class":249}," d",[193,839,840],{"class":206}," of",[193,842,843],{"class":213}," targets) {\n",[193,845,846,849,851,853,855,857,859,861,863,866,868,870,873,876,879,882],{"class":195,"line":360},[193,847,848],{"class":206},"  const",[193,850,250],{"class":249},[193,852,253],{"class":206},[193,854,256],{"class":206},[193,856,224],{"class":223},[193,858,227],{"class":213},[193,860,764],{"class":296},[193,862,767],{"class":213},[193,864,865],{"class":296},"}\u002Fdeployments\u002F${",[193,867,806],{"class":213},[193,869,25],{"class":296},[193,871,872],{"class":213},"id",[193,874,875],{"class":296},"}?force=true`",[193,877,878],{"class":213},", { method: ",[193,880,881],{"class":296},"'DELETE'",[193,883,884],{"class":213},", headers });\n",[193,886,887,890,893,895,897,899,901,903,906,909,911,914,916],{"class":195,"line":369},[193,888,889],{"class":213},"  console.",[193,891,892],{"class":223},"log",[193,894,227],{"class":213},[193,896,764],{"class":296},[193,898,806],{"class":213},[193,900,25],{"class":296},[193,902,872],{"class":213},[193,904,905],{"class":296},"} → ${",[193,907,908],{"class":213},"res",[193,910,25],{"class":296},[193,912,913],{"class":213},"status",[193,915,734],{"class":296},[193,917,342],{"class":213},[193,919,920],{"class":195,"line":375},[193,921,922],{"class":213},"}\n",[193,924,925,928,930,932,935,938,940,943,946,949,951],{"class":195,"line":524},[193,926,927],{"class":213},"console.",[193,929,892],{"class":223},[193,931,227],{"class":213},[193,933,934],{"class":296},"`deleted ${",[193,936,937],{"class":213},"targets",[193,939,25],{"class":296},[193,941,942],{"class":249},"length",[193,944,945],{"class":296},"} deployment(s) for ${",[193,947,948],{"class":213},"alias",[193,950,734],{"class":296},[193,952,342],{"class":213},[14,954,955],{},"The comment on the pull request is not decoration: it tells anyone returning to an old thread that the link they are about to click is gone deliberately.",[27,957,959],{"id":958},"sweep-the-stragglers-on-a-schedule","Sweep the Stragglers on a Schedule",[14,961,962],{},"Teardown fails sometimes — a rate limit, an API outage, a pull request closed while CI was disabled. A weekly sweep catches whatever the event-driven path missed:",[176,964,966],{"className":187,"code":965,"language":189,"meta":181,"style":181},"\u002F\u002F scripts\u002Fsweep-previews.mjs — delete previews older than 14 days with no open PR\nconst base = `https:\u002F\u002Fapi.cloudflare.com\u002Fclient\u002Fv4\u002Faccounts\u002F${process.env.CF_ACCOUNT_ID}\u002Fpages\u002Fprojects\u002Fdocs`;\nconst headers = { authorization: `Bearer ${process.env.CF_API_TOKEN}` };\nconst CUTOFF_DAYS = 14;\n\nconst openPrs = await (await fetch(\n  'https:\u002F\u002Fapi.github.com\u002Frepos\u002Facme\u002Fdocs\u002Fpulls?state=open&per_page=100',\n  { headers: { authorization: `Bearer ${process.env.GITHUB_TOKEN}` } })).json();\nconst openBranches = new Set(openPrs.map((pr) => `pr-${pr.number}`));\n\nconst deployments = (await (await fetch(`${base}\u002Fdeployments?env=preview`, { headers })).json()).result ?? [];\nconst cutoffMs = CUTOFF_DAYS * 86400_000;\nlet deleted = 0;\n\nfor (const d of deployments) {\n  const branch = d.deployment_trigger?.metadata?.branch ?? '';\n  const ageMs = Date.now() - new Date(d.created_on).getTime();\n  if (openBranches.has(branch)) continue;          \u002F\u002F still under review\n  if (ageMs \u003C cutoffMs) continue;                  \u002F\u002F recently closed, keep briefly\n  await fetch(`${base}\u002Fdeployments\u002F${d.id}?force=true`, { method: 'DELETE', headers });\n  deleted++;\n}\nconsole.log(`swept ${deleted} stale preview(s) of ${deployments.length}`);\n",[183,967,968,973,997,1023,1037,1041,1061,1069,1096,1140,1144,1183,1202,1218,1222,1237,1255,1289,1312,1333,1362,1372,1376],{"__ignoreMap":181},[193,969,970],{"class":195,"line":196},[193,971,972],{"class":199},"\u002F\u002F scripts\u002Fsweep-previews.mjs — delete previews older than 14 days with no open PR\n",[193,974,975,977,979,981,983,985,987,989,991,993,995],{"class":195,"line":203},[193,976,661],{"class":206},[193,978,682],{"class":249},[193,980,253],{"class":206},[193,982,687],{"class":296},[193,984,690],{"class":213},[193,986,25],{"class":296},[193,988,237],{"class":213},[193,990,25],{"class":296},[193,992,699],{"class":249},[193,994,702],{"class":296},[193,996,705],{"class":213},[193,998,999,1001,1003,1005,1007,1009,1011,1013,1015,1017,1019,1021],{"class":195,"line":217},[193,1000,661],{"class":206},[193,1002,712],{"class":249},[193,1004,253],{"class":206},[193,1006,717],{"class":213},[193,1008,720],{"class":296},[193,1010,690],{"class":213},[193,1012,25],{"class":296},[193,1014,237],{"class":213},[193,1016,25],{"class":296},[193,1018,731],{"class":249},[193,1020,734],{"class":296},[193,1022,737],{"class":213},[193,1024,1025,1027,1030,1032,1035],{"class":195,"line":243},[193,1026,661],{"class":206},[193,1028,1029],{"class":249}," CUTOFF_DAYS",[193,1031,253],{"class":206},[193,1033,1034],{"class":249}," 14",[193,1036,705],{"class":213},[193,1038,1039],{"class":195,"line":273},[193,1040,459],{"emptyLinePlaceholder":458},[193,1042,1043,1045,1048,1050,1052,1054,1056,1058],{"class":195,"line":303},[193,1044,661],{"class":206},[193,1046,1047],{"class":249}," openPrs",[193,1049,253],{"class":206},[193,1051,256],{"class":206},[193,1053,279],{"class":213},[193,1055,757],{"class":206},[193,1057,224],{"class":223},[193,1059,1060],{"class":213},"(\n",[193,1062,1063,1066],{"class":195,"line":323},[193,1064,1065],{"class":296},"  'https:\u002F\u002Fapi.github.com\u002Frepos\u002Facme\u002Fdocs\u002Fpulls?state=open&per_page=100'",[193,1067,1068],{"class":213},",\n",[193,1070,1071,1074,1076,1078,1080,1082,1084,1087,1089,1092,1094],{"class":195,"line":345},[193,1072,1073],{"class":213},"  { headers: { authorization: ",[193,1075,720],{"class":296},[193,1077,690],{"class":213},[193,1079,25],{"class":296},[193,1081,237],{"class":213},[193,1083,25],{"class":296},[193,1085,1086],{"class":249},"GITHUB_TOKEN",[193,1088,734],{"class":296},[193,1090,1091],{"class":213}," } })).",[193,1093,776],{"class":223},[193,1095,779],{"class":213},[193,1097,1098,1100,1103,1105,1107,1110,1113,1116,1118,1121,1123,1125,1128,1130,1132,1135,1137],{"class":195,"line":354},[193,1099,661],{"class":206},[193,1101,1102],{"class":249}," openBranches",[193,1104,253],{"class":206},[193,1106,314],{"class":206},[193,1108,1109],{"class":223}," Set",[193,1111,1112],{"class":213},"(openPrs.",[193,1114,1115],{"class":223},"map",[193,1117,803],{"class":213},[193,1119,1120],{"class":230},"pr",[193,1122,809],{"class":213},[193,1124,812],{"class":206},[193,1126,1127],{"class":296}," `pr-${",[193,1129,1120],{"class":213},[193,1131,25],{"class":296},[193,1133,1134],{"class":213},"number",[193,1136,734],{"class":296},[193,1138,1139],{"class":213},"));\n",[193,1141,1142],{"class":195,"line":360},[193,1143,459],{"emptyLinePlaceholder":458},[193,1145,1146,1148,1151,1153,1155,1157,1159,1161,1163,1165,1167,1169,1171,1173,1175,1178,1180],{"class":195,"line":369},[193,1147,661],{"class":206},[193,1149,1150],{"class":249}," deployments",[193,1152,253],{"class":206},[193,1154,279],{"class":213},[193,1156,757],{"class":206},[193,1158,279],{"class":213},[193,1160,757],{"class":206},[193,1162,224],{"class":223},[193,1164,227],{"class":213},[193,1166,764],{"class":296},[193,1168,767],{"class":213},[193,1170,770],{"class":296},[193,1172,773],{"class":213},[193,1174,776],{"class":223},[193,1176,1177],{"class":213},"()).result ",[193,1179,794],{"class":206},[193,1181,1182],{"class":213}," [];\n",[193,1184,1185,1187,1190,1192,1194,1197,1200],{"class":195,"line":375},[193,1186,661],{"class":206},[193,1188,1189],{"class":249}," cutoffMs",[193,1191,253],{"class":206},[193,1193,1029],{"class":249},[193,1195,1196],{"class":206}," *",[193,1198,1199],{"class":249}," 86400_000",[193,1201,705],{"class":213},[193,1203,1204,1207,1210,1213,1216],{"class":195,"line":524},[193,1205,1206],{"class":206},"let",[193,1208,1209],{"class":213}," deleted ",[193,1211,1212],{"class":206},"=",[193,1214,1215],{"class":249}," 0",[193,1217,705],{"class":213},[193,1219,1220],{"class":195,"line":536},[193,1221,459],{"emptyLinePlaceholder":458},[193,1223,1224,1226,1228,1230,1232,1234],{"class":195,"line":547},[193,1225,830],{"class":206},[193,1227,279],{"class":213},[193,1229,661],{"class":206},[193,1231,837],{"class":249},[193,1233,840],{"class":206},[193,1235,1236],{"class":213}," deployments) {\n",[193,1238,1239,1241,1244,1246,1248,1250,1253],{"class":195,"line":555},[193,1240,848],{"class":206},[193,1242,1243],{"class":249}," branch",[193,1245,253],{"class":206},[193,1247,815],{"class":213},[193,1249,794],{"class":206},[193,1251,1252],{"class":296}," ''",[193,1254,705],{"class":213},[193,1256,1257,1259,1262,1264,1267,1270,1273,1276,1278,1281,1284,1287],{"class":195,"line":566},[193,1258,848],{"class":206},[193,1260,1261],{"class":249}," ageMs",[193,1263,253],{"class":206},[193,1265,1266],{"class":213}," Date.",[193,1268,1269],{"class":223},"now",[193,1271,1272],{"class":213},"() ",[193,1274,1275],{"class":206},"-",[193,1277,314],{"class":206},[193,1279,1280],{"class":223}," Date",[193,1282,1283],{"class":213},"(d.created_on).",[193,1285,1286],{"class":223},"getTime",[193,1288,779],{"class":213},[193,1290,1291,1294,1297,1300,1303,1306,1309],{"class":195,"line":577},[193,1292,1293],{"class":206},"  if",[193,1295,1296],{"class":213}," (openBranches.",[193,1298,1299],{"class":223},"has",[193,1301,1302],{"class":213},"(branch)) ",[193,1304,1305],{"class":206},"continue",[193,1307,1308],{"class":213},";          ",[193,1310,1311],{"class":199},"\u002F\u002F still under review\n",[193,1313,1314,1316,1319,1322,1325,1327,1330],{"class":195,"line":589},[193,1315,1293],{"class":206},[193,1317,1318],{"class":213}," (ageMs ",[193,1320,1321],{"class":206},"\u003C",[193,1323,1324],{"class":213}," cutoffMs) ",[193,1326,1305],{"class":206},[193,1328,1329],{"class":213},";                  ",[193,1331,1332],{"class":199},"\u002F\u002F recently closed, keep briefly\n",[193,1334,1335,1338,1340,1342,1344,1346,1348,1350,1352,1354,1356,1358,1360],{"class":195,"line":600},[193,1336,1337],{"class":206},"  await",[193,1339,224],{"class":223},[193,1341,227],{"class":213},[193,1343,764],{"class":296},[193,1345,767],{"class":213},[193,1347,865],{"class":296},[193,1349,806],{"class":213},[193,1351,25],{"class":296},[193,1353,872],{"class":213},[193,1355,875],{"class":296},[193,1357,878],{"class":213},[193,1359,881],{"class":296},[193,1361,884],{"class":213},[193,1363,1364,1367,1370],{"class":195,"line":608},[193,1365,1366],{"class":213},"  deleted",[193,1368,1369],{"class":206},"++",[193,1371,705],{"class":213},[193,1373,1374],{"class":195,"line":619},[193,1375,922],{"class":213},[193,1377,1378,1380,1382,1384,1387,1390,1393,1396,1398,1400,1402],{"class":195,"line":625},[193,1379,927],{"class":213},[193,1381,892],{"class":223},[193,1383,227],{"class":213},[193,1385,1386],{"class":296},"`swept ${",[193,1388,1389],{"class":213},"deleted",[193,1391,1392],{"class":296},"} stale preview(s) of ${",[193,1394,1395],{"class":213},"deployments",[193,1397,25],{"class":296},[193,1399,942],{"class":249},[193,1401,734],{"class":296},[193,1403,342],{"class":213},[14,1405,1406],{},"Two safety properties matter here. The sweep never deletes a preview whose pull request is open, no matter how old it is — long-running branches are a legitimate case. And it keeps recently closed previews for a fortnight, because a reviewer often revisits a link shortly after merge.",[49,1408,1409,1540],{},[52,1410,60,1415,60,1418,60,1421,60,1424,60,1525],{"viewBox":1411,"role":55,"ariaLabelledBy":1412,"xmlns":59},"0 0 760 290",[1413,1414],"cp-lifecycle-title","cp-lifecycle-desc",[62,1416,1417],{"id":1413},"Preview lifecycle with event teardown and a scheduled sweep",[66,1419,1420],{"id":1414},"A preview is created when a pull request opens and updated on each push. When the pull request closes, an event-driven teardown deletes it. A weekly sweep deletes anything older than fourteen days whose pull request is no longer open, catching teardowns that failed.",[70,1422],{"x":72,"y":72,"width":73,"height":1423,"fill":75},"290",[77,1425,80,1426,80,1429,80,1438,80,1444,80,1449,80,1453,80,1457,80,1460,80,1463,80,1467,80,1470,80,1476,80,1481,80,1497,80,1501,80,1505,80,1509,80,1513,80,1521,60],{"style":79},[82,1427,1428],{"x":84,"y":85,"fill":86,"style":87},"Two mechanisms, because one of them will fail",[70,1430],{"x":91,"y":1431,"width":1432,"height":1433,"rx":1434,"fill":1435,"opacity":1436,"stroke":1437,"style":99},"60","150","52","10","#8ac926","0.18","#5a8a16",[82,1439,1443],{"x":1440,"y":1441,"fill":86,"style":1442},"115","84","font-size:12px;font-weight:700;text-anchor:middle","PR opened",[82,1445,1448],{"x":1440,"y":1446,"fill":115,"style":1447},"102","font-size:11px;text-anchor:middle","preview created",[70,1450],{"x":1451,"y":1431,"width":1432,"height":1433,"rx":1434,"fill":139,"opacity":1452,"stroke":139,"style":99},"220","0.14",[82,1454,1456],{"x":1455,"y":1441,"fill":86,"style":1442},"295","each push",[82,1458,1459],{"x":1455,"y":1446,"fill":115,"style":1447},"preview updated",[70,1461],{"x":120,"y":1431,"width":1432,"height":1433,"rx":1434,"fill":122,"opacity":1462,"stroke":124,"style":99},"0.3",[82,1464,1466],{"x":1465,"y":1441,"fill":86,"style":1442},"475","PR closed",[82,1468,1469],{"x":1465,"y":1446,"fill":115,"style":1447},"teardown fires",[70,1471],{"x":1472,"y":1431,"width":1473,"height":1433,"rx":1434,"fill":1474,"opacity":1475,"stroke":115,"style":99},"580","140","#d9e2ef","0.55",[82,1477,1480],{"x":1478,"y":1479,"fill":86,"style":1442},"650","90","gone",[77,1482,1485,1486,1485,1491,1485,1494,80],{"stroke":115,"fill":1483,"style":1484},"none","stroke-width:2px","\n      ",[1487,1488],"path",{"d":1489,"style":1490},"M192 86 L218 86","marker-end:url(#cp-arrow)",[1487,1492],{"d":1493,"style":1490},"M372 86 L398 86",[1487,1495],{"d":1496,"style":1490},"M552 86 L578 86",[70,1498],{"x":120,"y":1499,"width":121,"height":1500,"rx":1434,"fill":154,"opacity":97,"stroke":154,"style":99},"160","76",[82,1502,1504],{"x":127,"y":1503,"fill":86,"style":1442},"186","weekly sweep",[82,1506,1508],{"x":127,"y":1507,"fill":115,"style":1447},"206","older than 14 days · no open PR",[82,1510,1512],{"x":127,"y":1511,"fill":115,"style":1447},"224","catches every failed teardown",[77,1514,1485,1515,1485,1518,80],{"stroke":115,"fill":1483,"style":1484},[1487,1516],{"d":1517,"style":1490},"M475 114 L520 158",[1487,1519],{"d":1520,"style":1490},"M700 158 L660 116",[82,1522,1524],{"x":91,"y":1523,"fill":115,"style":116},"264","The sweep never touches a preview whose pull request is still open, however old the branch is",[1526,1527,80,1528,60],"defs",{},[1529,1530,1485,1537,80],"marker",{"id":1531,"viewBox":1532,"refX":1533,"refY":1534,"markerWidth":1535,"markerHeight":1535,"orient":1536},"cp-arrow","0 0 10 10","8","5","7","auto-start-reverse",[1487,1538],{"d":1539,"fill":115},"M0 0 L10 5 L0 10 z",[165,1541,1542],{},"Event-driven teardown handles the normal case in seconds; the sweep exists because the normal case is not the only case.",[27,1544,1546],{"id":1545},"access-control-for-unreleased-content","Access Control for Unreleased Content",[14,1548,1549,1550,1553],{},"For public documentation, ",[183,1551,1552],{},"noindex"," plus an unguessable hostname is proportionate. For content under embargo — a release announcement, a pricing change, an unannounced feature — a preview URL is a link that will eventually be pasted somewhere it should not be.",[14,1555,1556],{},"An access rule at the edge costs one configuration entry and removes the whole category of risk:",[176,1558,1561],{"className":1559,"code":1560,"language":82,"meta":181},[179],"Application:  *.preview.example.com\nPolicy:       Allow — emails ending @example.com\nSession:      24 hours\n",[183,1562,1560],{"__ignoreMap":181},[49,1564,1565,1618],{},[52,1566,60,1571,60,1574,60,1577,60,1580],{"viewBox":1567,"role":55,"ariaLabelledBy":1568,"xmlns":59},"0 0 760 280",[1569,1570],"cp-access-title","cp-access-desc",[62,1572,1573],{"id":1569},"Three levels of preview protection",[66,1575,1576],{"id":1570},"Three tiers. Public documentation previews use noindex and an unguessable hostname. Embargoed content adds an identity-based access rule at the edge. Regulated content adds an audit log of who viewed which preview. Each tier is a superset of the one before.",[70,1578],{"x":72,"y":72,"width":73,"height":1579,"fill":75},"280",[77,1581,80,1582,80,1585,80,1589,80,1593,80,1596,80,1599,80,1603,80,1607,80,1610,80,1614,60],{"style":79},[82,1583,1584],{"x":84,"y":85,"fill":86,"style":87},"Match the protection to what is on the page",[70,1586],{"x":91,"y":92,"width":1587,"height":1431,"rx":1434,"fill":1435,"opacity":1588,"stroke":1437,"style":99},"680","0.16",[82,1590,1592],{"x":84,"y":1591,"fill":86,"style":104},"80","Public documentation — noindex header + unguessable hostname",[82,1594,1595],{"x":84,"y":1446,"fill":115,"style":1447},"no login, no friction for reviewers, nothing sensitive at stake",[70,1597],{"x":91,"y":1598,"width":1587,"height":1431,"rx":1434,"fill":122,"opacity":123,"stroke":124,"style":99},"124",[82,1600,1602],{"x":84,"y":1601,"fill":86,"style":104},"148","Embargoed content — add an identity policy at the edge",[82,1604,1606],{"x":84,"y":1605,"fill":115,"style":1447},"170","a pasted link is useless to anyone outside the organisation",[70,1608],{"x":91,"y":1609,"width":1587,"height":1431,"rx":1434,"fill":154,"opacity":1452,"stroke":154,"style":99},"192",[82,1611,1613],{"x":84,"y":1612,"fill":86,"style":104},"216","Regulated content — add an access log",[82,1615,1617],{"x":84,"y":1616,"fill":115,"style":1447},"238","who viewed which preview, retained alongside the deployment record",[165,1619,1620],{},"Each tier is cheap and additive. The mistake is applying the third tier to a public docs site, which adds a login step to every review for no benefit.",[14,1622,1623],{},"The tier also changes retention. An embargoed preview should be deleted the day the pull request closes rather than kept for a fortnight, because the link has already been shared with whoever needed it.",[27,1625,1627],{"id":1626},"measured-impact","Measured Impact",[14,1629,1630],{},"A documentation repository with two years of history, before and after introducing teardown and a weekly sweep:",[1632,1633,1634,1650],"table",{},[1635,1636,1637],"thead",{},[1638,1639,1640,1644,1647],"tr",{},[1641,1642,1643],"th",{},"Measure",[1641,1645,1646],{},"Before",[1641,1648,1649],{},"After",[1651,1652,1653,1665,1676,1687,1698],"tbody",{},[1638,1654,1655,1659,1662],{},[1656,1657,1658],"td",{},"Live preview deployments",[1656,1660,1661],{},"412",[1656,1663,1664],{},"6 (open PRs only)",[1638,1666,1667,1670,1673],{},[1656,1668,1669],{},"Preview URLs in the search index",[1656,1671,1672],{},"37",[1656,1674,1675],{},"0 (after removal requests)",[1638,1677,1678,1681,1684],{},[1656,1679,1680],{},"Preview storage",[1656,1682,1683],{},"31 GB",[1656,1685,1686],{},"480 MB",[1638,1688,1689,1692,1695],{},[1656,1690,1691],{},"Deployments listed by the API",[1656,1693,1694],{},"412 (slow paging)",[1656,1696,1697],{},"6",[1638,1699,1700,1703,1706],{},[1656,1701,1702],{},"Previews holding a rotated token",[1656,1704,1705],{},"118",[1656,1707,72],{},[14,1709,1710],{},"The last row was the finding that made this a priority rather than housekeeping. A token had been rotated eight months earlier, and more than a hundred live deployments still held the old value in their build-time configuration — harmless because the token no longer worked, and an uncomfortable thing to explain in a review.",[27,1712,1714],{"id":1713},"removing-what-is-already-indexed","Removing What Is Already Indexed",[14,1716,1717],{},"If previews have been live and crawlable for a while, deleting them is necessary but not sufficient — the URLs may already be in a search index, and a deleted preview returns a 404 that takes weeks to clear on its own.",[14,1719,1720,1721,1723],{},"Three steps shorten that. Confirm the scope with a site query against the preview hostname, so you know how many URLs are involved rather than guessing. Submit a removal request for the hostname in your search console, which suppresses them within a day or two. And keep the ",[183,1722,1552],{}," header serving on the preview hostname itself, so anything crawled during the cleanup window is dropped rather than re-added.",[14,1725,1726],{},"Do not redirect preview URLs to production equivalents. It looks helpful and it teaches the index that the preview hostname is a legitimate source of your content, which is the opposite of what you want. A 404 or a 410 is the correct response for a preview that no longer exists.",[27,1728,1730],{"id":1729},"pitfalls-rollback","Pitfalls & Rollback",[32,1732,1733,1740,1753,1759,1765,1771],{},[35,1734,1735,1739],{},[1736,1737,1738],"strong",{},"Only tearing down on merge."," Abandoned pull requests are the larger population and never merge.",[35,1741,1742,1748,1749,1752],{},[1736,1743,1744,1745,1747],{},"Relying on ",[183,1746,384],{}," alone."," It prevents crawling, not indexing. Serve ",[183,1750,1751],{},"X-Robots-Tag: noindex"," as well.",[35,1754,1755,1758],{},[1736,1756,1757],{},"Sweeping by age alone."," A long-lived branch under active review will be deleted mid-review. Check for an open pull request first.",[35,1760,1761,1764],{},[1736,1762,1763],{},"Deleting the production deployment."," Filter deletions by environment explicitly; a script that lists all deployments and deletes old ones is one missing filter away from an outage.",[35,1766,1767,1770],{},[1736,1768,1769],{},"No feedback on the pull request."," Reviewers returning to an old thread find a dead link and file a bug.",[35,1772,1773,1776],{},[1736,1774,1775],{},"Rollback:"," previews are disposable by construction — a deleted preview is recreated by pushing to the branch again. If a sweep is too aggressive, raise the cutoff; nothing is lost that a rebuild cannot recreate.",[27,1778,1780],{"id":1779},"conclusion","Conclusion",[14,1782,1783,1784,1786,1787,25],{},"Treat previews as ephemeral in both directions: create them automatically, and remove them automatically. Serve ",[183,1785,1552],{}," for as long as they live, delete on pull request close rather than merge, and run a weekly sweep that respects open branches for the cases where the event never arrived. The result is a handful of live previews that correspond exactly to work in progress, which is also the only state in which the preview list is useful. The creation side is in ",[21,1788,24],{"href":23},[27,1790,1792],{"id":1791},"faq","FAQ",[1794,1795,1797],"h3",{"id":1796},"why-do-stale-previews-matter-if-nobody-visits-them","Why do stale previews matter if nobody visits them?",[14,1799,1800],{},"Search engines visit them. A preview URL that gets linked from a pull request or a chat message can be crawled and indexed, which puts duplicate content and unfinished pages into search results under a hostname you do not monitor.",[1794,1802,1804],{"id":1803},"should-previews-be-deleted-on-merge-or-on-a-schedule","Should previews be deleted on merge or on a schedule?",[14,1806,1807],{},"Both. Delete on merge or close, because that is when the preview stops being useful, and sweep on a schedule to catch the ones whose teardown failed or whose pull request was closed while CI was down.",[1794,1809,1811],{"id":1810},"how-do-i-keep-previews-out-of-search-results","How do I keep previews out of search results?",[14,1813,1814,1815,1817,1818,1820],{},"Serve ",[183,1816,1751],{}," on every preview response and a ",[183,1819,384],{}," that disallows everything. Do it at the host or edge layer rather than in the markup, so it covers assets and any page the build did not produce.",[1794,1822,1824],{"id":1823},"do-previews-need-authentication","Do previews need authentication?",[14,1826,1827,1828,1830],{},"For public documentation, usually not — ",[183,1829,1552],{}," plus an unguessable hostname is enough. For unreleased content, yes: an access rule at the edge is cheap and removes the risk of an embargoed page being shared accidentally.",[1794,1832,1834],{"id":1833},"what-is-a-reasonable-retention-period","What is a reasonable retention period?",[14,1836,1837],{},"Delete on close, and sweep anything older than about two weeks regardless. Longer retention rarely helps, because a preview more than a fortnight old no longer matches the branch it came from.",[27,1839,1841],{"id":1840},"related","Related",[32,1843,1844,1853,1860,1867,1874],{},[35,1845,1846,1849,1850,1852],{},[1736,1847,1848],{},"Parent:"," ",[21,1851,24],{"href":23}," — how the previews are created.",[35,1854,1855,1859],{},[21,1856,1858],{"href":1857},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fpreview-environments-for-pull-requests\u002Fautomating-preview-deploy-pipelines-with-github-actions\u002F","Automating Preview Deploy Pipelines With GitHub Actions"," — the workflow this attaches to.",[35,1861,1862,1866],{},[21,1863,1865],{"href":1864},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fnetlify-vs-vercel-deployment-strategies\u002Fsetting-up-deploy-previews-on-netlify-for-every-pull-request\u002F","Setting Up Deploy Previews on Netlify for Every Pull Request"," — the same lifecycle on another host.",[35,1868,1869,1873],{},[21,1870,1872],{"href":1871},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fgithub-actions-for-automated-ssg-builds\u002Fdeploying-to-multiple-environments-from-one-workflow\u002F","Deploying to Multiple Environments From One Workflow"," — scoping preview credentials.",[35,1875,1876,1880],{},[21,1877,1879],{"href":1878},"\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"," — what previews are for while they live.",[1882,1883,1884],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}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 .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}",{"title":181,"searchDepth":203,"depth":203,"links":1886},[1887,1888,1889,1890,1891,1892,1893,1894,1895,1896,1897,1904],{"id":29,"depth":203,"text":30},{"id":46,"depth":203,"text":47},{"id":170,"depth":203,"text":171},{"id":391,"depth":203,"text":392},{"id":958,"depth":203,"text":959},{"id":1545,"depth":203,"text":1546},{"id":1626,"depth":203,"text":1627},{"id":1713,"depth":203,"text":1714},{"id":1729,"depth":203,"text":1730},{"id":1779,"depth":203,"text":1780},{"id":1791,"depth":203,"text":1792,"children":1898},[1899,1900,1901,1902,1903],{"id":1796,"depth":217,"text":1797},{"id":1803,"depth":217,"text":1804},{"id":1810,"depth":217,"text":1811},{"id":1823,"depth":217,"text":1824},{"id":1833,"depth":217,"text":1834},{"id":1840,"depth":203,"text":1841},[1906,1909,1912,1913],{"name":1907,"item":1908},"Home","\u002F",{"name":1910,"item":1911},"Production-Ready Deployment & CI\u002FCD Workflows","\u002Fproduction-ready-deployment-cicd-workflows\u002F",{"name":24,"item":23},{"name":5,"item":1914},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fpreview-environments-for-pull-requests\u002Fcleaning-up-stale-preview-deployments\u002F","2026-08-01","Preview deploys accumulate: indexed by search engines, holding old secrets, and costing storage. Tear them down on merge, sweep the stragglers, and keep them out of the index.","md",[1919,1920,1921,1923,1927],{"q":1797,"a":1800},{"q":1804,"a":1807},{"q":1811,"a":1922},"Serve X-Robots-Tag noindex on every preview response and a robots.txt that disallows everything. Do it at the host or edge layer rather than in the markup, so it covers assets and any page the build did not produce.",{"q":1824,"a":1924},{"For public documentation, usually not — noindex plus an unguessable hostname is enough":1925},{" For unreleased content, yes":1926},"an access rule at the edge is cheap and removes the risk of an embargoed page being shared accidentally.",{"q":1834,"a":1837},{},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fpreview-environments-for-pull-requests\u002Fcleaning-up-stale-preview-deployments",{"title":5,"description":1916},"production-ready-deployment-cicd-workflows\u002Fpreview-environments-for-pull-requests\u002Fcleaning-up-stale-preview-deployments\u002Findex","article","cvDRQG7kkjXsnYTYyFH5ZvYDtP0giBXh0pQJKjxgn7U",[1935,1938,1941,1944,1947,1950,1953,1956,1959,1962,1965,1968,1971,1974,1977,1980,1983,1986,1989,1992,1995,1998,2001,2004,2007,2010,2013,2016,2019,2022,2025,2028,2031,2034,2037,2040,2043,2046,2049,2052,2055,2058,2061,2064,2067,2070,2073,2076,2079,2082,2085,2088,2091,2094,2097,2100,2103,2106,2109,2112,2115,2118,2121,2124,2126,2129,2132,2135,2138,2141,2144,2147,2150,2153,2156,2159,2162,2165,2166,2168,2171,2174,2177],{"path":1936,"title":1937},"\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":1939,"title":1940},"\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":1942,"title":1943},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fastro-vs-eleventy-for-documentation-sites","Astro vs Eleventy for Documentation Sites",{"path":1945,"title":1946},"\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":1948,"title":1949},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fhugo-build-times-for-large-repositories","Hugo Build Times for Large Repositories",{"path":1951,"title":1952},"\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":1954,"title":1955},"\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":1957,"title":1958},"\u002Fchoosing-the-right-static-site-generator-for-production","Choosing the Right Static Site Generator for Production",{"path":1960,"title":1961},"\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":1963,"title":1964},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fjekyll-plugin-ecosystem","Jekyll Plugin Ecosystem",{"path":1966,"title":1967},"\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":1969,"title":1970},"\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":1972,"title":1973},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fmigrating-between-static-site-generators","Migrating Between Static Site Generators",{"path":1975,"title":1976},"\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":1978,"title":1979},"\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":1981,"title":1982},"\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":1984,"title":1985},"\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":1987,"title":1988},"\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":1990,"title":1991},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fnextjs-static-export-for-content-sites","Next.js Static Export for Content Sites",{"path":1993,"title":1994},"\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":1996,"title":1997},"\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":1999,"title":2000},"\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":2002,"title":2003},"\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":2005,"title":2006},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fssg-framework-selection-matrix","SSG Framework Selection Matrix",{"path":2008,"title":2009},"\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":2011,"title":2012},"\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":2014,"title":2015},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fcdn-caching-rules-for-ssgs","CDN Caching Rules for SSGs",{"path":2017,"title":2018},"\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":2020,"title":2021},"\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":2023,"title":2024},"\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":2026,"title":2027},"\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":2029,"title":2030},"\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":2032,"title":2033},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fcumulative-layout-shift-fixes-for-static-sites","Cumulative Layout Shift Fixes for Static Sites",{"path":2035,"title":2036},"\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":2038,"title":2039},"\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":2041,"title":2042},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Ffont-loading-strategies-for-static-sites","Font Loading Strategies for Static Sites",{"path":2044,"title":2045},"\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":2047,"title":2048},"\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":2050,"title":2051},"\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":2053,"title":2054},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fimage-optimization-pipelines-in-astro","Image Optimization Pipelines in Astro",{"path":2056,"title":2057},"\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":2059,"title":2060},"\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":2062,"title":2063},"\u002Fperformance-optimization-core-web-vitals-for-ssgs","Core Web Vitals Optimization for SSGs",{"path":2065,"title":2066},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fjavascript-hydration-partial-rendering\u002Fastro-islands-vs-full-hydration-performance","Astro Islands vs Full Hydration Performance",{"path":2068,"title":2069},"\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":2071,"title":2072},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fjavascript-hydration-partial-rendering","JavaScript Hydration & Partial Rendering",{"path":2074,"title":2075},"\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":2077,"title":2078},"\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":2080,"title":2081},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Flargest-contentful-paint-optimization-for-static-sites","Largest Contentful Paint Optimization for Static Sites",{"path":2083,"title":2084},"\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":2086,"title":2087},"\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":2089,"title":2090},"\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":2092,"title":2093},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fthird-party-script-performance-on-static-sites","Third-Party Script Performance on Static Sites",{"path":2095,"title":2096},"\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":2098,"title":2099},"\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":2101,"title":2102},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fcloudflare-pages-edge-caching-setup\u002Fautomating-eleventy-deployments-with-cloudflare-pages","Automating Eleventy Deployments on Cloudflare Pages",{"path":2104,"title":2105},"\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":2107,"title":2108},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fcloudflare-pages-edge-caching-setup","Cloudflare Pages Edge Caching Setup",{"path":2110,"title":2111},"\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":2113,"title":2114},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fcontent-workflows-for-documentation-teams","Content Workflows for Documentation Teams",{"path":2116,"title":2117},"\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":2119,"title":2120},"\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":2122,"title":2123},"\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":2125,"title":1872},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fgithub-actions-for-automated-ssg-builds\u002Fdeploying-to-multiple-environments-from-one-workflow",{"path":2127,"title":2128},"\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":2130,"title":2131},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fgithub-actions-for-automated-ssg-builds","GitHub Actions for Automated SSG Builds",{"path":2133,"title":2134},"\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":2136,"title":2137},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fincremental-builds-and-build-caching-for-ssgs\u002Fenabling-incremental-builds-in-eleventy","Enabling Incremental Builds in Eleventy",{"path":2139,"title":2140},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fincremental-builds-and-build-caching-for-ssgs","Incremental Builds and Build Caching for SSGs",{"path":2142,"title":2143},"\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":2145,"title":2146},"\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":2148,"title":2149},"\u002Fproduction-ready-deployment-cicd-workflows","Production-Ready Deployment & CI\u002FCD for SSGs",{"path":2151,"title":2152},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fnetlify-vs-vercel-deployment-strategies","Netlify vs Vercel Deployment Strategies",{"path":2154,"title":2155},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fnetlify-vs-vercel-deployment-strategies\u002Fnetlify-build-hooks-for-content-updates","Netlify Build Hooks for Content Updates",{"path":2157,"title":2158},"\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":2160,"title":2161},"\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":2163,"title":2164},"\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":1929,"title":5},{"path":2167,"title":24},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fpreview-environments-for-pull-requests",{"path":2169,"title":2170},"\u002Fproduction-ready-deployment-cicd-workflows\u002Frollbacks-and-deploy-safety-for-static-sites\u002Fatomic-deploys-vs-incremental-uploads","Atomic Deploys vs Incremental Uploads",{"path":2172,"title":2173},"\u002Fproduction-ready-deployment-cicd-workflows\u002Frollbacks-and-deploy-safety-for-static-sites","Rollbacks and Deploy Safety for Static Sites",{"path":2175,"title":2176},"\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":2178,"title":1879},"\u002Fproduction-ready-deployment-cicd-workflows\u002Frollbacks-and-deploy-safety-for-static-sites\u002Frunning-smoke-tests-against-a-preview-url",1785611671486]