[{"data":1,"prerenderedAt":2345},["ShallowReactive",2],{"page:\u002Fproduction-ready-deployment-cicd-workflows\u002Fself-hosting-static-sites\u002Fcloudfront-functions-for-redirects":3,"all-docs-nav":1802},{"id":4,"title":5,"body":6,"breadcrumb":1778,"dateModified":1787,"datePublished":1787,"description":1788,"extension":1789,"faq":1790,"meta":1795,"navigation":1796,"path":1797,"seo":1798,"slug":12,"stem":1799,"type":1800,"__hash__":1801},"content\u002Fproduction-ready-deployment-cicd-workflows\u002Fself-hosting-static-sites\u002Fcloudfront-functions-for-redirects\u002Findex.md","CloudFront Functions for Redirects",{"type":7,"value":8,"toc":1759},"minimark",[9,13,22,36,41,54,58,61,204,228,540,543,703,707,710,1005,1008,1012,1015,1173,1189,1193,1196,1281,1285,1292,1296,1299,1443,1446,1450,1453,1540,1610,1614,1674,1678,1681,1685,1690,1693,1697,1700,1704,1707,1711,1714,1718,1755],[10,11,5],"h1",{"id":12},"cloudfront-functions-for-redirects",[14,15,16,17,21],"p",{},"Every site that lives long enough accumulates redirects: renamed pages, a reorganised section, a migration from another generator, a retired product's docs folded into an archive. Managed hosts read them from a ",[18,19,20],"code",{},"_redirects"," file. On S3 behind CloudFront there is no such file — the bucket is private, the website endpoint's routing rules are limited and require a public bucket, and S3 object redirects only work on that same endpoint. The place to handle redirects is CloudFront itself, in the same viewer-request function that already handles clean URLs.",[14,23,24,25,30,31,35],{},"This guide generates a redirect map from the build, serves it from a CloudFront Function for small sets and from CloudFront KeyValueStore for large ones, resolves chains before they ship, and tests the result on every deploy. It is part of ",[26,27,29],"a",{"href":28},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fself-hosting-static-sites\u002F","Self-Hosting Static Sites on S3, Nginx and Caddy",", and extends the function from ",[26,32,34],{"href":33},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fself-hosting-static-sites\u002Fclean-urls-and-trailing-slashes-on-s3\u002F","Clean URLs and Trailing Slashes on S3",".",[37,38,40],"h2",{"id":39},"prerequisites","Prerequisites",[42,43,44,48,51],"ul",{},[45,46,47],"li",{},"A CloudFront distribution in front of a private S3 bucket.",[45,49,50],{},"A redirect list in one source of truth — a JSON or YAML file in the repository.",[45,52,53],{},"Permission to create CloudFront Functions and KeyValueStores, and to update them from CI.",[37,55,57],{"id":56},"step-1-one-redirect-source-many-outputs","Step 1: One Redirect Source, Many Outputs",[14,59,60],{},"Keep redirects in a single file in the repository and generate every host-specific format from it. That way the same list serves S3, a staging nginx and a Cloudflare Pages preview without drift:",[62,63,68],"pre",{"className":64,"code":65,"language":66,"meta":67,"style":67},"language-yaml shiki shiki-themes github-light github-dark","# redirects.yml\n- { from: \u002Fdocs\u002Fgetting-started\u002F, to: \u002Fguides\u002Finstall\u002F }\n- { from: \u002Fblog\u002F2021\u002Fnew-cli\u002F,   to: \u002Fchangelog\u002F2021\u002Fcli-2-0\u002F }\n- { from: \u002Fapi\u002Fv1\u002F,               to: \u002Farchive\u002Fapi-v1\u002F, prefix: true }\n- { from: \u002Fpricing-old\u002F,          to: https:\u002F\u002Fexample.com\u002Fpricing\u002F, status: 302 }\n","yaml","",[18,69,70,79,111,135,170],{"__ignoreMap":67},[71,72,75],"span",{"class":73,"line":74},"line",1,[71,76,78],{"class":77},"sJ8bj","# redirects.yml\n",[71,80,82,86,90,93,97,100,103,105,108],{"class":73,"line":81},2,[71,83,85],{"class":84},"sVt8B","- { ",[71,87,89],{"class":88},"s9eBZ","from",[71,91,92],{"class":84},": ",[71,94,96],{"class":95},"sZZnC","\u002Fdocs\u002Fgetting-started\u002F",[71,98,99],{"class":84},", ",[71,101,102],{"class":88},"to",[71,104,92],{"class":84},[71,106,107],{"class":95},"\u002Fguides\u002Finstall\u002F",[71,109,110],{"class":84}," }\n",[71,112,114,116,118,120,123,126,128,130,133],{"class":73,"line":113},3,[71,115,85],{"class":84},[71,117,89],{"class":88},[71,119,92],{"class":84},[71,121,122],{"class":95},"\u002Fblog\u002F2021\u002Fnew-cli\u002F",[71,124,125],{"class":84},",   ",[71,127,102],{"class":88},[71,129,92],{"class":84},[71,131,132],{"class":95},"\u002Fchangelog\u002F2021\u002Fcli-2-0\u002F",[71,134,110],{"class":84},[71,136,138,140,142,144,147,150,152,154,157,159,162,164,168],{"class":73,"line":137},4,[71,139,85],{"class":84},[71,141,89],{"class":88},[71,143,92],{"class":84},[71,145,146],{"class":95},"\u002Fapi\u002Fv1\u002F",[71,148,149],{"class":84},",               ",[71,151,102],{"class":88},[71,153,92],{"class":84},[71,155,156],{"class":95},"\u002Farchive\u002Fapi-v1\u002F",[71,158,99],{"class":84},[71,160,161],{"class":88},"prefix",[71,163,92],{"class":84},[71,165,167],{"class":166},"sj4cs","true",[71,169,110],{"class":84},[71,171,173,175,177,179,182,185,187,189,192,194,197,199,202],{"class":73,"line":172},5,[71,174,85],{"class":84},[71,176,89],{"class":88},[71,178,92],{"class":84},[71,180,181],{"class":95},"\u002Fpricing-old\u002F",[71,183,184],{"class":84},",          ",[71,186,102],{"class":88},[71,188,92],{"class":84},[71,190,191],{"class":95},"https:\u002F\u002Fexample.com\u002Fpricing\u002F",[71,193,99],{"class":84},[71,195,196],{"class":88},"status",[71,198,92],{"class":84},[71,200,201],{"class":166},"302",[71,203,110],{"class":84},[14,205,206,207,209,210,213,214,216,217,219,220,223,224,227],{},"At build time a script validates the list — every ",[18,208,89],{}," starts with ",[18,211,212],{},"\u002F",", no ",[18,215,89],{}," appears twice, every internal ",[18,218,102],{}," exists in the build output — and resolves chains so that ",[18,221,222],{},"A → B → C"," becomes ",[18,225,226],{},"A → C",":",[62,229,233],{"className":230,"code":231,"language":232,"meta":67,"style":67},"language-js shiki shiki-themes github-light github-dark","\u002F\u002F scripts\u002Fredirects.mjs (excerpt)\nconst map = new Map(rules.map((r) => [r.from, r]));\nfor (const r of rules) {\n  const seen = new Set([r.from]);\n  let target = r.to;\n  while (map.has(target)) {\n    if (seen.has(target)) throw new Error(`redirect loop at ${target}`);\n    seen.add(target);\n    target = map.get(target).to;\n  }\n  r.to = target;\n  if (!r.to.startsWith('http') && !existsSync(`dist${r.to}index.html`) && !existsSync(`dist${r.to}`))\n    throw new Error(`redirect target missing: ${r.from} → ${r.to}`);\n}\n","js",[18,234,235,240,281,300,318,332,347,384,396,413,419,430,500,534],{"__ignoreMap":67},[71,236,237],{"class":73,"line":74},[71,238,239],{"class":77},"\u002F\u002F scripts\u002Fredirects.mjs (excerpt)\n",[71,241,242,246,249,252,255,259,262,265,268,272,275,278],{"class":73,"line":81},[71,243,245],{"class":244},"szBVR","const",[71,247,248],{"class":166}," map",[71,250,251],{"class":244}," =",[71,253,254],{"class":244}," new",[71,256,258],{"class":257},"sScJk"," Map",[71,260,261],{"class":84},"(rules.",[71,263,264],{"class":257},"map",[71,266,267],{"class":84},"((",[71,269,271],{"class":270},"s4XuR","r",[71,273,274],{"class":84},") ",[71,276,277],{"class":244},"=>",[71,279,280],{"class":84}," [r.from, r]));\n",[71,282,283,286,289,291,294,297],{"class":73,"line":113},[71,284,285],{"class":244},"for",[71,287,288],{"class":84}," (",[71,290,245],{"class":244},[71,292,293],{"class":166}," r",[71,295,296],{"class":244}," of",[71,298,299],{"class":84}," rules) {\n",[71,301,302,305,308,310,312,315],{"class":73,"line":137},[71,303,304],{"class":244},"  const",[71,306,307],{"class":166}," seen",[71,309,251],{"class":244},[71,311,254],{"class":244},[71,313,314],{"class":257}," Set",[71,316,317],{"class":84},"([r.from]);\n",[71,319,320,323,326,329],{"class":73,"line":172},[71,321,322],{"class":244},"  let",[71,324,325],{"class":84}," target ",[71,327,328],{"class":244},"=",[71,330,331],{"class":84}," r.to;\n",[71,333,335,338,341,344],{"class":73,"line":334},6,[71,336,337],{"class":244},"  while",[71,339,340],{"class":84}," (map.",[71,342,343],{"class":257},"has",[71,345,346],{"class":84},"(target)) {\n",[71,348,350,353,356,358,361,364,366,369,372,375,378,381],{"class":73,"line":349},7,[71,351,352],{"class":244},"    if",[71,354,355],{"class":84}," (seen.",[71,357,343],{"class":257},[71,359,360],{"class":84},"(target)) ",[71,362,363],{"class":244},"throw",[71,365,254],{"class":244},[71,367,368],{"class":257}," Error",[71,370,371],{"class":84},"(",[71,373,374],{"class":95},"`redirect loop at ${",[71,376,377],{"class":84},"target",[71,379,380],{"class":95},"}`",[71,382,383],{"class":84},");\n",[71,385,387,390,393],{"class":73,"line":386},8,[71,388,389],{"class":84},"    seen.",[71,391,392],{"class":257},"add",[71,394,395],{"class":84},"(target);\n",[71,397,399,402,404,407,410],{"class":73,"line":398},9,[71,400,401],{"class":84},"    target ",[71,403,328],{"class":244},[71,405,406],{"class":84}," map.",[71,408,409],{"class":257},"get",[71,411,412],{"class":84},"(target).to;\n",[71,414,416],{"class":73,"line":415},10,[71,417,418],{"class":84},"  }\n",[71,420,422,425,427],{"class":73,"line":421},11,[71,423,424],{"class":84},"  r.to ",[71,426,328],{"class":244},[71,428,429],{"class":84}," target;\n",[71,431,433,436,438,441,444,447,449,452,454,457,460,463,465,468,470,472,474,477,479,481,483,485,487,489,491,493,495,497],{"class":73,"line":432},12,[71,434,435],{"class":244},"  if",[71,437,288],{"class":84},[71,439,440],{"class":244},"!",[71,442,443],{"class":84},"r.to.",[71,445,446],{"class":257},"startsWith",[71,448,371],{"class":84},[71,450,451],{"class":95},"'http'",[71,453,274],{"class":84},[71,455,456],{"class":244},"&&",[71,458,459],{"class":244}," !",[71,461,462],{"class":257},"existsSync",[71,464,371],{"class":84},[71,466,467],{"class":95},"`dist${",[71,469,271],{"class":84},[71,471,35],{"class":95},[71,473,102],{"class":84},[71,475,476],{"class":95},"}index.html`",[71,478,274],{"class":84},[71,480,456],{"class":244},[71,482,459],{"class":244},[71,484,462],{"class":257},[71,486,371],{"class":84},[71,488,467],{"class":95},[71,490,271],{"class":84},[71,492,35],{"class":95},[71,494,102],{"class":84},[71,496,380],{"class":95},[71,498,499],{"class":84},"))\n",[71,501,503,506,508,510,512,515,517,519,521,524,526,528,530,532],{"class":73,"line":502},13,[71,504,505],{"class":244},"    throw",[71,507,254],{"class":244},[71,509,368],{"class":257},[71,511,371],{"class":84},[71,513,514],{"class":95},"`redirect target missing: ${",[71,516,271],{"class":84},[71,518,35],{"class":95},[71,520,89],{"class":84},[71,522,523],{"class":95},"} → ${",[71,525,271],{"class":84},[71,527,35],{"class":95},[71,529,102],{"class":84},[71,531,380],{"class":95},[71,533,383],{"class":84},[71,535,537],{"class":73,"line":536},14,[71,538,539],{"class":84},"}\n",[14,541,542],{},"On this site the first run found 14 chains (from three successive reorganisations) and 3 targets that no longer existed.",[544,545,546,699],"figure",{},[547,548,555,556,555,560,555,564,555,571,555,679],"svg",{"viewBox":549,"role":550,"ariaLabelledBy":551,"xmlns":554},"0 0 760 270","img",[552,553],"cfr-chain-title","cfr-chain-desc","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","\n  ",[557,558,559],"title",{"id":552},"Resolving a redirect chain at build time",[561,562,563],"desc",{"id":553},"An old URL from 2019 redirects to a 2021 URL, which redirects to a 2023 URL, which redirects to the current page: three hops. The build script collapses the chain so every old URL redirects directly to the current page in one hop.",[565,566],"rect",{"x":567,"y":567,"width":568,"height":569,"fill":570},"0","760","270","#ffffff",[572,573,575,576,575,584,575,590,575,600,575,606,575,608,575,612,575,615,575,619,575,626,575,629,575,646,575,652,575,656,575,659,575,661,575,663,575,665,575,669,575,674,555],"g",{"style":574},"font-family:system-ui, sans-serif;font-size:12px","\n    ",[577,578,583],"text",{"x":579,"y":580,"fill":581,"style":582},"380","28","#1f2937","font-size:16px;font-weight:700;text-anchor:middle","Three reorganisations, one hop",[577,585,589],{"x":586,"y":587,"fill":581,"style":588},"30","72","font-weight:700","Before",[565,591],{"x":592,"y":593,"width":594,"height":595,"rx":596,"fill":597,"stroke":598,"style":599},"110","54","130","32","6","#f8fafc","#d9e2ef","stroke-width:1px",[577,601,605],{"x":602,"y":603,"fill":581,"style":604},"175","75","font-size:11px;text-anchor:middle","\u002Fdocs\u002Fstart\u002F (2019)",[565,607],{"x":569,"y":593,"width":594,"height":595,"rx":596,"fill":597,"stroke":598,"style":599},[577,609,611],{"x":610,"y":603,"fill":581,"style":604},"335","\u002Fgetting-started\u002F",[565,613],{"x":614,"y":593,"width":594,"height":595,"rx":596,"fill":597,"stroke":598,"style":599},"430",[577,616,618],{"x":617,"y":603,"fill":581,"style":604},"495","\u002Fguides\u002Fstart\u002F",[565,620],{"x":621,"y":593,"width":622,"height":595,"rx":596,"fill":623,"opacity":624,"stroke":625,"style":599},"590","140","#8ac926","0.2","#5a8a16",[577,627,107],{"x":628,"y":603,"fill":581,"style":604},"660",[572,630,634,635,634,640,634,643,575],{"stroke":631,"fill":632,"style":633},"#d83b41","none","stroke-width:2px","\n      ",[636,637],"path",{"d":638,"style":639},"M242 70 L266 70","marker-end:url(#cfr-arrow)",[636,641],{"d":642,"style":639},"M402 70 L426 70",[636,644],{"d":645,"style":639},"M562 70 L586 70",[577,647,651],{"x":648,"y":592,"fill":649,"style":650},"420","#b32b30","font-size:11px;font-weight:700;text-anchor:middle","3 hops · ~3 × RTT before the reader sees a page",[577,653,655],{"x":586,"y":654,"fill":581,"style":588},"172","After",[565,657],{"x":592,"y":658,"width":594,"height":595,"rx":596,"fill":597,"stroke":598,"style":599},"154",[577,660,605],{"x":602,"y":602,"fill":581,"style":604},[565,662],{"x":621,"y":658,"width":622,"height":595,"rx":596,"fill":623,"opacity":624,"stroke":625,"style":599},[577,664,107],{"x":628,"y":602,"fill":581,"style":604},[636,666],{"d":667,"stroke":625,"fill":632,"style":668},"M242 170 L586 170","stroke-width:2px;marker-end:url(#cfr-arrow-g)",[577,670,673],{"x":648,"y":671,"fill":672,"style":650},"210","#3f6410","1 hop, resolved at build time",[577,675,678],{"x":579,"y":676,"fill":677,"style":604},"250","#556071","14 chains like this were found on the first run of the build check",[680,681,575,682,575,694,555],"defs",{},[683,684,634,691,575],"marker",{"id":685,"viewBox":686,"refX":687,"refY":688,"markerWidth":689,"markerHeight":689,"orient":690},"cfr-arrow","0 0 10 10","8","5","7","auto-start-reverse",[636,692],{"d":693,"fill":631},"M0 0 L10 5 L0 10 z",[683,695,634,697,575],{"id":696,"viewBox":686,"refX":687,"refY":688,"markerWidth":689,"markerHeight":689,"orient":690},"cfr-arrow-g",[636,698],{"d":693,"fill":625},[700,701,702],"figcaption",{},"Chains accumulate silently with each reorganisation; resolving them in the build keeps every old link to one round trip.",[37,704,706],{"id":705},"step-2-small-sets-inline-in-the-function","Step 2: Small Sets — Inline in the Function",[14,708,709],{},"Under a few hundred rules, embed the map directly in the function code, generated by the build. The 10 KB code limit fits roughly 250 short pairs:",[62,711,713],{"className":230,"code":712,"language":232,"meta":67,"style":67},"\u002F\u002F generated: cloudfront\u002Fredirects-fn.js\nvar R = {\"\u002Fdocs\u002Fgetting-started\u002F\":\"\u002Fguides\u002Finstall\u002F\",\"\u002Fblog\u002F2021\u002Fnew-cli\u002F\":\"\u002Fchangelog\u002F2021\u002Fcli-2-0\u002F\"};\nvar P = [[\"\u002Fapi\u002Fv1\u002F\",\"\u002Farchive\u002Fapi-v1\u002F\"]];\nfunction handler(event) {\n  var req = event.request, uri = req.uri;\n  var to = R[uri];\n  if (!to) for (var i = 0; i \u003C P.length; i++) if (uri.indexOf(P[i][0]) === 0) { to = P[i][1] + uri.slice(P[i][0].length); break; }\n  if (to) return { statusCode: 301, statusDescription: 'Moved Permanently',\n    headers: { location: { value: to }, 'cache-control': { value: 'public, max-age=3600' } } };\n  return cleanUrls(req);   \u002F\u002F the rewrite from the clean-URLs function\n}\n",[18,714,715,720,755,778,794,812,826,945,970,987,1001],{"__ignoreMap":67},[71,716,717],{"class":73,"line":74},[71,718,719],{"class":77},"\u002F\u002F generated: cloudfront\u002Fredirects-fn.js\n",[71,721,722,725,728,730,733,736,738,741,744,747,749,752],{"class":73,"line":81},[71,723,724],{"class":244},"var",[71,726,727],{"class":166}," R",[71,729,251],{"class":244},[71,731,732],{"class":84}," {",[71,734,735],{"class":95},"\"\u002Fdocs\u002Fgetting-started\u002F\"",[71,737,227],{"class":84},[71,739,740],{"class":95},"\"\u002Fguides\u002Finstall\u002F\"",[71,742,743],{"class":84},",",[71,745,746],{"class":95},"\"\u002Fblog\u002F2021\u002Fnew-cli\u002F\"",[71,748,227],{"class":84},[71,750,751],{"class":95},"\"\u002Fchangelog\u002F2021\u002Fcli-2-0\u002F\"",[71,753,754],{"class":84},"};\n",[71,756,757,759,762,764,767,770,772,775],{"class":73,"line":113},[71,758,724],{"class":244},[71,760,761],{"class":166}," P",[71,763,251],{"class":244},[71,765,766],{"class":84}," [[",[71,768,769],{"class":95},"\"\u002Fapi\u002Fv1\u002F\"",[71,771,743],{"class":84},[71,773,774],{"class":95},"\"\u002Farchive\u002Fapi-v1\u002F\"",[71,776,777],{"class":84},"]];\n",[71,779,780,783,786,788,791],{"class":73,"line":137},[71,781,782],{"class":244},"function",[71,784,785],{"class":257}," handler",[71,787,371],{"class":84},[71,789,790],{"class":270},"event",[71,792,793],{"class":84},") {\n",[71,795,796,799,802,804,807,809],{"class":73,"line":172},[71,797,798],{"class":244},"  var",[71,800,801],{"class":84}," req ",[71,803,328],{"class":244},[71,805,806],{"class":84}," event.request, uri ",[71,808,328],{"class":244},[71,810,811],{"class":84}," req.uri;\n",[71,813,814,816,819,821,823],{"class":73,"line":334},[71,815,798],{"class":244},[71,817,818],{"class":84}," to ",[71,820,328],{"class":244},[71,822,727],{"class":166},[71,824,825],{"class":84},"[uri];\n",[71,827,828,830,832,834,837,839,841,843,846,848,851,854,857,859,861,864,867,870,872,875,878,881,883,886,889,891,894,897,899,902,904,906,908,911,914,917,920,923,925,927,929,931,934,936,939,942],{"class":73,"line":349},[71,829,435],{"class":244},[71,831,288],{"class":84},[71,833,440],{"class":244},[71,835,836],{"class":84},"to) ",[71,838,285],{"class":244},[71,840,288],{"class":84},[71,842,724],{"class":244},[71,844,845],{"class":84}," i ",[71,847,328],{"class":244},[71,849,850],{"class":166}," 0",[71,852,853],{"class":84},"; i ",[71,855,856],{"class":244},"\u003C",[71,858,761],{"class":166},[71,860,35],{"class":84},[71,862,863],{"class":166},"length",[71,865,866],{"class":84},"; i",[71,868,869],{"class":244},"++",[71,871,274],{"class":84},[71,873,874],{"class":244},"if",[71,876,877],{"class":84}," (uri.",[71,879,880],{"class":257},"indexOf",[71,882,371],{"class":84},[71,884,885],{"class":166},"P",[71,887,888],{"class":84},"[i][",[71,890,567],{"class":166},[71,892,893],{"class":84},"]) ",[71,895,896],{"class":244},"===",[71,898,850],{"class":166},[71,900,901],{"class":84},") { to ",[71,903,328],{"class":244},[71,905,761],{"class":166},[71,907,888],{"class":84},[71,909,910],{"class":166},"1",[71,912,913],{"class":84},"] ",[71,915,916],{"class":244},"+",[71,918,919],{"class":84}," uri.",[71,921,922],{"class":257},"slice",[71,924,371],{"class":84},[71,926,885],{"class":166},[71,928,888],{"class":84},[71,930,567],{"class":166},[71,932,933],{"class":84},"].",[71,935,863],{"class":166},[71,937,938],{"class":84},"); ",[71,940,941],{"class":244},"break",[71,943,944],{"class":84},"; }\n",[71,946,947,949,952,955,958,961,964,967],{"class":73,"line":386},[71,948,435],{"class":244},[71,950,951],{"class":84}," (to) ",[71,953,954],{"class":244},"return",[71,956,957],{"class":84}," { statusCode: ",[71,959,960],{"class":166},"301",[71,962,963],{"class":84},", statusDescription: ",[71,965,966],{"class":95},"'Moved Permanently'",[71,968,969],{"class":84},",\n",[71,971,972,975,978,981,984],{"class":73,"line":398},[71,973,974],{"class":84},"    headers: { location: { value: to }, ",[71,976,977],{"class":95},"'cache-control'",[71,979,980],{"class":84},": { value: ",[71,982,983],{"class":95},"'public, max-age=3600'",[71,985,986],{"class":84}," } } };\n",[71,988,989,992,995,998],{"class":73,"line":415},[71,990,991],{"class":244},"  return",[71,993,994],{"class":257}," cleanUrls",[71,996,997],{"class":84},"(req);   ",[71,999,1000],{"class":77},"\u002F\u002F the rewrite from the clean-URLs function\n",[71,1002,1003],{"class":73,"line":421},[71,1004,539],{"class":84},[14,1006,1007],{},"Exact matches are an object lookup; prefix rules are a short loop and should stay few.",[37,1009,1011],{"id":1010},"step-3-large-sets-keyvaluestore","Step 3: Large Sets — KeyValueStore",[14,1013,1014],{},"A migration can easily produce thousands of redirects. CloudFront KeyValueStore holds up to 5 MB of pairs, readable from a function with sub-millisecond latency, and can be updated independently of the function code:",[62,1016,1018],{"className":230,"code":1017,"language":232,"meta":67,"style":67},"import cf from 'cloudfront';\nconst kvs = cf.kvs('a1b2c3d4-5678-90ab-cdef-EXAMPLE11111');\nasync function handler(event) {\n  const req = event.request;\n  try {\n    const to = await kvs.get(req.uri);\n    return { statusCode: 301, statusDescription: 'Moved Permanently',\n      headers: { location: { value: to }, 'cache-control': { value: 'public, max-age=3600' } } };\n  } catch (e) {\n    return cleanUrls(req);   \u002F\u002F key not found: continue normally\n  }\n}\n",[18,1019,1020,1036,1058,1074,1086,1094,1115,1130,1143,1154,1165,1169],{"__ignoreMap":67},[71,1021,1022,1025,1028,1030,1033],{"class":73,"line":74},[71,1023,1024],{"class":244},"import",[71,1026,1027],{"class":84}," cf ",[71,1029,89],{"class":244},[71,1031,1032],{"class":95}," 'cloudfront'",[71,1034,1035],{"class":84},";\n",[71,1037,1038,1040,1043,1045,1048,1051,1053,1056],{"class":73,"line":81},[71,1039,245],{"class":244},[71,1041,1042],{"class":166}," kvs",[71,1044,251],{"class":244},[71,1046,1047],{"class":84}," cf.",[71,1049,1050],{"class":257},"kvs",[71,1052,371],{"class":84},[71,1054,1055],{"class":95},"'a1b2c3d4-5678-90ab-cdef-EXAMPLE11111'",[71,1057,383],{"class":84},[71,1059,1060,1063,1066,1068,1070,1072],{"class":73,"line":113},[71,1061,1062],{"class":244},"async",[71,1064,1065],{"class":244}," function",[71,1067,785],{"class":257},[71,1069,371],{"class":84},[71,1071,790],{"class":270},[71,1073,793],{"class":84},[71,1075,1076,1078,1081,1083],{"class":73,"line":137},[71,1077,304],{"class":244},[71,1079,1080],{"class":166}," req",[71,1082,251],{"class":244},[71,1084,1085],{"class":84}," event.request;\n",[71,1087,1088,1091],{"class":73,"line":172},[71,1089,1090],{"class":244},"  try",[71,1092,1093],{"class":84}," {\n",[71,1095,1096,1099,1102,1104,1107,1110,1112],{"class":73,"line":334},[71,1097,1098],{"class":244},"    const",[71,1100,1101],{"class":166}," to",[71,1103,251],{"class":244},[71,1105,1106],{"class":244}," await",[71,1108,1109],{"class":84}," kvs.",[71,1111,409],{"class":257},[71,1113,1114],{"class":84},"(req.uri);\n",[71,1116,1117,1120,1122,1124,1126,1128],{"class":73,"line":349},[71,1118,1119],{"class":244},"    return",[71,1121,957],{"class":84},[71,1123,960],{"class":166},[71,1125,963],{"class":84},[71,1127,966],{"class":95},[71,1129,969],{"class":84},[71,1131,1132,1135,1137,1139,1141],{"class":73,"line":386},[71,1133,1134],{"class":84},"      headers: { location: { value: to }, ",[71,1136,977],{"class":95},[71,1138,980],{"class":84},[71,1140,983],{"class":95},[71,1142,986],{"class":84},[71,1144,1145,1148,1151],{"class":73,"line":398},[71,1146,1147],{"class":84},"  } ",[71,1149,1150],{"class":244},"catch",[71,1152,1153],{"class":84}," (e) {\n",[71,1155,1156,1158,1160,1162],{"class":73,"line":415},[71,1157,1119],{"class":244},[71,1159,994],{"class":257},[71,1161,997],{"class":84},[71,1163,1164],{"class":77},"\u002F\u002F key not found: continue normally\n",[71,1166,1167],{"class":73,"line":421},[71,1168,418],{"class":84},[71,1170,1171],{"class":73,"line":432},[71,1172,539],{"class":84},[14,1174,1175,1176,1179,1180,1183,1184,1188],{},"The deploy job syncs the store from the generated list using the ",[18,1177,1178],{},"cloudfront-keyvaluestore"," API's batch ",[18,1181,1182],{},"update-keys"," operation, putting new and changed keys and deleting removed ones. Update the store ",[1185,1186,1187],"strong",{},"before"," uploading content that removes old pages, so the redirects exist by the time the old URLs stop resolving.",[37,1190,1192],{"id":1191},"choosing-between-inline-and-keyvaluestore","Choosing Between Inline and KeyValueStore",[14,1194,1195],{},"The two approaches trade deployment coupling against capacity. Inline maps deploy with the function, so a redirect change is a function publish — atomic, versioned, and trivially rolled back — but the 10 KB limit caps the list. KeyValueStore separates data from code: thousands of entries, updated without republishing the function, but now there are two things to deploy in the right order and two things to roll back.",[544,1197,1198,1278],{},[547,1199,555,1204,555,1207,555,1210,555,1212,555,1271],{"viewBox":1200,"role":550,"ariaLabelledBy":1201,"xmlns":554},"0 0 760 250",[1202,1203],"cfr-pick-title","cfr-pick-desc",[557,1205,1206],{"id":1202},"Inline map or KeyValueStore",[561,1208,1209],{"id":1203},"A scale of redirect count. Up to about 250 rules, an inline map in the function is simplest: one artifact, atomic publish. Beyond that, KeyValueStore holds up to 5 megabytes of pairs, updated separately from the function code, with the store updated before content changes.",[565,1211],{"x":567,"y":567,"width":568,"height":676,"fill":570},[572,1213,575,1214,575,1217,575,1223,575,1227,575,1231,575,1236,575,1239,575,1244,575,1250,575,1256,575,1260,575,1264,575,1267,555],{"style":574},[577,1215,1216],{"x":579,"y":580,"fill":581,"style":582},"Pick by the size of the list",[73,1218],{"x1":1219,"y1":1220,"x2":1221,"y2":1220,"stroke":677,"style":1222},"60","120","720","stroke-width:2px;marker-end:url(#cfr-p-arrow)",[577,1224,567],{"x":1219,"y":1225,"fill":677,"style":1226},"144","font-size:11px",[577,1228,1230],{"x":1229,"y":1225,"fill":677,"style":604},"240","~250 rules",[577,1232,1235],{"x":1233,"y":1225,"fill":677,"style":1234},"700","font-size:11px;text-anchor:end","thousands",[73,1237],{"x1":1229,"y1":1238,"x2":1229,"y2":594,"stroke":631,"style":633},"100",[565,1240],{"x":1219,"y":593,"width":1241,"height":1242,"rx":687,"fill":623,"opacity":624,"stroke":625,"style":1243},"170","40","stroke-width:1.5px",[577,1245,1249],{"x":1246,"y":1247,"fill":581,"style":1248},"145","79","font-weight:700;text-anchor:middle","inline map",[565,1251],{"x":1252,"y":593,"width":1253,"height":1242,"rx":687,"fill":1254,"opacity":1255,"stroke":1254,"style":1243},"260","440","#1982c4","0.14",[577,1257,1259],{"x":1258,"y":1247,"fill":581,"style":1248},"480","KeyValueStore (up to 5 MB)",[577,1261,1263],{"x":1246,"y":1262,"fill":677,"style":604},"176","one artifact, atomic publish",[577,1265,1266],{"x":1258,"y":1262,"fill":677,"style":604},"data separate from code; update store before content",[577,1268,1270],{"x":579,"y":1269,"fill":677,"style":604},"222","The 10 KB function code limit is what draws the line",[680,1272,575,1273,555],{},[683,1274,634,1276,575],{"id":1275,"viewBox":686,"refX":687,"refY":688,"markerWidth":689,"markerHeight":689,"orient":690},"cfr-p-arrow",[636,1277],{"d":693,"fill":677},[700,1279,1280],{},"Most sites never leave the left side; a migration from another generator is what pushes a list past it.",[37,1282,1284],{"id":1283},"redirects-and-caching","Redirects and Caching",[14,1286,1287,1288,1291],{},"Because the function runs on viewer requests, a redirect response is generated at the edge on every request and is never stored in CloudFront's cache — there is nothing to invalidate when the list changes. What does get cached is the 301 in readers' browsers, for as long as the ",[18,1289,1290],{},"Cache-Control"," on the redirect says. A one-hour lifetime balances the two concerns: repeat visits within the hour skip the round trip entirely, and a mistaken redirect stops affecting a given browser within an hour of being fixed. Search engines treat a 301 as permanent regardless of the cache header and transfer ranking signals to the target after recrawling, which on this site took two to five weeks for most of the 3,140 migrated URLs.",[37,1293,1295],{"id":1294},"step-4-test-redirects-on-every-deploy","Step 4: Test Redirects on Every Deploy",[14,1297,1298],{},"The generated list is also the test suite. After each deploy, request a sample of sources and check each returns exactly one 301 to its expected target, which itself returns 200:",[62,1300,1304],{"className":1301,"code":1302,"language":1303,"meta":67,"style":67},"language-bash shiki shiki-themes github-light github-dark","node -e \"const r=require('.\u002Fdist\u002Fredirects.json');r.sort(()=>Math.random()-.5).slice(0,40).forEach(x=>console.log(x.from,x.to))\" |\nwhile read -r from to; do\n  got=$(curl -s -o \u002Fdev\u002Fnull -w '%{http_code} %{redirect_url}' \"https:\u002F\u002Fdocs.example.com$from\")\n  [ \"$got\" = \"301 https:\u002F\u002Fdocs.example.com$to\" ] || { echo \"FAIL $from → $got\"; exit 1; }\ndone\n","bash",[18,1305,1306,1320,1342,1382,1438],{"__ignoreMap":67},[71,1307,1308,1311,1314,1317],{"class":73,"line":74},[71,1309,1310],{"class":257},"node",[71,1312,1313],{"class":166}," -e",[71,1315,1316],{"class":95}," \"const r=require('.\u002Fdist\u002Fredirects.json');r.sort(()=>Math.random()-.5).slice(0,40).forEach(x=>console.log(x.from,x.to))\"",[71,1318,1319],{"class":244}," |\n",[71,1321,1322,1325,1328,1331,1334,1336,1339],{"class":73,"line":81},[71,1323,1324],{"class":244},"while",[71,1326,1327],{"class":95}," read",[71,1329,1330],{"class":166}," -r",[71,1332,1333],{"class":95}," from",[71,1335,1101],{"class":95},[71,1337,1338],{"class":84},"; ",[71,1340,1341],{"class":244},"do\n",[71,1343,1344,1347,1349,1352,1355,1358,1361,1364,1367,1370,1373,1376,1379],{"class":73,"line":113},[71,1345,1346],{"class":84},"  got",[71,1348,328],{"class":244},[71,1350,1351],{"class":84},"$(",[71,1353,1354],{"class":257},"curl",[71,1356,1357],{"class":166}," -s",[71,1359,1360],{"class":166}," -o",[71,1362,1363],{"class":95}," \u002Fdev\u002Fnull",[71,1365,1366],{"class":166}," -w",[71,1368,1369],{"class":95}," '%{http_code} %{redirect_url}'",[71,1371,1372],{"class":95}," \"https:\u002F\u002Fdocs.example.com",[71,1374,1375],{"class":84},"$from",[71,1377,1378],{"class":95},"\"",[71,1380,1381],{"class":84},")\n",[71,1383,1384,1387,1389,1392,1394,1396,1399,1402,1404,1407,1410,1413,1416,1419,1421,1424,1426,1428,1430,1433,1436],{"class":73,"line":137},[71,1385,1386],{"class":84},"  [ ",[71,1388,1378],{"class":95},[71,1390,1391],{"class":84},"$got",[71,1393,1378],{"class":95},[71,1395,251],{"class":244},[71,1397,1398],{"class":95}," \"301 https:\u002F\u002Fdocs.example.com",[71,1400,1401],{"class":84},"$to",[71,1403,1378],{"class":95},[71,1405,1406],{"class":84}," ] ",[71,1408,1409],{"class":244},"||",[71,1411,1412],{"class":84}," { ",[71,1414,1415],{"class":166},"echo",[71,1417,1418],{"class":95}," \"FAIL ",[71,1420,1375],{"class":84},[71,1422,1423],{"class":95}," → ",[71,1425,1391],{"class":84},[71,1427,1378],{"class":95},[71,1429,1338],{"class":84},[71,1431,1432],{"class":166},"exit",[71,1434,1435],{"class":166}," 1",[71,1437,944],{"class":84},[71,1439,1440],{"class":73,"line":172},[71,1441,1442],{"class":244},"done\n",[14,1444,1445],{},"Forty random redirects per deploy take about four seconds and, over time, exercise the whole list. Run the full list nightly against production as well; at 3,140 entries it finishes in under five minutes and catches drift between the store and the source file.",[37,1447,1449],{"id":1448},"measured-impact","Measured Impact",[14,1451,1452],{},"A docs site migrated from Jekyll to Astro, producing 3,140 redirects, served from KeyValueStore:",[1454,1455,1456,1472],"table",{},[1457,1458,1459],"thead",{},[1460,1461,1462,1466,1469],"tr",{},[1463,1464,1465],"th",{},"Measure",[1463,1467,1468],{},"Before (S3 website routing rules, 50 max)",[1463,1470,1471],{},"After (Function + KVS)",[1473,1474,1475,1487,1497,1507,1518,1529],"tbody",{},[1460,1476,1477,1481,1484],{},[1478,1479,1480],"td",{},"Redirects supported",[1478,1482,1483],{},"50 (rest returned 404)",[1478,1485,1486],{},"3,140",[1460,1488,1489,1492,1495],{},[1478,1490,1491],{},"Chains in the list",[1478,1493,1494],{},"14",[1478,1496,567],{},[1460,1498,1499,1502,1505],{},[1478,1500,1501],{},"Redirect targets that 404",[1478,1503,1504],{},"3",[1478,1506,567],{},[1460,1508,1509,1512,1515],{},[1478,1510,1511],{},"Function time, p99",[1478,1513,1514],{},"—",[1478,1516,1517],{},"0.42 ms (with KVS lookup)",[1460,1519,1520,1523,1526],{},[1478,1521,1522],{},"404s from old URLs in edge logs, per day",[1478,1524,1525],{},"~1,900",[1478,1527,1528],{},"~35 (bots guessing)",[1460,1530,1531,1534,1537],{},[1478,1532,1533],{},"Search Console \"page with redirect\" errors",[1478,1535,1536],{},"212",[1478,1538,1539],{},"0 after recrawl",[544,1541,1542,1607],{},[547,1543,555,1548,555,1551,555,1554,555,1556],{"viewBox":1544,"role":550,"ariaLabelledBy":1545,"xmlns":554},"0 0 760 260",[1546,1547],"cfr-404-title","cfr-404-desc",[557,1549,1550],{"id":1546},"404s from old URLs after the migration",[561,1552,1553],{"id":1547},"A line of daily 404 responses for URLs from the old site over four weeks. It sits around 1,900 per day while only 50 redirects were supported, drops to about 35 per day the day the full KeyValueStore map deploys, and stays there.",[565,1555],{"x":567,"y":567,"width":568,"height":1252,"fill":570},[572,1557,575,1558,575,1561,575,1564,575,1567,575,1571,575,1575,575,1578,575,1583,575,1587,575,1593,575,1598,575,1603,555],{"style":574},[577,1559,1560],{"x":579,"y":580,"fill":581,"style":582},"Old-URL 404s per day, CloudFront logs",[73,1562],{"x1":1563,"y1":671,"x2":1221,"y2":671,"stroke":677,"style":1243},"70",[73,1565],{"x1":1563,"y1":1566,"x2":1563,"y2":671,"stroke":677,"style":1243},"50",[577,1568,567],{"x":1569,"y":1570,"fill":677,"style":1234},"62","214",[577,1572,1574],{"x":1569,"y":1573,"fill":677,"style":1234},"134","1,000",[577,1576,1577],{"x":1569,"y":593,"fill":677,"style":1234},"2,000",[1579,1580],"polyline",{"points":1581,"fill":632,"stroke":631,"style":1582},"90,62 140,58 190,64 240,60 290,66 340,61 390,207 440,207 490,208 540,207 590,207 640,208 690,207","stroke-width:2.5px",[73,1584],{"x1":1585,"y1":1566,"x2":1585,"y2":671,"stroke":625,"style":1586},"365","stroke-width:1.5px;stroke-dasharray:6 4",[577,1588,1592],{"x":1589,"y":1590,"fill":672,"style":1591},"371","64","font-size:11px;font-weight:700","full map deployed",[577,1594,1597],{"x":1595,"y":1596,"fill":649,"style":650},"200","84","~1,900 \u002F day",[577,1599,1602],{"x":1600,"y":1601,"fill":672,"style":650},"540","198","~35 \u002F day",[577,1604,1606],{"x":1605,"y":1229,"fill":677,"style":604},"395","week 1 to week 4 after the Jekyll-to-Astro migration",[700,1608,1609],{},"The residual 35 per day were bots probing paths that never existed — the right thing to 404.",[37,1611,1613],{"id":1612},"pitfalls-rollback","Pitfalls & Rollback",[42,1615,1616,1622,1628,1634,1640,1658,1668],{},[45,1617,1618,1621],{},[1185,1619,1620],{},"Routing rules on the website endpoint."," Limited to 50 and tied to a public bucket. Use the viewer function.",[45,1623,1624,1627],{},[1185,1625,1626],{},"Unresolved chains."," Each hop costs a round trip; resolve them in the build.",[45,1629,1630,1633],{},[1185,1631,1632],{},"Deleting pages before redirects exist."," Update the redirect store first, then remove old content.",[45,1635,1636,1639],{},[1185,1637,1638],{},"Case sensitivity."," CloudFront paths are case-sensitive; if old URLs had mixed case, add lowercase variants or normalise in the function.",[45,1641,1642,1645,1646,1649,1650,1653,1654,1657],{},[1185,1643,1644],{},"Query strings dropped."," A redirect built from ",[18,1647,1648],{},"req.uri"," alone loses ",[18,1651,1652],{},"?utm_source=…"," and search parameters. Append the original query string to the ",[18,1655,1656],{},"location"," unless the target intentionally discards it.",[45,1659,1660,1663,1664,1667],{},[1185,1661,1662],{},"Open redirects."," Never build a redirect target from request input such as a ",[18,1665,1666],{},"?next="," parameter; only map known sources to known targets.",[45,1669,1670,1673],{},[1185,1671,1672],{},"Rollback:"," KeyValueStore changes are versioned by ETag; re-applying the previous generated list restores it. The function itself can be rolled back by republishing the previous version.",[37,1675,1677],{"id":1676},"conclusion","Conclusion",[14,1679,1680],{},"On S3 and CloudFront, redirects belong in the viewer-request function next to the clean-URL rewrite: inline for a few hundred rules, KeyValueStore for thousands. Generating them from a single source, resolving chains and checking targets during the build, and sampling them after each deploy turned a migration that was leaking 1,900 broken old URLs a day into one that served 3,140 redirects in a single hop at under half a millisecond each.",[37,1682,1684],{"id":1683},"faq","FAQ",[1686,1687,1689],"h3",{"id":1688},"how-many-redirects-can-a-cloudfront-function-hold","How many redirects can a CloudFront Function hold?",[14,1691,1692],{},"A function's code is limited to 10 KB, which fits a few hundred short redirect pairs inline. For more, use CloudFront KeyValueStore, which holds up to 5 MB of key-value pairs that the function reads at request time.",[1686,1694,1696],{"id":1695},"should-redirects-be-301-or-302","Should redirects be 301 or 302?",[14,1698,1699],{},"Use 301 or 308 for permanent moves such as migrations, so browsers and search engines update. Use 302 or 307 for temporary redirects. Keep the browser cache lifetime of permanent redirects modest so mistakes can be corrected.",[1686,1701,1703],{"id":1702},"can-i-use-s3-object-redirects-instead","Can I use S3 object redirects instead?",[14,1705,1706],{},"S3 supports a per-object redirect header, but it only works through the website endpoint, which requires a public HTTP-only bucket. With a private bucket and Origin Access Control, handle redirects in CloudFront.",[1686,1708,1710],{"id":1709},"how-do-i-avoid-redirect-chains","How do I avoid redirect chains?",[14,1712,1713],{},"Resolve chains when generating the redirect list. If A redirects to B and B to C, emit A to C directly. A build-time check that no target is itself a source catches chains before deploy.",[37,1715,1717],{"id":1716},"related","Related",[42,1719,1720,1729,1734,1741,1748],{},[45,1721,1722,1725,1726,1728],{},[1185,1723,1724],{},"Parent:"," ",[26,1727,29],{"href":28}," — the S3 option in context.",[45,1730,1731,1733],{},[26,1732,34],{"href":33}," — the rewrite this function extends.",[45,1735,1736,1740],{},[26,1737,1739],{"href":1738},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fmigrating-between-static-site-generators\u002Fkeeping-redirects-working-after-an-ssg-migration\u002F","Keeping Redirects Working After an SSG Migration"," — building the redirect list itself.",[45,1742,1743,1747],{},[26,1744,1746],{"href":1745},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fcloudflare-pages-edge-caching-setup\u002Fconfiguring-redirects-on-cloudflare-pages\u002F","Configuring Redirects on Cloudflare Pages"," — the same list on a managed host.",[45,1749,1750,1754],{},[26,1751,1753],{"href":1752},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fmonitoring-static-sites-in-production\u002Flogging-404s-at-the-edge\u002F","Logging 404s at the Edge"," — finding the redirects you missed.",[1756,1757,1758],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}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}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":67,"searchDepth":81,"depth":81,"links":1760},[1761,1762,1763,1764,1765,1766,1767,1768,1769,1770,1771,1777],{"id":39,"depth":81,"text":40},{"id":56,"depth":81,"text":57},{"id":705,"depth":81,"text":706},{"id":1010,"depth":81,"text":1011},{"id":1191,"depth":81,"text":1192},{"id":1283,"depth":81,"text":1284},{"id":1294,"depth":81,"text":1295},{"id":1448,"depth":81,"text":1449},{"id":1612,"depth":81,"text":1613},{"id":1676,"depth":81,"text":1677},{"id":1683,"depth":81,"text":1684,"children":1772},[1773,1774,1775,1776],{"id":1688,"depth":113,"text":1689},{"id":1695,"depth":113,"text":1696},{"id":1702,"depth":113,"text":1703},{"id":1709,"depth":113,"text":1710},{"id":1716,"depth":81,"text":1717},[1779,1781,1784,1785],{"name":1780,"item":212},"Home",{"name":1782,"item":1783},"Production-Ready Deployment & CI\u002FCD Workflows","\u002Fproduction-ready-deployment-cicd-workflows\u002F",{"name":29,"item":28},{"name":5,"item":1786},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fself-hosting-static-sites\u002Fcloudfront-functions-for-redirects\u002F","2026-09-18","Serve hundreds of redirects for a static site on S3 from a CloudFront Function and KeyValueStore, generated from the build and tested on every deploy.","md",[1791,1792,1793,1794],{"q":1689,"a":1692},{"q":1696,"a":1699},{"q":1703,"a":1706},{"q":1710,"a":1713},{},true,"\u002Fproduction-ready-deployment-cicd-workflows\u002Fself-hosting-static-sites\u002Fcloudfront-functions-for-redirects",{"title":5,"description":1788},"production-ready-deployment-cicd-workflows\u002Fself-hosting-static-sites\u002Fcloudfront-functions-for-redirects\u002Findex","article","6eUkrActjOhgj5MeVWvMhwQ4nH0QyVBQ7gJH4I6CgSU",[1803,1806,1809,1812,1815,1818,1821,1824,1827,1830,1833,1836,1839,1842,1845,1848,1851,1854,1857,1860,1863,1866,1869,1872,1875,1878,1881,1884,1887,1889,1892,1895,1898,1901,1904,1907,1910,1913,1916,1919,1922,1925,1928,1931,1934,1937,1940,1943,1946,1949,1952,1955,1958,1961,1964,1967,1970,1973,1976,1979,1982,1985,1988,1991,1994,1997,2000,2003,2006,2009,2012,2015,2018,2021,2024,2027,2030,2033,2036,2039,2042,2045,2048,2051,2054,2057,2060,2063,2066,2069,2072,2075,2078,2081,2084,2087,2090,2093,2096,2099,2102,2105,2108,2111,2114,2117,2120,2123,2126,2129,2132,2135,2138,2141,2143,2146,2149,2152,2155,2158,2161,2164,2167,2170,2173,2176,2179,2182,2185,2188,2191,2194,2197,2200,2203,2206,2209,2212,2215,2218,2221,2224,2227,2230,2232,2235,2238,2241,2244,2247,2250,2253,2256,2259,2262,2265,2268,2271,2274,2277,2280,2283,2286,2289,2292,2295,2298,2301,2304,2307,2310,2312,2313,2316,2318,2321,2324,2327,2330,2333,2336,2339,2342],{"path":1804,"title":1805},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fastro-vs-eleventy-for-documentation-sites\u002Fastro-vs-eleventy-build-times-at-10000-pages","Astro vs Eleventy Build Times at 10,000 Pages",{"path":1807,"title":1808},"\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":1810,"title":1811},"\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":1813,"title":1814},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fastro-vs-eleventy-for-documentation-sites","Astro vs Eleventy for Documentation Sites",{"path":1816,"title":1817},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fastro-vs-eleventy-for-documentation-sites\u002Fshortcodes-vs-components-for-docs-authors","Shortcodes vs Components for Docs Authors",{"path":1819,"title":1820},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fastro-vs-eleventy-for-documentation-sites\u002Fsidebar-navigation-in-astro-and-eleventy","Sidebar Navigation in Astro and Eleventy",{"path":1822,"title":1823},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fdocs-frameworks-docusaurus-starlight-vitepress\u002Fcustomizing-starlight-without-forking-the-theme","Customizing Starlight Without Forking the Theme",{"path":1825,"title":1826},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fdocs-frameworks-docusaurus-starlight-vitepress\u002Fdocusaurus-vs-starlight-for-product-documentation","Docusaurus vs Starlight for Product Documentation",{"path":1828,"title":1829},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fdocs-frameworks-docusaurus-starlight-vitepress","Docs Frameworks: Docusaurus, Starlight and VitePress",{"path":1831,"title":1832},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fdocs-frameworks-docusaurus-starlight-vitepress\u002Fmdx-vs-markdoc-for-docs-content","MDX vs Markdoc for Docs Content",{"path":1834,"title":1835},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fdocs-frameworks-docusaurus-starlight-vitepress\u002Fmigrating-from-mkdocs-to-starlight","Migrating from MkDocs to Starlight",{"path":1837,"title":1838},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fdocs-frameworks-docusaurus-starlight-vitepress\u002Fversioned-documentation-with-docusaurus","Versioned Documentation with Docusaurus",{"path":1840,"title":1841},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fdocs-frameworks-docusaurus-starlight-vitepress\u002Fvitepress-for-library-documentation","VitePress for Library Documentation",{"path":1843,"title":1844},"\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":1846,"title":1847},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fhugo-build-times-for-large-repositories\u002Fhugo-partialcached-for-faster-builds","Hugo partialCached for Faster Builds",{"path":1849,"title":1850},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fhugo-build-times-for-large-repositories","Hugo Build Times for Large Repositories",{"path":1852,"title":1853},"\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":1855,"title":1856},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fhugo-build-times-for-large-repositories\u002Freducing-hugo-memory-usage-on-ci-runners","Reducing Hugo Memory Usage on CI Runners",{"path":1858,"title":1859},"\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":1861,"title":1862},"\u002Fchoosing-the-right-static-site-generator-for-production","Choosing the Right Static Site Generator for Production",{"path":1864,"title":1865},"\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":1867,"title":1868},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fjekyll-plugin-ecosystem","Jekyll Plugin Ecosystem",{"path":1870,"title":1871},"\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":1873,"title":1874},"\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":1876,"title":1877},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fjekyll-plugin-ecosystem\u002Fspeeding-up-slow-jekyll-builds","Speeding Up Slow Jekyll Builds",{"path":1879,"title":1880},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fjekyll-plugin-ecosystem\u002Fupgrading-jekyll-and-ruby-versions-safely","Upgrading Jekyll and Ruby Versions Safely",{"path":1882,"title":1883},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fmigrating-between-static-site-generators\u002Fconverting-front-matter-at-scale-during-migration","Converting Front Matter at Scale During Migration",{"path":1885,"title":1886},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fmigrating-between-static-site-generators","Migrating Between Static Site Generators",{"path":1888,"title":1739},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fmigrating-between-static-site-generators\u002Fkeeping-redirects-working-after-an-ssg-migration",{"path":1890,"title":1891},"\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":1893,"title":1894},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fmigrating-between-static-site-generators\u002Fmigrating-from-gatsby-to-astro","Migrating from Gatsby to Astro",{"path":1896,"title":1897},"\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":1899,"title":1900},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fmigrating-between-static-site-generators\u002Fmigrating-wordpress-to-a-static-site-generator","Migrating WordPress to a Static Site Generator",{"path":1902,"title":1903},"\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":1905,"title":1906},"\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":1908,"title":1909},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fnextjs-static-export-for-content-sites","Next.js Static Export for Content Sites",{"path":1911,"title":1912},"\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":1914,"title":1915},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fnextjs-static-export-for-content-sites\u002Fnextjs-app-router-static-export-limitations","Next.js App Router Static Export Limitations",{"path":1917,"title":1918},"\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":1920,"title":1921},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fnextjs-static-export-for-content-sites\u002Foptimizing-images-in-nextjs-static-export","Optimizing Images in Next.js Static Export",{"path":1923,"title":1924},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fsearch-for-static-sites\u002Fadding-pagefind-to-an-astro-site","Adding Pagefind to an Astro Site",{"path":1926,"title":1927},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fsearch-for-static-sites\u002Fbuilding-a-lunr-index-at-build-time-in-eleventy","Building a Lunr Index at Build Time in Eleventy",{"path":1929,"title":1930},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fsearch-for-static-sites","Search for Static Sites",{"path":1932,"title":1933},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fsearch-for-static-sites\u002Findexing-hugo-sites-with-pagefind","Indexing Hugo Sites with Pagefind",{"path":1935,"title":1936},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fsearch-for-static-sites\u002Fmultilingual-search-on-static-sites","Multilingual Search on Static Sites",{"path":1938,"title":1939},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fsearch-for-static-sites\u002Fpagefind-vs-algolia-docsearch","Pagefind vs Algolia DocSearch",{"path":1941,"title":1942},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fsearch-for-static-sites\u002Fsearch-index-size-budgets-for-large-docs","Search Index Size Budgets for Large Docs",{"path":1944,"title":1945},"\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":1947,"title":1948},"\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":1950,"title":1951},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fssg-framework-selection-matrix\u002Fevaluating-ssg-accessibility-defaults","Evaluating SSG Accessibility Defaults",{"path":1953,"title":1954},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fssg-framework-selection-matrix","SSG Framework Selection Matrix",{"path":1956,"title":1957},"\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":1959,"title":1960},"\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":1962,"title":1963},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fssg-framework-selection-matrix\u002Ftotal-cost-of-ownership-for-static-site-generators","Total Cost of Ownership for Static Site Generators",{"path":1965,"title":1966},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fcdn-caching-rules-for-ssgs\u002Fcache-busting-with-content-hashed-filenames","Cache Busting with Content-Hashed Filenames",{"path":1968,"title":1969},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fcdn-caching-rules-for-ssgs","CDN Caching Rules for SSGs",{"path":1971,"title":1972},"\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":1974,"title":1975},"\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":1977,"title":1978},"\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":1980,"title":1981},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fcdn-caching-rules-for-ssgs\u002Fstale-while-revalidate-for-static-html","Stale-While-Revalidate for Static HTML",{"path":1983,"title":1984},"\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":1986,"title":1987},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fcumulative-layout-shift-fixes-for-static-sites\u002Ffixing-cls-from-cookie-banners","Fixing CLS from Cookie Banners",{"path":1989,"title":1990},"\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":1992,"title":1993},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fcumulative-layout-shift-fixes-for-static-sites\u002Ffixing-cls-from-sticky-headers-and-anchor-links","Fixing CLS from Sticky Headers and Anchor Links",{"path":1995,"title":1996},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fcumulative-layout-shift-fixes-for-static-sites","Cumulative Layout Shift Fixes for Static Sites",{"path":1998,"title":1999},"\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":2001,"title":2002},"\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":2004,"title":2005},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Ffont-loading-strategies-for-static-sites\u002Ffont-display-optional-vs-swap","font-display: optional vs swap",{"path":2007,"title":2008},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Ffont-loading-strategies-for-static-sites","Font Loading Strategies for Static Sites",{"path":2010,"title":2011},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Ffont-loading-strategies-for-static-sites\u002Fmetric-matched-fallback-fonts-with-size-adjust","Metric-Matched Fallback Fonts with size-adjust",{"path":2013,"title":2014},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Ffont-loading-strategies-for-static-sites\u002Fpreloading-fonts-without-double-downloads","Preloading Fonts Without Double Downloads",{"path":2016,"title":2017},"\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":2019,"title":2020},"\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":2022,"title":2023},"\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":2025,"title":2026},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fimage-optimization-pipelines-in-astro\u002Fgenerating-open-graph-images-at-build-time","Generating Open Graph Images at Build Time",{"path":2028,"title":2029},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fimage-optimization-pipelines-in-astro","Image Optimization Pipelines in Astro",{"path":2031,"title":2032},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fimage-optimization-pipelines-in-astro\u002Flazy-loading-images-without-hurting-lcp","Lazy-Loading Images Without Hurting LCP",{"path":2034,"title":2035},"\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":2037,"title":2038},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fimage-optimization-pipelines-in-astro\u002Fresponsive-images-with-srcset-in-eleventy","Responsive Images with srcset in Eleventy",{"path":2040,"title":2041},"\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":2043,"title":2044},"\u002Fperformance-optimization-core-web-vitals-for-ssgs","Core Web Vitals Optimization for SSGs",{"path":2046,"title":2047},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fjavascript-hydration-partial-rendering\u002Fastro-islands-vs-full-hydration-performance","Astro Islands vs Full Hydration Performance",{"path":2049,"title":2050},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fjavascript-hydration-partial-rendering\u002Fdeferring-hydration-with-client-visible-in-astro","Deferring Hydration with client:visible in Astro",{"path":2052,"title":2053},"\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":2055,"title":2056},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fjavascript-hydration-partial-rendering","JavaScript Hydration & Partial Rendering",{"path":2058,"title":2059},"\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":2061,"title":2062},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fjavascript-hydration-partial-rendering\u002Freplacing-react-islands-with-web-components","Replacing React Islands with Web Components",{"path":2064,"title":2065},"\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":2067,"title":2068},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Flargest-contentful-paint-optimization-for-static-sites\u002Ffixing-lcp-on-text-heavy-documentation-pages","Fixing LCP on Text-Heavy Documentation Pages",{"path":2070,"title":2071},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Flargest-contentful-paint-optimization-for-static-sites","Largest Contentful Paint Optimization for Static Sites",{"path":2073,"title":2074},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Flargest-contentful-paint-optimization-for-static-sites\u002Fmeasuring-lcp-subparts-with-devtools","Measuring LCP Subparts with DevTools",{"path":2076,"title":2077},"\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":2079,"title":2080},"\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":2082,"title":2083},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fperformance-budgets-and-lighthouse-ci\u002Fcomparing-lab-and-field-data-with-crux","Comparing Lab and Field Data with CrUX",{"path":2085,"title":2086},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fperformance-budgets-and-lighthouse-ci","Performance Budgets and Lighthouse CI",{"path":2088,"title":2089},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fperformance-budgets-and-lighthouse-ci\u002Freducing-lighthouse-score-variance-in-ci","Reducing Lighthouse Score Variance in CI",{"path":2091,"title":2092},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fperformance-budgets-and-lighthouse-ci\u002Frunning-webpagetest-scripts-against-preview-deploys","Running WebPageTest Scripts Against Preview Deploys",{"path":2094,"title":2095},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fperformance-budgets-and-lighthouse-ci\u002Fsetting-up-lighthouse-ci-for-a-static-site","Setting Up Lighthouse CI for a Static Site",{"path":2097,"title":2098},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fperformance-budgets-and-lighthouse-ci\u002Ftracking-bundle-size-per-pull-request","Tracking Bundle Size per Pull Request",{"path":2100,"title":2101},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fperformance-budgets-and-lighthouse-ci\u002Fwriting-a-performance-budget-that-fails-builds","Writing a Performance Budget That Fails Builds",{"path":2103,"title":2104},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fresource-hints-and-navigation-speed\u002Fauditing-unused-preloads","Auditing Unused Preloads",{"path":2106,"title":2107},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fresource-hints-and-navigation-speed","Resource Hints and Navigation Speed",{"path":2109,"title":2110},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fresource-hints-and-navigation-speed\u002Finstant-navigation-with-speculation-rules","Instant Navigation with Speculation Rules",{"path":2112,"title":2113},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fresource-hints-and-navigation-speed\u002Fpreconnect-vs-dns-prefetch-on-static-sites","Preconnect vs DNS-Prefetch on Static Sites",{"path":2115,"title":2116},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fresource-hints-and-navigation-speed\u002Fprefetching-links-in-astro","Prefetching Links in Astro",{"path":2118,"title":2119},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fresource-hints-and-navigation-speed\u002Fview-transitions-on-multi-page-static-sites","View Transitions on Multi-Page Static Sites",{"path":2121,"title":2122},"\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":2124,"title":2125},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fthird-party-script-performance-on-static-sites","Third-Party Script Performance on Static Sites",{"path":2127,"title":2128},"\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":2130,"title":2131},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fthird-party-script-performance-on-static-sites\u002Floading-google-tag-manager-without-hurting-inp","Loading Google Tag Manager Without Hurting INP",{"path":2133,"title":2134},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fthird-party-script-performance-on-static-sites\u002Frunning-third-party-scripts-in-a-web-worker-with-partytown","Running Third-Party Scripts in a Web Worker with Partytown",{"path":2136,"title":2137},"\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":2139,"title":2140},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fcloudflare-pages-edge-caching-setup\u002Fautomating-eleventy-deployments-with-cloudflare-pages","Automating Eleventy Deployments on Cloudflare Pages",{"path":2142,"title":1746},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fcloudflare-pages-edge-caching-setup\u002Fconfiguring-redirects-on-cloudflare-pages",{"path":2144,"title":2145},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fcloudflare-pages-edge-caching-setup\u002Fcustom-domains-and-tls-on-cloudflare-pages","Custom Domains and TLS on Cloudflare Pages",{"path":2147,"title":2148},"\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":2150,"title":2151},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fcloudflare-pages-edge-caching-setup","Cloudflare Pages Edge Caching Setup",{"path":2153,"title":2154},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fcloudflare-pages-edge-caching-setup\u002Fmigrating-from-cloudflare-pages-to-workers-static-assets","Migrating from Cloudflare Pages to Workers Static Assets",{"path":2156,"title":2157},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fcontent-workflows-for-documentation-teams\u002Fchecking-links-in-pull-requests","Checking Links in Pull Requests",{"path":2159,"title":2160},"\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":2162,"title":2163},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fcontent-workflows-for-documentation-teams\u002Feditorial-checks-with-vale-in-ci","Editorial Checks with Vale in CI",{"path":2165,"title":2166},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fcontent-workflows-for-documentation-teams","Content Workflows for Documentation Teams",{"path":2168,"title":2169},"\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":2171,"title":2172},"\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":2174,"title":2175},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fgithub-actions-for-automated-ssg-builds\u002Fbuilding-astro-sites-with-github-actions","Building Astro Sites with GitHub Actions",{"path":2177,"title":2178},"\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":2180,"title":2181},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fgithub-actions-for-automated-ssg-builds\u002Fdeploying-to-github-pages-with-actions","Deploying to GitHub Pages with Actions",{"path":2183,"title":2184},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fgithub-actions-for-automated-ssg-builds\u002Fdeploying-to-multiple-environments-from-one-workflow","Deploying to Multiple Environments From One Workflow",{"path":2186,"title":2187},"\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":2189,"title":2190},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fgithub-actions-for-automated-ssg-builds","GitHub Actions for Automated SSG Builds",{"path":2192,"title":2193},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fgithub-actions-for-automated-ssg-builds\u002Fmatrix-builds-for-multi-site-monorepos","Matrix Builds for Multi-Site Monorepos",{"path":2195,"title":2196},"\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":2198,"title":2199},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fincremental-builds-and-build-caching-for-ssgs\u002Fenabling-incremental-builds-in-eleventy","Enabling Incremental Builds in Eleventy",{"path":2201,"title":2202},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fincremental-builds-and-build-caching-for-ssgs\u002Fincremental-builds-in-astro-with-the-content-layer","Incremental Builds in Astro with the Content Layer",{"path":2204,"title":2205},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fincremental-builds-and-build-caching-for-ssgs","Incremental Builds and Build Caching for SSGs",{"path":2207,"title":2208},"\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":2210,"title":2211},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fincremental-builds-and-build-caching-for-ssgs\u002Fremote-caching-with-turborepo-for-ssg-monorepos","Remote Caching with Turborepo for SSG Monorepos",{"path":2213,"title":2214},"\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":2216,"title":2217},"\u002Fproduction-ready-deployment-cicd-workflows","Production-Ready Deployment & CI\u002FCD for SSGs",{"path":2219,"title":2220},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fmonitoring-static-sites-in-production\u002Falerting-on-cache-hit-ratio-drops","Alerting on Cache Hit Ratio Drops",{"path":2222,"title":2223},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fmonitoring-static-sites-in-production\u002Fbuilding-a-core-web-vitals-dashboard-from-rum-data","Building a Core Web Vitals Dashboard from RUM Data",{"path":2225,"title":2226},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fmonitoring-static-sites-in-production\u002Fcrawling-for-broken-links-on-a-schedule","Crawling for Broken Links on a Schedule",{"path":2228,"title":2229},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fmonitoring-static-sites-in-production","Monitoring Static Sites in Production",{"path":2231,"title":1753},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fmonitoring-static-sites-in-production\u002Flogging-404s-at-the-edge",{"path":2233,"title":2234},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fmonitoring-static-sites-in-production\u002Fuptime-and-synthetic-checks-for-static-sites","Uptime and Synthetic Checks for Static Sites",{"path":2236,"title":2237},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fnetlify-vs-vercel-deployment-strategies\u002Fconfiguring-vercel-for-hugo-and-eleventy","Configuring Vercel for Hugo and Eleventy",{"path":2239,"title":2240},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fnetlify-vs-vercel-deployment-strategies","Netlify vs Vercel Deployment Strategies",{"path":2242,"title":2243},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fnetlify-vs-vercel-deployment-strategies\u002Fnetlify-build-hooks-for-content-updates","Netlify Build Hooks for Content Updates",{"path":2245,"title":2246},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fnetlify-vs-vercel-deployment-strategies\u002Fnetlify-redirects-and-rewrites-for-static-sites","Netlify Redirects and Rewrites for Static Sites",{"path":2248,"title":2249},"\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":2251,"title":2252},"\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":2254,"title":2255},"\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":2257,"title":2258},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fpreview-environments-for-pull-requests\u002Fcleaning-up-stale-preview-deployments","Cleaning Up Stale Preview Deployments",{"path":2260,"title":2261},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fpreview-environments-for-pull-requests","Preview Environments for Pull Requests",{"path":2263,"title":2264},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fpreview-environments-for-pull-requests\u002Fpassword-protecting-preview-deployments","Password-Protecting Preview Deployments",{"path":2266,"title":2267},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fpreview-environments-for-pull-requests\u002Fpreviewing-headless-cms-drafts","Previewing Headless CMS Drafts",{"path":2269,"title":2270},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fpreview-environments-for-pull-requests\u002Fvisual-regression-testing-on-preview-deploys","Visual Regression Testing on Preview Deploys",{"path":2272,"title":2273},"\u002Fproduction-ready-deployment-cicd-workflows\u002Frollbacks-and-deploy-safety-for-static-sites\u002Fatomic-deploys-vs-incremental-uploads","Atomic Deploys vs Incremental Uploads",{"path":2275,"title":2276},"\u002Fproduction-ready-deployment-cicd-workflows\u002Frollbacks-and-deploy-safety-for-static-sites\u002Fcanary-releases-for-static-sites","Canary Releases for Static Sites",{"path":2278,"title":2279},"\u002Fproduction-ready-deployment-cicd-workflows\u002Frollbacks-and-deploy-safety-for-static-sites\u002Ffeature-flags-on-static-sites","Feature Flags on Static Sites",{"path":2281,"title":2282},"\u002Fproduction-ready-deployment-cicd-workflows\u002Frollbacks-and-deploy-safety-for-static-sites","Rollbacks and Deploy Safety for Static Sites",{"path":2284,"title":2285},"\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":2287,"title":2288},"\u002Fproduction-ready-deployment-cicd-workflows\u002Frollbacks-and-deploy-safety-for-static-sites\u002Frunning-smoke-tests-against-a-preview-url","Running Smoke Tests Against a Preview URL",{"path":2290,"title":2291},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fsecurity-headers-for-static-sites\u002Fauditing-npm-dependencies-in-ssg-pipelines","Auditing npm Dependencies in SSG Pipelines",{"path":2293,"title":2294},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fsecurity-headers-for-static-sites\u002Fenabling-hsts-and-preload-safely","Enabling HSTS and Preload Safely",{"path":2296,"title":2297},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fsecurity-headers-for-static-sites\u002Fhash-based-csp-for-inline-scripts-in-astro","Hash-Based CSP for Inline Scripts in Astro",{"path":2299,"title":2300},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fsecurity-headers-for-static-sites","Security Headers and Hardening for Static Sites",{"path":2302,"title":2303},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fsecurity-headers-for-static-sites\u002Fsecuring-deploy-credentials-with-github-oidc","Securing Deploy Credentials with GitHub OIDC",{"path":2305,"title":2306},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fsecurity-headers-for-static-sites\u002Fsubresource-integrity-for-third-party-assets","Subresource Integrity for Third-Party Assets",{"path":2308,"title":2309},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fsecurity-headers-for-static-sites\u002Fwriting-a-content-security-policy-for-a-static-site","Writing a Content Security Policy for a Static Site",{"path":2311,"title":34},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fself-hosting-static-sites\u002Fclean-urls-and-trailing-slashes-on-s3",{"path":1797,"title":5},{"path":2314,"title":2315},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fself-hosting-static-sites\u002Fdeploying-a-static-site-to-s3-and-cloudfront","Deploying a Static Site to S3 and CloudFront",{"path":2317,"title":29},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fself-hosting-static-sites",{"path":2319,"title":2320},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fself-hosting-static-sites\u002Fserving-a-static-site-with-caddy","Serving a Static Site with Caddy",{"path":2322,"title":2323},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fself-hosting-static-sites\u002Fserving-a-static-site-with-nginx","Serving a Static Site with Nginx",{"path":2325,"title":2326},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fself-hosting-static-sites\u002Fzero-downtime-deploys-with-symlink-swaps","Zero-Downtime Deploys with Symlink Swaps",{"path":2328,"title":2329},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fserverless-functions-for-static-sites\u002Fadding-a-contact-form-with-cloudflare-workers","Adding a Contact Form with Cloudflare Workers",{"path":2331,"title":2332},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fserverless-functions-for-static-sites\u002Fhandling-form-submissions-on-a-static-site","Handling Form Submissions on a Static Site",{"path":2334,"title":2335},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fserverless-functions-for-static-sites","Serverless Functions for Static Sites",{"path":2337,"title":2338},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fserverless-functions-for-static-sites\u002Fnetlify-functions-vs-cloudflare-workers","Netlify Functions vs Cloudflare Workers",{"path":2340,"title":2341},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fserverless-functions-for-static-sites\u002Fprotecting-a-static-site-behind-authentication","Protecting a Static Site Behind Authentication",{"path":2343,"title":2344},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fserverless-functions-for-static-sites\u002Fproxying-third-party-apis-from-an-edge-function","Proxying Third-Party APIs from an Edge Function",1789722847789]