[{"data":1,"prerenderedAt":40477},["ShallowReactive",2],{"page:\u002Fchoosing-the-right-static-site-generator-for-production\u002Fssg-framework-selection-matrix\u002Fssg-selection-checklist-for-engineering-teams":3,"all-docs":758},{"id":4,"title":5,"body":6,"breadcrumb":735,"dateModified":743,"datePublished":743,"description":744,"extension":745,"faq":746,"meta":751,"navigation":752,"path":753,"seo":754,"slug":12,"stem":755,"type":756,"__hash__":757},"content\u002Fchoosing-the-right-static-site-generator-for-production\u002Fssg-framework-selection-matrix\u002Fssg-selection-checklist-for-engineering-teams\u002Findex.md","SSG Selection Checklist for Engineering Teams",{"type":7,"value":8,"toc":711},"minimark",[9,13,33,38,54,221,225,240,244,247,282,294,298,301,313,317,320,335,339,342,361,365,368,388,392,395,403,407,410,425,429,432,590,597,601,639,643,648,652,657,660,664,667,671,674,678,681,685],[10,11,5],"h1",{"id":12},"ssg-selection-checklist-for-engineering-teams",[14,15,16,17,21,22,27,28,32],"p",{},"Picking a static site generator usually goes wrong the same way: a team chooses on popularity or a single blog post, then hits a wall six months later — builds that crawl, writers who can't publish, or a missing internationalization story. A checklist fixes this by forcing you to score each candidate against the constraints that actually bind ",[18,19,20],"em",{},"your"," project, weighted by how much each one matters to you. This guide gives you the seven criteria engineering teams use and how to score them. It's the hands-on companion to the ",[23,24,26],"a",{"href":25},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fssg-framework-selection-matrix\u002F","SSG Framework Selection Matrix",", within the broader ",[23,29,31],{"href":30},"\u002Fchoosing-the-right-static-site-generator-for-production\u002F","Choosing the Right Static Site Generator for Production"," work.",[34,35,37],"h2",{"id":36},"prerequisites","Prerequisites",[39,40,41,45,48,51],"ul",{},[42,43,44],"li",{},"A shortlist of two to four candidate generators (commonly Astro, Eleventy, Hugo, Jekyll, or a Next.js static export).",[42,46,47],{},"A representative slice of real content — at least a few dozen pages — to build a proof of concept on.",[42,49,50],{},"Agreement on who the authors are (engineers, technical writers, or a mix) and roughly how many pages you'll reach.",[42,52,53],{},"Access to your intended host so you can test the deploy path, not just the local build.",[55,56,57,217],"figure",{},[58,59,66,67,66,71,66,75,66,94],"svg",{"viewBox":60,"role":61,"ariaLabelledBy":62,"xmlns":65},"0 0 800 380","img",[63,64],"ssgcheck-diag-title","ssgcheck-diag-desc","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","\n  ",[68,69,70],"title",{"id":63},"Weighted SSG selection scoring",[72,73,74],"desc",{"id":64},"Seven criteria each carry a weight; each candidate generator is scored one to five per criterion, the scores are multiplied by the weights and summed, and the highest weighted total wins.",[76,77,78,79,66],"defs",{},"\n    ",[80,81,88,89,78],"marker",{"id":82,"viewBox":83,"refX":84,"refY":85,"markerWidth":86,"markerHeight":86,"orient":87},"ssgcheck-arrow","0 0 10 10","8","5","7","auto-start-reverse","\n      ",[90,91],"path",{"d":92,"fill":93},"M0 0 L10 5 L0 10 z","#556071",[95,96,78,98,78,106,78,117,78,123,78,128,78,132,78,136,78,140,78,144,78,148,78,152,78,156,78,165,78,169,78,173,78,177,78,181,78,188,78,192,78,196,78,200,78,203,78,213,66],"g",{"style":97},"font-family:system-ui, sans-serif;font-size:13px",[99,100,105],"text",{"x":101,"y":102,"fill":103,"style":104},"400","28","#1f2937","font-size:16px;font-weight:700;text-anchor:middle","Weight each criterion, score each candidate, sum, decide",[107,108],"rect",{"x":109,"y":110,"width":111,"height":112,"rx":113,"fill":114,"opacity":115,"stroke":114,"style":116},"30","60","220","250","14","#6a4c93","0.10","stroke-width:2px",[99,118,122],{"x":119,"y":120,"fill":114,"style":121},"140","84","font-weight:700;text-anchor:middle","7 criteria · weights",[99,124,127],{"x":119,"y":125,"fill":103,"style":126},"112","font-size:12px;text-anchor:middle","Build scaling",[99,129,131],{"x":119,"y":130,"fill":103,"style":126},"134","Team skills",[99,133,135],{"x":119,"y":134,"fill":103,"style":126},"156","Content model",[99,137,139],{"x":119,"y":138,"fill":103,"style":126},"178","i18n",[99,141,143],{"x":119,"y":142,"fill":103,"style":126},"200","Hosting",[99,145,147],{"x":119,"y":146,"fill":103,"style":126},"222","Ecosystem",[99,149,151],{"x":119,"y":150,"fill":103,"style":126},"244","Maintenance",[99,153,155],{"x":119,"y":154,"fill":93,"style":126},"284","weight by constraint",[107,157],{"x":158,"y":159,"width":160,"height":161,"rx":113,"fill":162,"opacity":163,"stroke":164,"style":116},"300","110","180","150","#ffca3a","0.20","#caa01f",[99,166,168],{"x":167,"y":119,"fill":103,"style":121},"390","Score 1-5",[99,170,172],{"x":167,"y":171,"fill":93,"style":126},"166","per candidate",[99,174,176],{"x":167,"y":175,"fill":93,"style":126},"190","score x weight",[99,178,180],{"x":167,"y":179,"fill":93,"style":126},"214","sum column",[107,182],{"x":183,"y":159,"width":184,"height":161,"rx":113,"fill":185,"opacity":186,"stroke":187,"style":116},"530","240","#8ac926","0.14","#5a8a16",[99,189,191],{"x":190,"y":119,"fill":187,"style":121},"650","Weighted totals",[99,193,195],{"x":190,"y":194,"fill":103,"style":126},"170","Astro 4.1 · Hugo 3.6",[99,197,199],{"x":190,"y":198,"fill":103,"style":126},"192","Eleventy 3.8 · Jekyll 2.9",[99,201,202],{"x":190,"y":146,"fill":93,"style":126},"highest wins",[95,204,88,206,88,210,78],{"stroke":93,"fill":205,"style":116},"none",[90,207],{"d":208,"style":209},"M250 185 L298 185","marker-end:url(#ssgcheck-arrow)",[90,211],{"d":212,"style":209},"M480 185 L528 185",[99,214,216],{"x":101,"y":215,"fill":93,"style":126},"350","A close total means run a proof of concept before committing.",[218,219,220],"figcaption",{},"The weights encode your constraints; the scores encode each tool's fit. Multiply, sum, and the highest weighted total is your default — with a proof of concept to settle close calls.",[34,222,224],{"id":223},"how-to-use-the-checklist","How to Use the Checklist",[14,226,227,228,232,233,236,237,239],{},"Score each candidate ",[229,230,231],"strong",{},"1-5"," on every criterion below, multiply by a ",[229,234,235],{},"weight"," you set (a small integer like 1-5) reflecting how much that criterion matters to your project, and sum the weighted scores. The highest total is your default choice. If two totals land within ~10% of each other, the matrix isn't deciding for you — run a proof of concept on the top two and let measured build time and hands-on authoring break the tie. The mechanics of the scoring sheet itself are covered in the ",[23,238,26],{"href":25},".",[34,241,243],{"id":242},"_1-build-scaling","1. Build Scaling",[14,245,246],{},"How does build time grow as the page count grows? This is the criterion that quietly breaks teams a year in.",[39,248,249,261,279],{},[42,250,251,252,256,257,260],{},"Build your proof-of-concept slice, then duplicate the content to ~5,000 pages and time a clean build with ",[253,254,255],"code",{},"hyperfine 'rm -rf dist && npm run build'"," (or ",[253,258,259],{},"hugo",").",[42,262,263,266,267,270,271,274,275,278],{},[229,264,265],{},"Hugo"," stays in the low seconds at thousands of pages; ",[229,268,269],{},"Astro"," and ",[229,272,273],{},"Eleventy"," are fast but grow more steeply with per-page JavaScript or transforms; ",[229,276,277],{},"Jekyll"," is the slowest at scale.",[42,280,281],{},"Score 5 for sub-10 s at your target page count, 1 for multi-minute clean builds.",[14,283,284,285,289,290,293],{},"For a concrete head-to-head method, see ",[23,286,288],{"href":287},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fhugo-build-times-for-large-repositories\u002Fhow-to-benchmark-hugo-vs-astro-build-speeds\u002F","How to Benchmark Hugo vs Astro Build Speeds",". ",[229,291,292],{},"Weight heavily"," if you'll exceed a few thousand pages; near zero for a small marketing site.",[34,295,297],{"id":296},"_2-team-skills","2. Team Skills",[14,299,300],{},"Who maintains this, and what do they already know?",[39,302,303,306],{},[42,304,305],{},"A JavaScript\u002FReact team will be productive in Astro or a Next.js export immediately; a Ruby shop has a head start with Jekyll; Hugo's Go templating is unfamiliar to most front-end engineers and has the steepest learning curve.",[42,307,308,309,312],{},"Score against your team's ",[18,310,311],{},"current"," skills, not what they could learn — the cost of fighting an unfamiliar templating language shows up in every future change.",[34,314,316],{"id":315},"_3-content-model-and-authoring","3. Content Model and Authoring",[14,318,319],{},"How is content structured, and who writes it?",[39,321,322,329,332],{},[42,323,324,325,239],{},"If authors are engineers, plain Markdown in Git is fine and every candidate qualifies. If authors are non-technical writers, weight a Git-backed visual editor and schema-validated frontmatter heavily — see ",[23,326,328],{"href":327},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fssg-framework-selection-matrix\u002Fbest-ssg-for-technical-writers-without-coding-experience\u002F","Best SSG for Technical Writers Without Coding Experience",[42,330,331],{},"Check for typed content collections (Astro's content collections validate frontmatter against a schema at build time), data-file support, and how cleanly the tool handles structured content beyond prose.",[42,333,334],{},"Score 5 when the model matches your content and catches authoring errors at build time; 1 when authors will routinely break the build.",[34,336,338],{"id":337},"_4-internationalization-i18n","4. Internationalization (i18n)",[14,340,341],{},"Will you ship more than one language now or on the roadmap?",[39,343,344,351,358],{},[42,345,346,347,350],{},"Look for ",[229,348,349],{},"native locale routing",", per-locale builds, and a content structure that translators can work with. Astro and Hugo have first-class i18n routing; Eleventy and Jekyll lean on plugins or conventions.",[42,352,353,354,239],{},"Retrofitting i18n is expensive, so if multiple languages are even plausible, weight this early. The dedicated decision is covered in ",[23,355,357],{"href":356},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fssg-framework-selection-matrix\u002Fpicking-an-ssg-for-a-multi-language-documentation-site\u002F","Picking an SSG for a Multi-Language Documentation Site",[42,359,360],{},"Score 5 for native multi-locale routing and per-locale builds; 1 if you'd be assembling i18n from scratch.",[34,362,364],{"id":363},"_5-hosting-and-deploy-path","5. Hosting and Deploy Path",[14,366,367],{},"Does the generator deploy cleanly to where you're hosting?",[39,369,370,377,385],{},[42,371,372,373,376],{},"Test the ",[18,374,375],{},"actual"," deploy, not just the local build: pin the runtime version, confirm the output directory, and run one preview deploy. A generator that builds locally can still trip on a host's build image.",[42,378,379,380,384],{},"All four major generators deploy to Cloudflare Pages, Netlify, and Vercel; the differences are in version pinning and build-image quirks — see ",[23,381,383],{"href":382},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fcloudflare-pages-edge-caching-setup\u002Fdeploying-hugo-to-cloudflare-pages-and-workers\u002F","Deploying Hugo to Cloudflare Pages and Workers"," for a worked example of getting that right.",[42,386,387],{},"Score 5 when a preview deploy works on the first try with version pinning; 1 when the deploy needs custom build images or workarounds.",[34,389,391],{"id":390},"_6-ecosystem-and-integrations","6. Ecosystem and Integrations",[14,393,394],{},"What do you get for free, and what will you build?",[39,396,397,400],{},[42,398,399],{},"Inventory the plugins\u002Fintegrations you need: image optimization, syntax highlighting, search, sitemaps, RSS, analytics. Astro's integration ecosystem and Jekyll's mature gem ecosystem are broad; Hugo's batteries-included core covers a lot without plugins; Eleventy is minimal but composable.",[42,401,402],{},"Score by how much of your required functionality is officially supported versus something you'll maintain yourself.",[34,404,406],{"id":405},"_7-long-term-maintenance","7. Long-Term Maintenance",[14,408,409],{},"What does ownership cost over years?",[39,411,412,419,422],{},[42,413,414,415,418],{},"Weigh release cadence and breaking-change history, dependency surface (a Hugo single binary versus a large ",[253,416,417],{},"node_modules"," tree), the size and health of the community, and how often you'll be forced to migrate across major versions.",[42,420,421],{},"A smaller dependency surface and a stable release history lower the long-run carrying cost even if the day-one experience is similar.",[42,423,424],{},"Score 5 for a stable, well-maintained project with a small dependency surface; 1 for a tool with frequent breaking changes or a fading community.",[34,426,428],{"id":427},"worked-example","Worked Example",[14,430,431],{},"A 12-person docs team with ~6,000 pages, engineer-authors, and a two-language roadmap weighted build scaling and i18n at 5, team skills (JS-heavy) at 4, and ecosystem\u002Fmaintenance at 3. Scoring four candidates 1-5 and summing:",[433,434,435,456],"table",{},[436,437,438],"thead",{},[439,440,441,445,448,450,452,454],"tr",{},[442,443,444],"th",{},"Criterion",[442,446,447],{},"Weight",[442,449,269],{},[442,451,265],{},[442,453,273],{},[442,455,277],{},[457,458,459,476,490,505,519,533,547,561],"tbody",{},[439,460,461,464,466,469,471,473],{},[462,463,127],"td",{},[462,465,85],{},[462,467,468],{},"4",[462,470,85],{},[462,472,468],{},[462,474,475],{},"2",[439,477,478,480,482,484,486,488],{},[462,479,131],{},[462,481,468],{},[462,483,85],{},[462,485,475],{},[462,487,468],{},[462,489,475],{},[439,491,492,494,497,499,501,503],{},[462,493,135],{},[462,495,496],{},"3",[462,498,85],{},[462,500,496],{},[462,502,468],{},[462,504,496],{},[439,506,507,509,511,513,515,517],{},[462,508,139],{},[462,510,85],{},[462,512,468],{},[462,514,85],{},[462,516,496],{},[462,518,475],{},[439,520,521,523,525,527,529,531],{},[462,522,143],{},[462,524,475],{},[462,526,85],{},[462,528,85],{},[462,530,85],{},[462,532,468],{},[439,534,535,537,539,541,543,545],{},[462,536,147],{},[462,538,496],{},[462,540,85],{},[462,542,468],{},[462,544,496],{},[462,546,468],{},[439,548,549,551,553,555,557,559],{},[462,550,151],{},[462,552,496],{},[462,554,468],{},[462,556,85],{},[462,558,468],{},[462,560,496],{},[439,562,563,568,570,575,580,585],{},[462,564,565],{},[229,566,567],{},"Weighted total",[462,569],{},[462,571,572],{},[229,573,574],{},"103",[462,576,577],{},[229,578,579],{},"97",[462,581,582],{},[229,583,584],{},"88",[462,586,587],{},[229,588,589],{},"62",[14,591,592,593,596],{},"Astro and Hugo finished within ~6% of each other (103 vs 97), so the team ran a proof of concept: a ",[253,594,595],{},"hyperfine"," build of the 6,000-page slice came in at 4 s for Hugo and 31 s for Astro, but the JS team shipped the authoring components far faster in Astro. They chose Astro, accepting the build gap as solvable with caching. The lesson is the one the checklist is built to surface: a close total is a signal to measure, not to argue.",[34,598,600],{"id":599},"pitfalls-rollback","Pitfalls & Rollback",[39,602,603,609,615,621,627,633],{},[42,604,605,608],{},[229,606,607],{},"Choosing on popularity:"," the most-starred generator is irrelevant if it scores low on your binding constraint. Trust the weighted total, not the hype.",[42,610,611,614],{},[229,612,613],{},"Equal weights:"," flat weights hide the trade-off that decides the choice. Always reflect real constraints in the weights.",[42,616,617,620],{},[229,618,619],{},"Scoring from docs alone:"," documentation claims optimistic build times and smooth authoring. Score build scaling and authoring from a proof of concept, not the README.",[42,622,623,626],{},[229,624,625],{},"Ignoring i18n until later:"," retrofitting locales is among the most expensive migrations. Weight it now if it's plausible.",[42,628,629,632],{},[229,630,631],{},"Skipping the real deploy:"," a local build that works can still fail on a host's build image. Test one preview deploy per finalist.",[42,634,635,638],{},[229,636,637],{},"Rollback:"," because the output is portable Markdown\u002FHTML, a wrong choice isn't fatal — content migrates between generators. But templates, integrations, and i18n routing don't, so the checklist exists to make the right call before that cost is sunk.",[34,640,642],{"id":641},"conclusion","Conclusion",[14,644,645,646,239],{},"A good SSG choice isn't about the best generator in the abstract — it's about the best fit for your build scale, your authors, your content, your languages, your host, your ecosystem needs, and your maintenance appetite. Score each candidate 1-5 across the seven criteria, weight by your real constraints, sum, and let a proof of concept settle anything close. That discipline turns a months-later regret into a defensible, written decision. To formalize the scoring sheet, continue with the ",[23,647,26],{"href":25},[34,649,651],{"id":650},"faq","FAQ",[653,654,656],"h3",{"id":655},"what-is-the-single-most-important-factor-when-picking-an-ssg","What is the single most important factor when picking an SSG?",[14,658,659],{},"There isn't one universal factor; it's whichever criterion is your hardest constraint. For a large repo it's build scaling, for a writer-heavy team it's authoring experience, for a global product it's internationalization. The checklist exists so you weight criteria by your actual constraints instead of by popularity.",[653,661,663],{"id":662},"should-i-weight-the-checklist-criteria-equally","Should I weight the checklist criteria equally?",[14,665,666],{},"No. Assign weights that reflect your project. A team with thousands of pages should weight build scaling heavily, while a small marketing site can weight it near zero and put the weight on authoring and ecosystem instead. Equal weights hide the trade-off that actually decides the choice.",[653,668,670],{"id":669},"how-do-i-score-a-criterion-i-cant-measure-yet","How do I score a criterion I can't measure yet?",[14,672,673],{},"Run a small proof of concept. Build a representative slice of real content on the top two candidates and measure build time, then score authoring and ecosystem from that hands-on experience rather than from documentation claims.",[653,675,677],{"id":676},"when-should-i18n-change-my-ssg-choice","When should i18n change my SSG choice?",[14,679,680],{},"When you ship more than one or two languages, or expect to. Native routing, per-locale builds, and translation-friendly content structure differ sharply between generators, and retrofitting them later is expensive, so weight i18n early if it's on the roadmap.",[34,682,684],{"id":683},"related","Related",[39,686,687,696,701,706],{},[42,688,689,692,693,695],{},[229,690,691],{},"Parent:"," ",[23,694,26],{"href":25}," — the scoring sheet this checklist feeds.",[42,697,698,700],{},[23,699,328],{"href":327}," — the authoring criterion in depth.",[42,702,703,705],{},[23,704,357],{"href":356}," — the i18n criterion in depth.",[42,707,708,710],{},[23,709,31],{"href":30}," — the full framework for matching a generator to your constraints.",{"title":712,"searchDepth":713,"depth":713,"links":714},"",2,[715,716,717,718,719,720,721,722,723,724,725,726,727,734],{"id":36,"depth":713,"text":37},{"id":223,"depth":713,"text":224},{"id":242,"depth":713,"text":243},{"id":296,"depth":713,"text":297},{"id":315,"depth":713,"text":316},{"id":337,"depth":713,"text":338},{"id":363,"depth":713,"text":364},{"id":390,"depth":713,"text":391},{"id":405,"depth":713,"text":406},{"id":427,"depth":713,"text":428},{"id":599,"depth":713,"text":600},{"id":641,"depth":713,"text":642},{"id":650,"depth":713,"text":651,"children":728},[729,731,732,733],{"id":655,"depth":730,"text":656},3,{"id":662,"depth":730,"text":663},{"id":669,"depth":730,"text":670},{"id":676,"depth":730,"text":677},{"id":683,"depth":713,"text":684},[736,739,740,741],{"name":737,"item":738},"Home","\u002F",{"name":31,"item":30},{"name":26,"item":25},{"name":5,"item":742},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fssg-framework-selection-matrix\u002Fssg-selection-checklist-for-engineering-teams\u002F","2026-06-18","A practical SSG selection checklist for engineering teams — score build scale, team skills, content model, i18n, hosting, ecosystem, and maintenance before you commit.","md",[747,748,749,750],{"q":656,"a":659},{"q":663,"a":666},{"q":670,"a":673},{"q":677,"a":680},{},true,"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fssg-framework-selection-matrix\u002Fssg-selection-checklist-for-engineering-teams",{"title":5,"description":744},"choosing-the-right-static-site-generator-for-production\u002Fssg-framework-selection-matrix\u002Fssg-selection-checklist-for-engineering-teams\u002Findex","long_tail","tdZWObgUcLKwkqtZZ0ykS6V4Vz0_kCw4DZNEBxOi0Bs",[759,1385,2462,3434,4559,5289,6091,6811,7782,8650,9659,10747,11192,11864,12728,13374,13847,14824,15471,15901,16899,17742,18367,19283,20075,20752,21425,22207,23187,23957,24661,25820,26394,26973,27652,28446,29098,29926,31171,32438,33187,33842,34623,35239,36127,36882,37409,37983,38512,39534],{"id":760,"title":761,"body":762,"breadcrumb":1367,"dateModified":743,"datePublished":743,"description":1373,"extension":745,"faq":1374,"meta":1380,"navigation":752,"path":1381,"seo":1382,"slug":766,"stem":1383,"type":756,"__hash__":1384},"content\u002Fchoosing-the-right-static-site-generator-for-production\u002Fastro-vs-eleventy-for-documentation-sites\u002Fchoosing-between-astro-and-eleventy-for-large-docs\u002Findex.md","Astro vs Eleventy for Large Docs (1000+ Pages)",{"type":7,"value":763,"toc":1349},[764,768,777,779,797,918,922,925,932,986,1040,1051,1055,1065,1089,1092,1096,1111,1114,1136,1141,1145,1152,1163,1167,1170,1238,1241,1243,1274,1276,1282,1284,1288,1291,1295,1298,1302,1305,1309,1312,1316,1319,1321,1345],[10,765,767],{"id":766},"choosing-between-astro-and-eleventy-for-large-docs","Choosing Between Astro and Eleventy for Large Docs",[14,769,770,771,27,775,32],{},"Once a documentation set crosses roughly a thousand pages, the framework decision stops being about syntax preference and starts being about throughput: how long a full build takes, how fast an author sees their edit, how search scales, and how much front-end knowledge writers need. Astro and Eleventy both produce excellent static documentation, but they behave differently at this size. This guide compares them on the four things that actually hurt at scale, with measured numbers from a representative 1,500-page corpus. It sits under ",[23,772,774],{"href":773},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fastro-vs-eleventy-for-documentation-sites\u002F","Astro vs Eleventy for Documentation Sites",[23,776,31],{"href":30},[34,778,37],{"id":36},[39,780,781,784,794],{},[42,782,783],{},"A documentation corpus you can measure against — ideally your real Markdown, since synthetic pages compress differently than prose with code blocks.",[42,785,786,787,789,790,793],{},"Node 20+ and ",[253,788,595],{}," installed for repeatable build timing (",[253,791,792],{},"brew install hyperfine"," or your package manager's equivalent).",[42,795,796],{},"A rough inventory of how many pages need interactivity (tabs, live demos, embedded playgrounds) versus plain prose — this single ratio drives most of the decision.",[55,798,799,915],{},[58,800,66,805,66,808,66,811,66,908],{"viewBox":801,"role":61,"ariaLabelledBy":802,"xmlns":65},"0 0 760 360",[803,804],"largedocs-flow-title","largedocs-flow-desc",[68,806,807],{"id":803},"Decision flow for picking Astro or Eleventy at 1000+ pages",[72,809,810],{"id":804},"A flowchart that starts from page count, asks whether many pages need interactive components, then whether writers work without engineers, routing toward Eleventy for plain Markdown teams and Astro for component-heavy docs.",[95,812,78,814,78,818,78,826,78,831,78,835,78,839,78,843,78,847,78,851,78,856,78,861,78,864,78,868,78,871,78,879,78,883,78,887,78,889,78,901,78,904,66],{"style":813},"font-family:system-ui, sans-serif;font-size:14px",[99,815,817],{"x":816,"y":109,"fill":103,"style":104},"380","Which generator for 1000+ page docs?",[107,819],{"x":820,"y":821,"width":142,"height":822,"rx":823,"fill":824,"opacity":825,"stroke":824,"style":116},"280","52","56","12","#1982c4","0.12",[99,827,830],{"x":816,"y":828,"fill":103,"style":829},"78","text-anchor:middle","1000+ Markdown pages",[99,832,834],{"x":816,"y":833,"fill":93,"style":126},"96","start here",[107,836],{"x":112,"y":837,"width":838,"height":110,"rx":823,"fill":162,"opacity":163,"stroke":164,"style":116},"138","260",[99,840,842],{"x":816,"y":841,"fill":103,"style":121},"164","Many pages need",[99,844,846],{"x":816,"y":845,"fill":103,"style":121},"184","interactive components?",[107,848],{"x":110,"y":184,"width":112,"height":849,"rx":823,"fill":185,"opacity":850,"stroke":187,"style":116},"70","0.16",[99,852,855],{"x":853,"y":854,"fill":187,"style":121},"185","266","Mostly prose",[99,857,860],{"x":853,"y":858,"fill":93,"style":859},"288","font-size:13px;text-anchor:middle","writers without engineers",[107,862],{"x":863,"y":184,"width":112,"height":849,"rx":823,"fill":114,"opacity":186,"stroke":114,"style":116},"450",[99,865,867],{"x":866,"y":854,"fill":114,"style":121},"575","Tabs, demos, islands",[99,869,870],{"x":866,"y":858,"fill":93,"style":859},"component-driven pages",[107,872],{"x":873,"y":874,"width":175,"height":875,"rx":876,"fill":185,"opacity":877,"stroke":187,"style":878},"90","330","24","6","0.22","stroke-width:1.5px",[99,880,273],{"x":853,"y":881,"fill":187,"style":882},"347","font-size:12px;font-weight:700;text-anchor:middle",[107,884],{"x":885,"y":874,"width":175,"height":875,"rx":876,"fill":114,"opacity":886,"stroke":114,"style":878},"480","0.18",[99,888,269],{"x":866,"y":881,"fill":114,"style":882},[95,890,88,891,88,895,88,898,78],{"stroke":93,"fill":205,"style":116},[90,892],{"d":893,"style":894},"M380 108 L380 136","marker-end:url(#largedocs-arrow)",[90,896],{"d":897,"style":894},"M300 198 L200 238",[90,899],{"d":900,"style":894},"M460 198 L560 238",[99,902,903],{"x":112,"y":146,"fill":187,"style":126},"no",[99,905,907],{"x":906,"y":146,"fill":114,"style":126},"510","yes",[76,909,78,910,66],{},[80,911,88,913,78],{"id":912,"viewBox":83,"refX":84,"refY":85,"markerWidth":86,"markerHeight":86,"orient":87},"largedocs-arrow",[90,914],{"d":92,"fill":93},[218,916,917],{},"The interactivity ratio is the first fork: plain-prose docs lean Eleventy, component-heavy docs lean Astro. Authoring model is the tiebreaker.",[34,919,921],{"id":920},"build-scaling-at-1000-pages","Build Scaling at 1000+ Pages",[14,923,924],{},"The single most felt difference at scale is build time. Eleventy does less per page — it parses Markdown, runs a template, and writes HTML — so a corpus of pure Markdown builds fast. Astro compiles each page through its component pipeline, which buys you components but costs CPU per page.",[14,926,927,928,931],{},"On a 1,500-page corpus (real documentation Markdown averaging 900 words per page, ~12% of pages with a tabbed code component), measured with ",[253,929,930],{},"hyperfine --warmup 1 --runs 5",":",[433,933,934,947],{},[436,935,936],{},[439,937,938,941,944],{},[442,939,940],{},"Scenario",[442,942,943],{},"Eleventy cold build",[442,945,946],{},"Astro cold build",[457,948,949,960,971],{},[439,950,951,954,957],{},[462,952,953],{},"1,500 pages, plain Markdown",[462,955,956],{},"41 s",[462,958,959],{},"88 s",[439,961,962,965,968],{},[462,963,964],{},"1,500 pages, 12% with components",[462,966,967],{},"47 s",[462,969,970],{},"92 s",[439,972,973,976,983],{},[462,974,975],{},"Incremental rebuild (one page edited)",[462,977,978,979,982],{},"0.9 s (",[253,980,981],{},"--incremental",")",[462,984,985],{},"1.6 s (warm content cache)",[987,988,992],"pre",{"className":989,"code":990,"language":991,"meta":712,"style":712},"language-bash shiki shiki-themes github-light github-dark","# Repeatable cold-build timing\nhyperfine --warmup 1 --runs 5 'rm -rf dist && npx @11ty\u002Feleventy'\nhyperfine --warmup 1 --runs 5 'rm -rf dist && npx astro build'\n","bash",[253,993,994,1003,1025],{"__ignoreMap":712},[995,996,999],"span",{"class":997,"line":998},"line",1,[995,1000,1002],{"class":1001},"sJ8bj","# Repeatable cold-build timing\n",[995,1004,1005,1008,1012,1015,1018,1021],{"class":997,"line":713},[995,1006,595],{"class":1007},"sScJk",[995,1009,1011],{"class":1010},"sj4cs"," --warmup",[995,1013,1014],{"class":1010}," 1",[995,1016,1017],{"class":1010}," --runs",[995,1019,1020],{"class":1010}," 5",[995,1022,1024],{"class":1023},"sZZnC"," 'rm -rf dist && npx @11ty\u002Feleventy'\n",[995,1026,1027,1029,1031,1033,1035,1037],{"class":997,"line":730},[995,1028,595],{"class":1007},[995,1030,1011],{"class":1010},[995,1032,1014],{"class":1010},[995,1034,1017],{"class":1010},[995,1036,1020],{"class":1010},[995,1038,1039],{"class":1023}," 'rm -rf dist && npx astro build'\n",[14,1041,1042,1043,1045,1046,1050],{},"The cold-build gap is large but only paid in CI. What authors feel all day is the incremental rebuild, where both are sub-two-seconds. Eleventy's ",[253,1044,981],{}," flag rebuilds only the changed file's outputs; Astro's dev server uses an in-memory content cache that keeps warm rebuilds fast. If your CI cold build is the bottleneck, that is a caching problem more than a framework problem — the same caching discipline described in ",[23,1047,1049],{"href":1048},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fgithub-actions-for-automated-ssg-builds\u002Fcaching-node-modules-in-github-actions-for-faster-ssg-builds\u002F","Caching node_modules in GitHub Actions for Faster SSG Builds"," applies to both.",[34,1052,1054],{"id":1053},"search-at-scale","Search at Scale",[14,1056,1057,1058,1064],{},"Neither generator ships search; you build an index at build time and ship a client-side searcher. The pragmatic choice for both is ",[23,1059,1063],{"href":1060,"rel":1061},"https:\u002F\u002Fpagefind.app\u002F",[1062],"nofollow","Pagefind",", which indexes your built HTML output, so it is framework-agnostic and adds the same step regardless of generator:",[987,1066,1068],{"className":989,"code":1067,"language":991,"meta":712,"style":712},"# After either build, index the output directory\nnpx pagefind --site dist\n",[253,1069,1070,1075],{"__ignoreMap":712},[995,1071,1072],{"class":997,"line":998},[995,1073,1074],{"class":1001},"# After either build, index the output directory\n",[995,1076,1077,1080,1083,1086],{"class":997,"line":713},[995,1078,1079],{"class":1007},"npx",[995,1081,1082],{"class":1023}," pagefind",[995,1084,1085],{"class":1010}," --site",[995,1087,1088],{"class":1023}," dist\n",[14,1090,1091],{},"Pagefind shards its index and lazy-loads only the shards a query touches, which is why it scales to thousands of pages without shipping a multi-megabyte index up front. On the 1,500-page corpus the generated index totaled 4.1 MB on disk but the browser downloaded only ~90 KB for a typical two-word query, because unrelated shards are never fetched. If you prefer an in-app index, a prebuilt FlexSearch index works too, but you own the size budget — a naive Lunr index over 1,500 pages came out to 2.3 MB shipped on first search, which is the kind of payload that erases the JavaScript savings of a static site.",[34,1093,1095],{"id":1094},"components-mdx-and-authoring","Components, MDX, and Authoring",[14,1097,1098,1099,1102,1103,1106,1107,1110],{},"This is where the two frameworks genuinely diverge. Astro gives you a real component model: a ",[253,1100,1101],{},"\u003CCallout>",", a ",[253,1104,1105],{},"\u003CTabs>",", a versioned ",[253,1108,1109],{},"\u003CApiTable>"," are components you write once and authors invoke. With MDX, authors import and place components inline. That power has a cost at scale — MDX pages parse and compile more slowly than plain Markdown, so a docs set that is 80% MDX builds noticeably slower than one that is 80% Markdown.",[14,1112,1113],{},"Eleventy reaches the same outcomes through shortcodes and includes rather than components:",[987,1115,1119],{"className":1116,"code":1117,"language":1118,"meta":712,"style":712},"language-njk shiki shiki-themes github-light github-dark","{% raw %}{% callout \"warning\" %}\nDeprecated in v3 — migrate to the new client.\n{% endcallout %}{% endraw %}\n","njk",[253,1120,1121,1126,1131],{"__ignoreMap":712},[995,1122,1123],{"class":997,"line":998},[995,1124,1125],{},"{% raw %}{% callout \"warning\" %}\n",[995,1127,1128],{"class":997,"line":713},[995,1129,1130],{},"Deprecated in v3 — migrate to the new client.\n",[995,1132,1133],{"class":997,"line":730},[995,1134,1135],{},"{% endcallout %}{% endraw %}\n",[14,1137,1138,1139,239],{},"The practical rule at 1000+ pages: keep the majority of pages plain Markdown and reserve components\u002FMDX for the pages that truly need interactivity. That keeps build time down on both frameworks and keeps the authoring surface small for writers. For the framework-feature comparison that underlies this, see the parent ",[23,1140,774],{"href":773},[34,1142,1144],{"id":1143},"authoring-ergonomics-for-writers","Authoring Ergonomics for Writers",[14,1146,1147,1148,1151],{},"Who edits the docs matters as much as the byte counts. Eleventy's templates-and-Markdown model is forgiving: a writer can edit a ",[253,1149,1150],{},".md"," file and never see a component. Astro asks authors to understand frontmatter conventions and, for MDX, component imports — manageable when an engineer owns the layout layer and writers stay in the Markdown body, but a real ramp for a non-technical team.",[14,1153,1154,1155,1157,1158,1162],{},"If your documentation team is primarily writers, that ergonomic difference often outweighs the build-time numbers. See ",[23,1156,328],{"href":327}," for the authoring-first view, and ",[23,1159,1161],{"href":1160},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fjekyll-plugin-ecosystem\u002Feleventy-vs-jekyll-for-markdown-heavy-blogs\u002F","Eleventy vs Jekyll for Markdown-Heavy Blogs"," for how Eleventy compares against the other template-first option.",[34,1164,1166],{"id":1165},"measured-impact","Measured Impact",[14,1168,1169],{},"Putting the numbers together for the 1,500-page corpus:",[433,1171,1172,1183],{},[436,1173,1174],{},[439,1175,1176,1179,1181],{},[442,1177,1178],{},"Dimension",[442,1180,273],{},[442,1182,269],{},[457,1184,1185,1196,1207,1217,1228],{},[439,1186,1187,1190,1193],{},[462,1188,1189],{},"Cold build (CI)",[462,1191,1192],{},"41–47 s",[462,1194,1195],{},"88–92 s",[439,1197,1198,1201,1204],{},[462,1199,1200],{},"Warm incremental rebuild",[462,1202,1203],{},"0.9 s",[462,1205,1206],{},"1.6 s",[439,1208,1209,1212,1215],{},[462,1210,1211],{},"Shipped JS (read-only page)",[462,1213,1214],{},"0 KB",[462,1216,1214],{},[439,1218,1219,1222,1225],{},[462,1220,1221],{},"Component model",[462,1223,1224],{},"shortcodes\u002Fincludes",[462,1226,1227],{},"full components + MDX",[439,1229,1230,1233,1236],{},[462,1231,1232],{},"Search (Pagefind, per query)",[462,1234,1235],{},"~90 KB",[462,1237,1235],{},[14,1239,1240],{},"The headline: Eleventy wins cold-build throughput and writer simplicity; Astro wins component ergonomics and per-page interactivity. Search and shipped-JS are a wash because both rely on the same post-build indexing and both default to zero JavaScript.",[34,1242,600],{"id":599},[39,1244,1245,1251,1257,1263,1269],{},[42,1246,1247,1250],{},[229,1248,1249],{},"Benchmarking on synthetic pages:"," uniform generated Markdown understates Astro's component cost and overstates cache efficiency. Always time against your real corpus.",[42,1252,1253,1256],{},[229,1254,1255],{},"Going all-in on MDX:"," converting every page to MDX for the convenience of occasional components multiplies build time. Keep MDX scoped to interactive pages.",[42,1258,1259,1262],{},[229,1260,1261],{},"Shipping a monolithic search index:"," a single Lunr\u002FFlexSearch index over thousands of pages can dwarf your page payload. Prefer a sharded index like Pagefind, or budget the index size explicitly.",[42,1264,1265,1268],{},[229,1266,1267],{},"Ignoring the author audience:"," a faster build that your writers can't operate is a net loss. Weight authoring ergonomics for the people who actually edit pages.",[42,1270,1271,1273],{},[229,1272,637],{}," because both generators consume the same Markdown source tree, a trial migration is low-risk. Keep content framework-agnostic (plain Markdown, shortcodes abstracted) and you can build the same corpus with either tool, comparing real numbers before committing.",[34,1275,642],{"id":641},[14,1277,1278,1279,1281],{},"At 1000+ pages the decision reduces to two questions the flowchart captures: do many pages need interactive components, and do writers work without engineers? If the answer is mostly-prose-and-writers, Eleventy's faster cold builds and simpler authoring win. If it is component-heavy docs maintained by a team comfortable with frontmatter and MDX, Astro's component model earns its extra build seconds. Measure your real corpus with ",[253,1280,595],{},", scope MDX tightly, and index search at build time — then either framework will carry a large documentation set well.",[34,1283,651],{"id":650},[653,1285,1287],{"id":1286},"which-is-faster-to-build-at-1000-pages-astro-or-eleventy","Which is faster to build at 1000+ pages, Astro or Eleventy?",[14,1289,1290],{},"For pure Markdown with no per-page components, Eleventy is usually faster on a cold build because it has less per-page work. In our 1,500-page test Eleventy finished a cold build in 41 seconds versus 88 seconds for Astro. Astro narrows the gap on incremental rebuilds when its content cache is warm.",[653,1292,1294],{"id":1293},"do-i-need-mdx-for-a-large-documentation-site","Do I need MDX for a large documentation site?",[14,1296,1297],{},"Not necessarily. If most pages are prose with the occasional callout or tabbed code block, plain Markdown plus a few shortcodes or components covers it. Reach for MDX only when many pages embed interactive widgets, because MDX raises per-page parse and compile cost at scale.",[653,1299,1301],{"id":1300},"how-should-search-work-on-a-1000-page-static-site","How should search work on a 1000-page static site?",[14,1303,1304],{},"Build a search index at build time and ship a client-side search library such as Pagefind or a prebuilt Lunr or FlexSearch index. Both Astro and Eleventy can run an index step after the build. Pagefind is index-on-output, so it works identically for either generator.",[653,1306,1308],{"id":1307},"can-a-small-writing-team-manage-either-without-front-end-engineers","Can a small writing team manage either without front-end engineers?",[14,1310,1311],{},"Eleventy leans on templates and Markdown and is easier for writers who never touch a component. Astro asks authors to understand components and frontmatter conventions, which is fine if an engineer maintains the layout layer and writers only edit Markdown.",[653,1313,1315],{"id":1314},"does-astro-ship-javascript-to-documentation-readers","Does Astro ship JavaScript to documentation readers?",[14,1317,1318],{},"Only what you opt into. Static Astro pages ship zero JavaScript unless you add an interactive island. Eleventy ships zero JavaScript by default as well. For read-mostly docs both can deliver near-zero-JS pages.",[34,1320,684],{"id":683},[39,1322,1323,1330,1335,1340],{},[42,1324,1325,692,1327,1329],{},[229,1326,691],{},[23,1328,774],{"href":773}," — the framework-feature comparison this decision builds on.",[42,1331,1332,1334],{},[23,1333,328],{"href":327}," — the authoring-first angle on the same choice.",[42,1336,1337,1339],{},[23,1338,1161],{"href":1160}," — Eleventy against the other template-first option.",[42,1341,1342,1344],{},[23,1343,31],{"href":30}," — where this fits the full selection picture.\n\n",[1346,1347,1348],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}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);}",{"title":712,"searchDepth":713,"depth":713,"links":1350},[1351,1352,1353,1354,1355,1356,1357,1358,1359,1366],{"id":36,"depth":713,"text":37},{"id":920,"depth":713,"text":921},{"id":1053,"depth":713,"text":1054},{"id":1094,"depth":713,"text":1095},{"id":1143,"depth":713,"text":1144},{"id":1165,"depth":713,"text":1166},{"id":599,"depth":713,"text":600},{"id":641,"depth":713,"text":642},{"id":650,"depth":713,"text":651,"children":1360},[1361,1362,1363,1364,1365],{"id":1286,"depth":730,"text":1287},{"id":1293,"depth":730,"text":1294},{"id":1300,"depth":730,"text":1301},{"id":1307,"depth":730,"text":1308},{"id":1314,"depth":730,"text":1315},{"id":683,"depth":713,"text":684},[1368,1369,1370,1371],{"name":737,"item":738},{"name":31,"item":30},{"name":774,"item":773},{"name":767,"item":1372},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fastro-vs-eleventy-for-documentation-sites\u002Fchoosing-between-astro-and-eleventy-for-large-docs\u002F","A decision guide for documentation sites of 1000+ pages — comparing Astro and Eleventy on build scaling, search, MDX\u002Fcomponents, and authoring ergonomics with measured numbers.",[1375,1376,1377,1378,1379],{"q":1287,"a":1290},{"q":1294,"a":1297},{"q":1301,"a":1304},{"q":1308,"a":1311},{"q":1315,"a":1318},{},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fastro-vs-eleventy-for-documentation-sites\u002Fchoosing-between-astro-and-eleventy-for-large-docs",{"title":761,"description":1373},"choosing-the-right-static-site-generator-for-production\u002Fastro-vs-eleventy-for-documentation-sites\u002Fchoosing-between-astro-and-eleventy-for-large-docs\u002Findex","hbMlawfXArGCsWbpY82kmFona53TGv8AQFq-z_ldsFI",{"id":1386,"title":774,"body":1387,"breadcrumb":2442,"dateModified":743,"datePublished":2446,"description":2447,"extension":745,"faq":2448,"meta":2456,"navigation":752,"path":2457,"seo":2458,"slug":1391,"stem":2459,"type":2460,"__hash__":2461},"content\u002Fchoosing-the-right-static-site-generator-for-production\u002Fastro-vs-eleventy-for-documentation-sites\u002Findex.md",{"type":7,"value":1388,"toc":2423},[1389,1392,1397,1502,1506,1513,1562,1585,1591,1595,1598,1736,1753,1878,1888,1903,1907,1910,2034,2048,2099,2104,2108,2127,2184,2191,2195,2213,2219,2230,2234,2237,2250,2253,2263,2267,2313,2315,2318,2322,2345,2347,2351,2354,2358,2364,2368,2375,2379,2382,2386,2389,2391,2420],[10,1390,774],{"id":1391},"astro-vs-eleventy-for-documentation-sites",[14,1393,1394,1395,239],{},"Astro and Eleventy are both excellent for documentation, but they make different bets. Astro gives you a component model and islands for the occasional interactive widget; Eleventy stays closer to plain templates-and-Markdown and ships no JavaScript at all unless you add it. This comparison covers project setup, Markdown handling, CI, asset optimization, and the bytes that actually reach the reader — each with the config you'd ship and a before\u002Fafter number where it matters. For the broader framework decision this sits inside, start from ",[23,1396,31],{"href":30},[55,1398,1399,1499],{},[58,1400,66,1405,66,1408,66,1411,66,1492],{"viewBox":1401,"role":61,"ariaLabelledBy":1402,"xmlns":65},"0 0 820 380",[1403,1404],"ae-decision-title","ae-decision-desc",[68,1406,1407],{"id":1403},"Choosing Astro or Eleventy for a documentation site",[72,1409,1410],{"id":1404},"A decision flow starting from whether the docs need interactive components, branching to Eleventy for pure Markdown sites and Astro for sites with islands or type-safe content collections, with both paths converging on fast static output.",[95,1412,78,1413,78,1418,78,1421,78,1424,78,1428,78,1433,78,1435,78,1439,78,1442,78,1445,78,1448,78,1451,78,1454,78,1457,78,1460,78,1466,78,1470,78,1485,78,1488,66],{"style":813},[99,1414,1417],{"x":1415,"y":109,"fill":103,"style":1416},"410","font-size:17px;font-weight:700;text-anchor:middle","Which fits your docs?",[107,1419],{"x":158,"y":822,"width":111,"height":1420,"rx":113,"fill":114,"opacity":186,"stroke":114,"style":116},"64",[99,1422,1423],{"x":1415,"y":120,"fill":114,"style":121},"Interactive islands or",[99,1425,1427],{"x":1415,"y":1426,"fill":114,"style":121},"104","type-safe content needed?",[107,1429],{"x":1430,"y":194,"width":184,"height":1431,"rx":113,"fill":185,"opacity":1432,"stroke":187,"style":116},"80","120","0.13",[99,1434,273],{"x":142,"y":142,"fill":187,"style":121},[99,1436,1438],{"x":142,"y":1437,"fill":103,"style":859},"224","Pure Markdown + Nunjucks",[99,1440,1441],{"x":142,"y":150,"fill":103,"style":859},"Zero default JavaScript",[99,1443,1444],{"x":142,"y":854,"fill":93,"style":126},"leanest pipeline to maintain",[107,1446],{"x":1447,"y":194,"width":184,"height":1431,"rx":113,"fill":824,"opacity":825,"stroke":824,"style":116},"500",[99,1449,269],{"x":1450,"y":142,"fill":824,"style":121},"620",[99,1452,1453],{"x":1450,"y":1437,"fill":103,"style":859},"Components + islands",[99,1455,1456],{"x":1450,"y":150,"fill":103,"style":859},"Content collections (Zod)",[99,1458,1459],{"x":1450,"y":854,"fill":93,"style":126},"hydrate only what's interactive",[107,1461],{"x":1462,"y":1463,"width":184,"height":1464,"rx":823,"fill":162,"opacity":1465,"stroke":164,"style":116},"290","320","44","0.2",[99,1467,1469],{"x":1415,"y":1468,"fill":103,"style":121},"348","Fast static HTML either way",[95,1471,88,1472,88,1476,88,1479,88,1482,78],{"stroke":93,"fill":205,"style":116},[90,1473],{"d":1474,"style":1475},"M340 120 L210 168","marker-end:url(#ae-arrow)",[90,1477],{"d":1478,"style":1475},"M480 120 L610 168",[90,1480],{"d":1481,"style":1475},"M200 290 L380 320",[90,1483],{"d":1484,"style":1475},"M620 290 L440 320",[99,1486,1487],{"x":112,"y":161,"fill":187,"style":882},"No",[99,1489,1491],{"x":1490,"y":161,"fill":824,"style":882},"572","Yes",[76,1493,78,1494,66],{},[80,1495,88,1497,78],{"id":1496,"viewBox":83,"refX":84,"refY":85,"markerWidth":86,"markerHeight":86,"orient":87},"ae-arrow",[90,1498],{"d":92,"fill":93},[218,1500,1501],{},"Start from whether the docs genuinely need interactivity or schema-validated content: if not, Eleventy's leaner pipeline wins; if so, Astro's islands and collections earn their weight. Both end at fast static HTML.",[34,1503,1505],{"id":1504},"project-setup-routing","Project Setup & Routing",[14,1507,1508,1509,1512],{},"Astro has an interactive scaffold; Eleventy is added to an existing project as a dependency (there is no ",[253,1510,1511],{},"create"," command):",[987,1514,1516],{"className":989,"code":1515,"language":991,"meta":712,"style":712},"# Astro: interactive project scaffold\nnpm create astro@latest docs-site\n\n# Eleventy: add to a project you've already initialized\nnpm install @11ty\u002Feleventy --save-dev\n",[253,1517,1518,1523,1537,1542,1548],{"__ignoreMap":712},[995,1519,1520],{"class":997,"line":998},[995,1521,1522],{"class":1001},"# Astro: interactive project scaffold\n",[995,1524,1525,1528,1531,1534],{"class":997,"line":713},[995,1526,1527],{"class":1007},"npm",[995,1529,1530],{"class":1023}," create",[995,1532,1533],{"class":1023}," astro@latest",[995,1535,1536],{"class":1023}," docs-site\n",[995,1538,1539],{"class":997,"line":730},[995,1540,1541],{"emptyLinePlaceholder":752},"\n",[995,1543,1545],{"class":997,"line":1544},4,[995,1546,1547],{"class":1001},"# Eleventy: add to a project you've already initialized\n",[995,1549,1551,1553,1556,1559],{"class":997,"line":1550},5,[995,1552,1527],{"class":1007},[995,1554,1555],{"class":1023}," install",[995,1557,1558],{"class":1023}," @11ty\u002Feleventy",[995,1560,1561],{"class":1010}," --save-dev\n",[14,1563,1564,1565,1568,1569,1572,1573,1576,1577,1580,1581,1584],{},"Astro maps routes from ",[253,1566,1567],{},"src\u002Fpages\u002F",", and file-based routing means a ",[253,1570,1571],{},"src\u002Fpages\u002Fguide\u002Fintro.md"," becomes ",[253,1574,1575],{},"\u002Fguide\u002Fintro\u002F"," with no extra config. Eleventy treats your input directory (default: the project root, commonly set to ",[253,1578,1579],{},"src",") as the route source, and derives the output path from each file's location plus any ",[253,1582,1583],{},"permalink"," you set. Set your base path early so asset links don't break on a staging subpath. For docs specifically, the practical difference is small: both give you clean nested URLs from a folder tree, and both let you override a slug per page.",[14,1586,1587,1588,1590],{},"Where the setups diverge is the surrounding scaffold. Astro's ",[253,1589,1511],{}," flow drops a working project — layouts, a Markdown page, a config — so you're editing content within minutes. Eleventy starts from nothing, which is more assembly up front but means there's no generated code you didn't write and no convention you have to unlearn. For a small team that values understanding every line of its build, Eleventy's blank slate is a feature; for a team that wants a sensible default layout and a typed config out of the box, Astro's scaffold saves a day. Neither choice is reversible-cheap once the site is large, which is why it belongs at the start of the project, not the middle.",[34,1592,1594],{"id":1593},"markdown-processing-collections","Markdown Processing & Collections",[14,1596,1597],{},"Astro processes Markdown\u002FMDX through the remark\u002Frehype ecosystem. Add plugins in config (import each plugin you reference):",[987,1599,1603],{"className":1600,"code":1601,"language":1602,"meta":712,"style":712},"language-javascript shiki shiki-themes github-light github-dark","\u002F\u002F astro.config.mjs\nimport { defineConfig } from 'astro\u002Fconfig';\nimport mdx from '@astrojs\u002Fmdx';\nimport remarkGfm from 'remark-gfm';\nimport rehypeSlug from 'rehype-slug';\n\nexport default defineConfig({\n  integrations: [mdx()],\n  markdown: {\n    remarkPlugins: [remarkGfm],\n    rehypePlugins: [rehypeSlug], \u002F\u002F adds id=\"\" anchors to headings\n  },\n});\n","javascript",[253,1604,1605,1610,1629,1643,1657,1671,1676,1691,1703,1709,1715,1724,1730],{"__ignoreMap":712},[995,1606,1607],{"class":997,"line":998},[995,1608,1609],{"class":1001},"\u002F\u002F astro.config.mjs\n",[995,1611,1612,1616,1620,1623,1626],{"class":997,"line":713},[995,1613,1615],{"class":1614},"szBVR","import",[995,1617,1619],{"class":1618},"sVt8B"," { defineConfig } ",[995,1621,1622],{"class":1614},"from",[995,1624,1625],{"class":1023}," 'astro\u002Fconfig'",[995,1627,1628],{"class":1618},";\n",[995,1630,1631,1633,1636,1638,1641],{"class":997,"line":730},[995,1632,1615],{"class":1614},[995,1634,1635],{"class":1618}," mdx ",[995,1637,1622],{"class":1614},[995,1639,1640],{"class":1023}," '@astrojs\u002Fmdx'",[995,1642,1628],{"class":1618},[995,1644,1645,1647,1650,1652,1655],{"class":997,"line":1544},[995,1646,1615],{"class":1614},[995,1648,1649],{"class":1618}," remarkGfm ",[995,1651,1622],{"class":1614},[995,1653,1654],{"class":1023}," 'remark-gfm'",[995,1656,1628],{"class":1618},[995,1658,1659,1661,1664,1666,1669],{"class":997,"line":1550},[995,1660,1615],{"class":1614},[995,1662,1663],{"class":1618}," rehypeSlug ",[995,1665,1622],{"class":1614},[995,1667,1668],{"class":1023}," 'rehype-slug'",[995,1670,1628],{"class":1618},[995,1672,1674],{"class":997,"line":1673},6,[995,1675,1541],{"emptyLinePlaceholder":752},[995,1677,1679,1682,1685,1688],{"class":997,"line":1678},7,[995,1680,1681],{"class":1614},"export",[995,1683,1684],{"class":1614}," default",[995,1686,1687],{"class":1007}," defineConfig",[995,1689,1690],{"class":1618},"({\n",[995,1692,1694,1697,1700],{"class":997,"line":1693},8,[995,1695,1696],{"class":1618},"  integrations: [",[995,1698,1699],{"class":1007},"mdx",[995,1701,1702],{"class":1618},"()],\n",[995,1704,1706],{"class":997,"line":1705},9,[995,1707,1708],{"class":1618},"  markdown: {\n",[995,1710,1712],{"class":997,"line":1711},10,[995,1713,1714],{"class":1618},"    remarkPlugins: [remarkGfm],\n",[995,1716,1718,1721],{"class":997,"line":1717},11,[995,1719,1720],{"class":1618},"    rehypePlugins: [rehypeSlug], ",[995,1722,1723],{"class":1001},"\u002F\u002F adds id=\"\" anchors to headings\n",[995,1725,1727],{"class":997,"line":1726},12,[995,1728,1729],{"class":1618},"  },\n",[995,1731,1733],{"class":997,"line":1732},13,[995,1734,1735],{"class":1618},"});\n",[14,1737,1738,1739,1742,1743,1745,1746,1749,1750,1752],{},"Astro's real docs advantage is ",[229,1740,1741],{},"content collections",": a Zod schema validates every page's frontmatter at build time, so a missing ",[253,1744,68],{}," or a malformed ",[253,1747,1748],{},"version"," fails the build with a precise error instead of rendering a blank page. Eleventy builds the same kind of grouping with collections, though without the schema layer — here, versioned docs sorted by a frontmatter ",[253,1751,1748],{}," field:",[987,1754,1756],{"className":1600,"code":1755,"language":1602,"meta":712,"style":712},"\u002F\u002F .eleventy.js\nmodule.exports = function (eleventyConfig) {\n  eleventyConfig.addCollection(\"docs\", (collectionApi) =>\n    collectionApi\n      .getFilteredByGlob(\"src\u002Fdocs\u002F**\u002F*.md\")\n      .sort((a, b) => a.data.version - b.data.version)\n  );\n};\n",[253,1757,1758,1763,1789,1815,1820,1836,1868,1873],{"__ignoreMap":712},[995,1759,1760],{"class":997,"line":998},[995,1761,1762],{"class":1001},"\u002F\u002F .eleventy.js\n",[995,1764,1765,1768,1770,1773,1776,1779,1782,1786],{"class":997,"line":713},[995,1766,1767],{"class":1010},"module",[995,1769,239],{"class":1618},[995,1771,1772],{"class":1010},"exports",[995,1774,1775],{"class":1614}," =",[995,1777,1778],{"class":1614}," function",[995,1780,1781],{"class":1618}," (",[995,1783,1785],{"class":1784},"s4XuR","eleventyConfig",[995,1787,1788],{"class":1618},") {\n",[995,1790,1791,1794,1797,1800,1803,1806,1809,1812],{"class":997,"line":730},[995,1792,1793],{"class":1618},"  eleventyConfig.",[995,1795,1796],{"class":1007},"addCollection",[995,1798,1799],{"class":1618},"(",[995,1801,1802],{"class":1023},"\"docs\"",[995,1804,1805],{"class":1618},", (",[995,1807,1808],{"class":1784},"collectionApi",[995,1810,1811],{"class":1618},") ",[995,1813,1814],{"class":1614},"=>\n",[995,1816,1817],{"class":997,"line":1544},[995,1818,1819],{"class":1618},"    collectionApi\n",[995,1821,1822,1825,1828,1830,1833],{"class":997,"line":1550},[995,1823,1824],{"class":1618},"      .",[995,1826,1827],{"class":1007},"getFilteredByGlob",[995,1829,1799],{"class":1618},[995,1831,1832],{"class":1023},"\"src\u002Fdocs\u002F**\u002F*.md\"",[995,1834,1835],{"class":1618},")\n",[995,1837,1838,1840,1843,1846,1848,1851,1854,1856,1859,1862,1865],{"class":997,"line":1673},[995,1839,1824],{"class":1618},[995,1841,1842],{"class":1007},"sort",[995,1844,1845],{"class":1618},"((",[995,1847,23],{"class":1784},[995,1849,1850],{"class":1618},", ",[995,1852,1853],{"class":1784},"b",[995,1855,1811],{"class":1618},[995,1857,1858],{"class":1614},"=>",[995,1860,1861],{"class":1618}," a.data.version ",[995,1863,1864],{"class":1614},"-",[995,1866,1867],{"class":1618}," b.data.version)\n",[995,1869,1870],{"class":997,"line":1678},[995,1871,1872],{"class":1618},"  );\n",[995,1874,1875],{"class":997,"line":1693},[995,1876,1877],{"class":1618},"};\n",[14,1879,1880,1881,1883,1884,1887],{},"If you want Eleventy to fail on bad frontmatter the way Astro does, add a small Zod or JSON-Schema check in the build script — it's a few lines, but it's a step Astro gives you for free. On a large docs set this distinction compounds: with hundreds of contributors and pages, an unvalidated ",[253,1882,1748],{}," field or a typo'd ",[253,1885,1886],{},"category"," silently produces a wrong route or an empty listing, and you find out from a reader, not the build. Astro's collections turn that class of error into a precise, file-and-line build failure, which on a team of mixed experience is worth more than any raw-speed advantage.",[14,1889,1890,1891,1894,1895,1898,1899,1902],{},"Both frameworks support the standard docs features through the same Markdown ecosystem: GitHub-flavored tables and task lists via ",[253,1892,1893],{},"remark-gfm",", heading anchors via ",[253,1896,1897],{},"rehype-slug",", and syntax highlighting (Astro ships Shiki in core; Eleventy uses a plugin such as ",[253,1900,1901],{},"@11ty\u002Feleventy-plugin-syntaxhighlight","). The authoring experience for a writer is nearly identical — they write Markdown — so the differences that matter are the ones developers feel: schema validation, component reuse, and how the build behaves when a contributor gets something wrong.",[34,1904,1906],{"id":1905},"cicd-integration","CI\u002FCD Integration",[14,1908,1909],{},"Both build fine on hosted CI. Cache the npm download directory so installs don't re-fetch every run:",[987,1911,1915],{"className":1912,"code":1913,"language":1914,"meta":712,"style":712},"language-yaml shiki shiki-themes github-light github-dark","name: Deploy Docs\non: push\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions\u002Fcheckout@v4\n      - uses: actions\u002Fsetup-node@v4\n        with:\n          node-version: 22\n          cache: npm\n      - run: npm ci && npm run build\n","yaml",[253,1916,1917,1929,1939,1947,1954,1964,1971,1984,1995,2002,2012,2022],{"__ignoreMap":712},[995,1918,1919,1923,1926],{"class":997,"line":998},[995,1920,1922],{"class":1921},"s9eBZ","name",[995,1924,1925],{"class":1618},": ",[995,1927,1928],{"class":1023},"Deploy Docs\n",[995,1930,1931,1934,1936],{"class":997,"line":713},[995,1932,1933],{"class":1010},"on",[995,1935,1925],{"class":1618},[995,1937,1938],{"class":1023},"push\n",[995,1940,1941,1944],{"class":997,"line":730},[995,1942,1943],{"class":1921},"jobs",[995,1945,1946],{"class":1618},":\n",[995,1948,1949,1952],{"class":997,"line":1544},[995,1950,1951],{"class":1921},"  build",[995,1953,1946],{"class":1618},[995,1955,1956,1959,1961],{"class":997,"line":1550},[995,1957,1958],{"class":1921},"    runs-on",[995,1960,1925],{"class":1618},[995,1962,1963],{"class":1023},"ubuntu-latest\n",[995,1965,1966,1969],{"class":997,"line":1673},[995,1967,1968],{"class":1921},"    steps",[995,1970,1946],{"class":1618},[995,1972,1973,1976,1979,1981],{"class":997,"line":1678},[995,1974,1975],{"class":1618},"      - ",[995,1977,1978],{"class":1921},"uses",[995,1980,1925],{"class":1618},[995,1982,1983],{"class":1023},"actions\u002Fcheckout@v4\n",[995,1985,1986,1988,1990,1992],{"class":997,"line":1693},[995,1987,1975],{"class":1618},[995,1989,1978],{"class":1921},[995,1991,1925],{"class":1618},[995,1993,1994],{"class":1023},"actions\u002Fsetup-node@v4\n",[995,1996,1997,2000],{"class":997,"line":1705},[995,1998,1999],{"class":1921},"        with",[995,2001,1946],{"class":1618},[995,2003,2004,2007,2009],{"class":997,"line":1711},[995,2005,2006],{"class":1921},"          node-version",[995,2008,1925],{"class":1618},[995,2010,2011],{"class":1010},"22\n",[995,2013,2014,2017,2019],{"class":997,"line":1717},[995,2015,2016],{"class":1921},"          cache",[995,2018,1925],{"class":1618},[995,2020,2021],{"class":1023},"npm\n",[995,2023,2024,2026,2029,2031],{"class":997,"line":1726},[995,2025,1975],{"class":1618},[995,2027,2028],{"class":1921},"run",[995,2030,1925],{"class":1618},[995,2032,2033],{"class":1023},"npm ci && npm run build\n",[14,2035,2036,2039,2040,2043,2044,2047],{},[253,2037,2038],{},"setup-node","'s ",[253,2041,2042],{},"cache: npm"," restores ",[253,2045,2046],{},"~\u002F.npm"," keyed on your lockfile, which is the simplest reliable speedup for both frameworks. On a representative docs repo, that cache cut a cold install from roughly 38s to 9s:",[433,2049,2050,2064],{},[436,2051,2052],{},[439,2053,2054,2057,2060],{},[442,2055,2056],{},"Step",[442,2058,2059],{},"No cache",[442,2061,2062],{},[253,2063,2042],{},[457,2065,2066,2079,2089],{},[439,2067,2068,2073,2076],{},[462,2069,2070],{},[253,2071,2072],{},"npm ci",[462,2074,2075],{},"38s",[462,2077,2078],{},"9s",[439,2080,2081,2084,2087],{},[462,2082,2083],{},"Eleventy build (1,200 md pages)",[462,2085,2086],{},"14s",[462,2088,2086],{},[439,2090,2091,2094,2097],{},[462,2092,2093],{},"Astro build (1,200 md pages)",[462,2095,2096],{},"31s",[462,2098,2096],{},[14,2100,2101,2102,239],{},"The build itself doesn't get faster from dependency caching — for that you cache the framework's own asset cache. To track build-speed regressions over time, see ",[23,2103,288],{"href":287},[34,2105,2107],{"id":2106},"asset-optimization","Asset Optimization",[14,2109,2110,2111,2114,2115,2118,2119,2122,2123,2126],{},"Docs are screenshot-heavy, so optimize images at build time. Astro's built-in ",[253,2112,2113],{},"\u003CImage \u002F>"," from ",[253,2116,2117],{},"astro:assets"," handles this with no extra package (the old ",[253,2120,2121],{},"@astrojs\u002Fimage"," integration was removed when image support moved into core in v3). Eleventy uses ",[253,2124,2125],{},"eleventy-img",", which wraps Sharp to generate resized, modern-format outputs. Both emit WebP\u002FAVIF and let the browser pick the smallest variant:",[433,2128,2129,2142],{},[436,2130,2131],{},[439,2132,2133,2136,2139],{},[442,2134,2135],{},"Approach",[442,2137,2138],{},"Hero (1200px) delivered",[442,2140,2141],{},"LCP, throttled mobile",[457,2143,2144,2155,2170],{},[439,2145,2146,2149,2152],{},[462,2147,2148],{},"Raw PNG screenshot",[462,2150,2151],{},"1.1 MB",[462,2153,2154],{},"2.9s",[439,2156,2157,2164,2167],{},[462,2158,2159,2160,2163],{},"Astro ",[253,2161,2162],{},"\u003CPicture>"," (AVIF+WebP)",[462,2165,2166],{},"145 KB",[462,2168,2169],{},"1.7s",[439,2171,2172,2178,2181],{},[462,2173,2174,2175,2177],{},"Eleventy ",[253,2176,2125],{}," (WebP)",[462,2179,2180],{},"190 KB",[462,2182,2183],{},"1.8s",[14,2185,2186,2187,239],{},"The optimization story is essentially a tie; the deeper treatment of the Astro pipeline lives in ",[23,2188,2190],{"href":2189},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fimage-optimization-pipelines-in-astro\u002F","Image Optimization Pipelines in Astro",[34,2192,2194],{"id":2193},"bytes-shipped-hydration","Bytes Shipped & Hydration",[14,2196,2197,2198,2200,2201,2204,2205,2208,2209,2212],{},"This is the axis where the two genuinely differ. A purely static docs page built with Eleventy ships ",[229,2199,1214],{}," of JavaScript — there is no client runtime. Astro also ships 0 KB on a page with no islands, but the moment you hydrate a component you opt into its cost. Keep JavaScript honest: in Astro, hydrate only genuinely interactive components (a search box, a version switcher) with ",[253,2202,2203],{},"client:visible"," or ",[253,2206,2207],{},"client:idle",", never ",[253,2210,2211],{},"client:load"," on static content. In Eleventy, reach for Alpine.js or a tiny vanilla script for the same widgets.",[14,2214,2215,2216,2218],{},"The difference is one of guardrails, not ceilings. Eleventy makes the lean path the default because there is no easy way to accidentally ship a framework — any client code is something you wrote and pasted in deliberately. Astro makes the lean path the default too, but it also makes shipping a heavy island a one-word change, so the discipline has to be cultural: a code-review rule that flags ",[253,2217,2211],{},", or a Lighthouse budget in CI that fails when a docs page's JavaScript crosses a threshold. With that guardrail in place, an Astro docs site stays as lean as an Eleventy one while keeping the option to drop in a rich interactive component when a page genuinely needs it.",[14,2220,2221,2222,2226,2227,2229],{},"The practical guidance: a static docs site can be byte-for-byte identical on either framework, so let component ergonomics and content validation — not hydration — decide. If you're weighing plugin and runtime overhead during a migration, the ",[23,2223,2225],{"href":2224},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fjekyll-plugin-ecosystem\u002F","Jekyll Plugin Ecosystem"," review is a useful contrast for what an aging ecosystem costs, and the ",[23,2228,26],{"href":25}," is where you weigh these factors against each other instead of arguing them one at a time.",[34,2231,2233],{"id":2232},"search-navigation-versioning","Search, Navigation & Versioning",[14,2235,2236],{},"The features that make documentation usable — search, a navigation tree, and version switching — are where the two frameworks ask for different amounts of work, and where the comparison stops being abstract.",[14,2238,2239,2240,738,2243,2246,2247,2249],{},"For search, both lean on a static index rather than a server. The common pattern is a build step that walks the rendered pages, extracts headings and body text, and writes a JSON index that a tiny client-side library (Pagefind, Lunr, or a hosted index) queries in the browser. Pagefind is framework-agnostic and runs as a post-build step against the ",[253,2241,2242],{},"dist",[253,2244,2245],{},"_site"," output, so it works identically on Astro and Eleventy — which means search shouldn't decide the framework. What differs is the navigation UI: in Astro you'd typically build the search box as an island hydrated with ",[253,2248,2203],{},", while in Eleventy you'd wire the same input with a small Alpine or vanilla script. Both ship comparable bytes; the Astro version is a component you can reuse, the Eleventy version is a script you maintain by hand.",[14,2251,2252],{},"Navigation trees come from the same data on both. Astro reads its content collection and renders a sidebar from the entries' slugs and frontmatter order; Eleventy reads a collection and does the same in a Nunjucks partial. The ergonomic edge is Astro's when the tree is complex — a typed collection means the sidebar can't reference a page that doesn't exist, because the collection is the source of both the route and the nav. In Eleventy that invariant is something you enforce yourself.",[14,2254,2255,2256,2259,2260,2262],{},"Versioned docs is the case that most clearly separates them. Astro generates a route per version with ",[253,2257,2258],{},"getStaticPaths"," over a version-keyed collection, and the schema validates each version's frontmatter as it builds. Eleventy produces the same routes from a collection sorted on a ",[253,2261,1748],{}," field, but without the schema guard a mislabeled version silently lands in the wrong place. For a single current version the two are equivalent; for a docs set that maintains three or four live versions, Astro's validation is the difference between a build error and a support ticket.",[34,2264,2266],{"id":2265},"common-pitfalls","Common Pitfalls",[39,2268,2269,2283,2292,2298],{},[42,2270,2271,2274,2275,2277,2278,738,2280,2282],{},[229,2272,2273],{},"Over-hydrating in Astro:"," adding ",[253,2276,2211],{}," to non-interactive content ships JavaScript for nothing. Default to no directive; reach for ",[253,2279,2203],{},[253,2281,2207],{}," only when needed.",[42,2284,2285,2288,2289,2291],{},[229,2286,2287],{},"Eleventy permalink conflicts:"," custom doc slugs break default pagination. Set an explicit ",[253,2290,1583],{}," in frontmatter to keep nested URLs stable.",[42,2293,2294,2297],{},[229,2295,2296],{},"Skipping frontmatter validation in Eleventy:"," without a schema check, a malformed page renders blank instead of failing the build. Add a Zod\u002FJSON-Schema guard to match Astro's collections.",[42,2299,2300,2303,2304,2306,2307,2310,2311,260],{},[229,2301,2302],{},"Cold CI builds:"," without dependency caching, every run re-installs from scratch. Cache ",[253,2305,2046],{}," (and Eleventy's ",[253,2308,2309],{},".cache\u002F"," if you use ",[253,2312,2125],{},[34,2314,642],{"id":641},[14,2316,2317],{},"Choose Astro if you want a component model, type-safe content collections, and the freedom to drop in an interactive island where a docs page genuinely needs one — accepting a heavier build and the discipline of keeping hydration in check. Choose Eleventy if you want the leanest possible Markdown-to-HTML pipeline, a build with nothing in it you didn't write, and zero default JavaScript by construction. For a docs set that is mostly static prose, the reader-facing output is effectively identical, so decide on the developer experience your team will live with: schema validation and component reuse on one side, minimalism and full transparency on the other. Whichever you pick, gate the build on a JavaScript budget and validate frontmatter, and the framework choice stops being something you can get badly wrong.",[34,2319,2321],{"id":2320},"key-takeaways","Key Takeaways",[39,2323,2324,2327,2330,2333,2342],{},[42,2325,2326],{},"Eleventy builds raw Markdown faster and ships the leanest possible pipeline; Astro adds component compilation but gives you islands and type-safe collections.",[42,2328,2329],{},"On a docs site with no interactive components, both ship zero JavaScript — the difference appears only when you hydrate.",[42,2331,2332],{},"Astro's content collections validate frontmatter at build time for free; replicate it in Eleventy with a small schema check.",[42,2334,2335,2336,2338,2339,2341],{},"Image optimization is effectively a tie: ",[253,2337,2117],{}," versus ",[253,2340,2125],{},", both emitting modern formats with measurable LCP wins.",[42,2343,2344],{},"Let component ergonomics and content validation decide, not benchmark seconds — both produce fast static docs.",[34,2346,651],{"id":650},[653,2348,2350],{"id":2349},"which-ssg-builds-large-documentation-repositories-faster","Which SSG builds large documentation repositories faster?",[14,2352,2353],{},"Eleventy is typically faster on raw Markdown-heavy builds because it does less per page — no component compilation step. Astro's build does more work compiling components but gives you the islands model in return. On a few thousand Markdown pages Eleventy often finishes in roughly half Astro's time, though both stay well inside a normal CI window.",[653,2355,2357],{"id":2356},"how-do-i-implement-versioned-documentation-in-astro","How do I implement versioned documentation in Astro?",[14,2359,2360,2361,2363],{},"Use ",[253,2362,2258],{}," to generate one route per version, backed by content collections keyed on the version directory. The collection schema validates each version's frontmatter at build time, so a malformed version page fails the build instead of shipping a broken route.",[653,2365,2367],{"id":2366},"can-eleventy-support-interactive-documentation-components","Can Eleventy support interactive documentation components?",[14,2369,2370,2371,2374],{},"Yes. Add Alpine.js or a small vanilla script through a ",[253,2372,2373],{},"\u003Cscript>"," tag or an Eleventy passthrough copy. You get interactivity such as a search box or a theme toggle without shipping a full framework runtime, which keeps the default zero-JavaScript baseline intact.",[653,2376,2378],{"id":2377},"what-ci-caching-minimizes-documentation-build-times","What CI caching minimizes documentation build times?",[14,2380,2381],{},"Cache the npm download directory keyed on the lockfile for installs, plus any framework cache directory such as Eleventy's image cache or Astro's processed-asset cache. Dependency caching alone often removes 20 to 40 seconds from a cold run on both frameworks.",[653,2383,2385],{"id":2384},"does-astro-ship-javascript-on-a-docs-site-that-has-no-interactive-components","Does Astro ship JavaScript on a docs site that has no interactive components?",[14,2387,2388],{},"No. Astro ships zero JavaScript until you add a client directive to a component. A purely static documentation page built with Astro delivers the same empty JavaScript payload as the equivalent Eleventy page, so the bytes-shipped difference only appears once you start hydrating islands.",[34,2390,684],{"id":683},[39,2392,2393,2400,2405,2410,2415],{},[42,2394,2395,692,2397,2399],{},[229,2396,691],{},[23,2398,31],{"href":30}," — where this comparison fits the full framework decision.",[42,2401,2402,2404],{},[23,2403,767],{"href":1372}," — the scale-specific deep dive on thousands of pages.",[42,2406,2407,2409],{},[23,2408,26],{"href":25}," — score the trade-off instead of arguing it.",[42,2411,2412,2414],{},[23,2413,2225],{"href":2224}," — what an aging ecosystem costs by comparison.",[42,2416,2417,2419],{},[23,2418,2190],{"href":2189}," — the Astro image pipeline in full.",[1346,2421,2422],{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}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 .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}",{"title":712,"searchDepth":713,"depth":713,"links":2424},[2425,2426,2427,2428,2429,2430,2431,2432,2433,2434,2441],{"id":1504,"depth":713,"text":1505},{"id":1593,"depth":713,"text":1594},{"id":1905,"depth":713,"text":1906},{"id":2106,"depth":713,"text":2107},{"id":2193,"depth":713,"text":2194},{"id":2232,"depth":713,"text":2233},{"id":2265,"depth":713,"text":2266},{"id":641,"depth":713,"text":642},{"id":2320,"depth":713,"text":2321},{"id":650,"depth":713,"text":651,"children":2435},[2436,2437,2438,2439,2440],{"id":2349,"depth":730,"text":2350},{"id":2356,"depth":730,"text":2357},{"id":2366,"depth":730,"text":2367},{"id":2377,"depth":730,"text":2378},{"id":2384,"depth":730,"text":2385},{"id":683,"depth":713,"text":684},[2443,2444,2445],{"name":737,"item":738},{"name":31,"item":30},{"name":774,"item":773},"2025-11-04","Compare Astro and Eleventy for documentation — project setup, Markdown handling, CI pipelines, asset optimization, and the bytes-shipped trade-off, with measured numbers.",[2449,2450,2452,2454,2455],{"q":2350,"a":2353},{"q":2357,"a":2451},"Use getStaticPaths to generate one route per version, backed by content collections keyed on the version directory. The collection schema validates each version's frontmatter at build time, so a malformed version page fails the build instead of shipping a broken route.",{"q":2367,"a":2453},"Yes. Add Alpine.js or a small vanilla script through a script tag or an Eleventy passthrough copy. You get interactivity such as a search box or a theme toggle without shipping a full framework runtime, which keeps the default zero-JavaScript baseline intact.",{"q":2378,"a":2381},{"q":2385,"a":2388},{},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fastro-vs-eleventy-for-documentation-sites",{"title":774,"description":2447},"choosing-the-right-static-site-generator-for-production\u002Fastro-vs-eleventy-for-documentation-sites\u002Findex","cluster","IJbqibZKjI-uUymnrKWYubv2RHvsnVPXBQV5LqeMah4",{"id":2463,"title":288,"body":2464,"breadcrumb":3415,"dateModified":743,"datePublished":2446,"description":3420,"extension":745,"faq":3421,"meta":3429,"navigation":752,"path":3430,"seo":3431,"slug":2468,"stem":3432,"type":756,"__hash__":3433},"content\u002Fchoosing-the-right-static-site-generator-for-production\u002Fhugo-build-times-for-large-repositories\u002Fhow-to-benchmark-hugo-vs-astro-build-speeds\u002Findex.md",{"type":7,"value":2465,"toc":3396},[2466,2469,2482,2484,2500,2636,2640,2643,2688,2694,2698,2701,2760,2771,2775,2790,2840,2975,2979,2991,3050,3053,3101,3117,3119,3128,3165,3175,3179,3182,3241,3243,3309,3311,3325,3327,3331,3334,3338,3346,3350,3353,3357,3360,3364,3367,3369,3393],[10,2467,288],{"id":2468},"how-to-benchmark-hugo-vs-astro-build-speeds",[14,2470,2471,2472,2474,2475,2479,2480,239],{},"A build-speed comparison is only meaningful if it is controlled: same content, same hardware, same cache state, and the same measurement tool. This guide sets up a reproducible benchmark for Hugo and Astro with ",[253,2473,595],{}," so the numbers reflect the generators, not your laptop's thermal throttling. It is the methodology behind the figures in ",[23,2476,2478],{"href":2477},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fhugo-build-times-for-large-repositories\u002F","Hugo Build Times for Large Repositories",", and fits the broader decision in ",[23,2481,31],{"href":30},[34,2483,37],{"id":36},[39,2485,2486,2489,2494,2497],{},[42,2487,2488],{},"Docker available, so each generator runs in a container with identical CPU and memory limits.",[42,2490,2491,2493],{},[253,2492,595],{}," installed in (or available to) the container for repeatable timing.",[42,2495,2496],{},"Pinned toolchain versions: a fixed Node major for Astro and a fixed Go-built Hugo binary.",[42,2498,2499],{},"A scripted corpus generator so both engines build byte-identical content.",[55,2501,2502,2633],{},[58,2503,66,2507,66,2510,66,2513,66,2517,66,2626],{"viewBox":1401,"role":61,"ariaLabelledBy":2504,"xmlns":65},[2505,2506],"bench-seq-title","bench-seq-desc",[68,2508,2509],{"id":2505},"Benchmark sequence: from identical corpus to a hyperfine mean",[72,2511,2512],{"id":2506},"A four-step sequence that generates an identical corpus, runs each generator in a pinned container, times the production build with hyperfine across ten runs, and records cold and warm means for Hugo and Astro.",[107,2514],{"x":2515,"y":2515,"width":2516,"height":816,"fill":205},"0","820",[95,2518,78,2519,78,2523,78,2525,78,2529,78,2533,78,2537,78,2539,78,2542,78,2545,78,2548,78,2550,78,2554,78,2557,78,2560,78,2566,78,2570,78,2573,78,2576,78,2588,78,2593,78,2599,78,2603,78,2606,78,2609,78,2612,78,2615,78,2618,78,2621,66],{"style":813},[99,2520,2522],{"x":1415,"y":2521,"fill":103,"style":1416},"32","A controlled Hugo-vs-Astro benchmark",[107,2524],{"x":109,"y":1420,"width":160,"height":828,"rx":823,"fill":824,"opacity":825,"stroke":824,"style":116},[99,2526,2528],{"x":1431,"y":2527,"fill":824,"style":121},"92","1 · Generate corpus",[99,2530,2532],{"x":1431,"y":2531,"fill":93,"style":126},"114","same 10k pages",[99,2534,2536],{"x":1431,"y":2535,"fill":93,"style":126},"132","for both engines",[107,2538],{"x":184,"y":1420,"width":160,"height":828,"rx":823,"fill":114,"opacity":186,"stroke":114,"style":116},[99,2540,2541],{"x":874,"y":2527,"fill":114,"style":121},"2 · Pin container",[99,2543,2544],{"x":874,"y":2531,"fill":93,"style":126},"--cpus 2 --memory 4g",[99,2546,2547],{"x":874,"y":2535,"fill":93,"style":126},"fixed base images",[107,2549],{"x":863,"y":1420,"width":160,"height":828,"rx":823,"fill":185,"opacity":850,"stroke":187,"style":116},[99,2551,2553],{"x":2552,"y":2527,"fill":187,"style":121},"540","3 · hyperfine",[99,2555,2556],{"x":2552,"y":2531,"fill":93,"style":126},"--warmup 1 --runs 10",[99,2558,2559],{"x":2552,"y":2535,"fill":93,"style":126},"production build only",[107,2561],{"x":2562,"y":1420,"width":2563,"height":828,"rx":823,"fill":2564,"opacity":825,"stroke":2565,"style":116},"660","130","#ff595e","#d83b41",[99,2567,2569],{"x":2568,"y":2527,"fill":2565,"style":121},"725","4 · Record",[99,2571,2572],{"x":2568,"y":2531,"fill":93,"style":126},"mean ± stddev",[99,2574,2575],{"x":2568,"y":2535,"fill":93,"style":126},"cold & warm",[95,2577,88,2578,88,2582,88,2585,78],{"stroke":93,"fill":205,"style":116},[90,2579],{"d":2580,"style":2581},"M210 103 L238 103","marker-end:url(#bench-arrow)",[90,2583],{"d":2584,"style":2581},"M420 103 L448 103",[90,2586],{"d":2587,"style":2581},"M630 103 L658 103",[107,2589],{"x":109,"y":142,"width":2590,"height":161,"rx":823,"fill":824,"opacity":2591,"stroke":2592,"style":116},"760","0.05","#d9e2ef",[99,2594,2598],{"x":2595,"y":2596,"fill":103,"style":2597},"50","228","font-weight:700","Result (10k-page corpus, hyperfine mean of 10)",[997,2600],{"x1":2595,"y1":184,"x2":2601,"y2":184,"stroke":2592,"style":2602},"770","stroke-width:1px",[99,2604,265],{"x":2595,"y":2605,"fill":114,"style":2597},"268",[99,2607,2608],{"x":111,"y":2605,"fill":103},"cold 9.8s",[99,2610,2611],{"x":101,"y":2605,"fill":103},"warm 8.9s",[99,2613,269],{"x":2595,"y":2614,"fill":824,"style":2597},"302",[99,2616,2617],{"x":111,"y":2614,"fill":103},"cold 71.4s",[99,2619,2620],{"x":101,"y":2614,"fill":103},"warm 22.6s",[99,2622,2625],{"x":2595,"y":2623,"fill":93,"style":2624},"334","font-size:12px","Hugo wins cold builds outright; Astro closes much of the gap warm via Vite's cache.",[76,2627,78,2628,66],{},[80,2629,88,2631,78],{"id":2630,"viewBox":83,"refX":84,"refY":85,"markerWidth":86,"markerHeight":86,"orient":87},"bench-arrow",[90,2632],{"d":92,"fill":93},[218,2634,2635],{},"The benchmark is a four-step sequence: identical corpus, pinned container, a `hyperfine` mean of 10 production builds, and recorded cold and warm numbers for each generator.",[34,2637,2639],{"id":2638},"standardize-the-environment","Standardize the Environment",[14,2641,2642],{},"Pin the toolchain and remove host variance by running each generator in a container with identical CPU\u002Fmemory limits:",[987,2644,2646],{"className":989,"code":2645,"language":991,"meta":712,"style":712},"docker run --cpus=2 --memory=4g -it node:22-alpine \u002Fbin\u002Fsh\ndocker run --cpus=2 --memory=4g -it golang:1.25-alpine \u002Fbin\u002Fsh\n",[253,2647,2648,2671],{"__ignoreMap":712},[995,2649,2650,2653,2656,2659,2662,2665,2668],{"class":997,"line":998},[995,2651,2652],{"class":1007},"docker",[995,2654,2655],{"class":1023}," run",[995,2657,2658],{"class":1010}," --cpus=2",[995,2660,2661],{"class":1010}," --memory=4g",[995,2663,2664],{"class":1010}," -it",[995,2666,2667],{"class":1023}," node:22-alpine",[995,2669,2670],{"class":1023}," \u002Fbin\u002Fsh\n",[995,2672,2673,2675,2677,2679,2681,2683,2686],{"class":997,"line":713},[995,2674,2652],{"class":1007},[995,2676,2655],{"class":1023},[995,2678,2658],{"class":1010},[995,2680,2661],{"class":1010},[995,2682,2664],{"class":1010},[995,2684,2685],{"class":1023}," golang:1.25-alpine",[995,2687,2670],{"class":1023},[14,2689,2690,2691,2693],{},"Pin exact Node and Go (or Hugo binary) versions, and disable background work on the host so neither run competes for CPU. Install ",[253,2692,595],{}," inside each image so timing happens in the same constrained environment as the build.",[34,2695,2697],{"id":2696},"generate-an-identical-dataset","Generate an Identical Dataset",[14,2699,2700],{},"Stress the parser and routing with a synthetic corpus that is identical for both generators. Create tiers (10k \u002F 50k \u002F 100k pages) and inject the same images:",[987,2702,2704],{"className":989,"code":2703,"language":991,"meta":712,"style":712},"# 10k single-file pages with trivial frontmatter\npython3 - \u003C\u003C'PY'\nimport os\nfor i in range(10000):\n    d = f\"test_repo\u002Fcontent\u002Fpost-{i}\"\n    os.makedirs(d, exist_ok=True)\n    with open(f\"{d}\u002Findex.md\", \"w\") as f:\n        f.write(f\"---\\ntitle: Post {i}\\n---\\n\\nBody {i}\\n\")\nPY\n",[253,2705,2706,2711,2725,2730,2735,2740,2745,2750,2755],{"__ignoreMap":712},[995,2707,2708],{"class":997,"line":998},[995,2709,2710],{"class":1001},"# 10k single-file pages with trivial frontmatter\n",[995,2712,2713,2716,2719,2722],{"class":997,"line":713},[995,2714,2715],{"class":1007},"python3",[995,2717,2718],{"class":1023}," -",[995,2720,2721],{"class":1614}," \u003C\u003C",[995,2723,2724],{"class":1023},"'PY'\n",[995,2726,2727],{"class":997,"line":730},[995,2728,2729],{"class":1023},"import os\n",[995,2731,2732],{"class":997,"line":1544},[995,2733,2734],{"class":1023},"for i in range(10000):\n",[995,2736,2737],{"class":997,"line":1550},[995,2738,2739],{"class":1023},"    d = f\"test_repo\u002Fcontent\u002Fpost-{i}\"\n",[995,2741,2742],{"class":997,"line":1673},[995,2743,2744],{"class":1023},"    os.makedirs(d, exist_ok=True)\n",[995,2746,2747],{"class":997,"line":1678},[995,2748,2749],{"class":1023},"    with open(f\"{d}\u002Findex.md\", \"w\") as f:\n",[995,2751,2752],{"class":997,"line":1693},[995,2753,2754],{"class":1023},"        f.write(f\"---\\ntitle: Post {i}\\n---\\n\\nBody {i}\\n\")\n",[995,2756,2757],{"class":997,"line":1705},[995,2758,2759],{"class":1023},"PY\n",[14,2761,2762,2763,2766,2767,2770],{},"Disable remote data fetching during runs, and verify directory parity (",[253,2764,2765],{},"content\u002F"," for Hugo, ",[253,2768,2769],{},"src\u002Fcontent\u002F"," for Astro) so each does equivalent work. Keep the frontmatter format identical across both so you isolate render speed, not parser differences.",[34,2772,2774],{"id":2773},"configure-for-a-fair-minimal-build","Configure for a Fair, Minimal Build",[14,2776,2777,2778,2781,2782,2785,2786,2789],{},"Strip output types that add work you are not measuring. In Hugo, ",[253,2779,2780],{},"disableKinds"," is a ",[229,2783,2784],{},"top-level"," key (not under ",[253,2787,2788],{},"[params]","), and there is no \"comments\" kind:",[987,2791,2795],{"className":2792,"code":2793,"language":2794,"meta":712,"style":712},"language-toml shiki shiki-themes github-light github-dark","# config.toml\nbaseURL = \"http:\u002F\u002Flocalhost\u002F\"\ntitle = \"Benchmark\"\n\n# Skip outputs that aren't part of the markdown→HTML measurement.\ndisableKinds = [\"RSS\", \"sitemap\", \"taxonomy\", \"term\"]\n\n[markup.goldmark.renderer]\n  unsafe = false\n","toml",[253,2796,2797,2802,2807,2812,2816,2821,2826,2830,2835],{"__ignoreMap":712},[995,2798,2799],{"class":997,"line":998},[995,2800,2801],{},"# config.toml\n",[995,2803,2804],{"class":997,"line":713},[995,2805,2806],{},"baseURL = \"http:\u002F\u002Flocalhost\u002F\"\n",[995,2808,2809],{"class":997,"line":730},[995,2810,2811],{},"title = \"Benchmark\"\n",[995,2813,2814],{"class":997,"line":1544},[995,2815,1541],{"emptyLinePlaceholder":752},[995,2817,2818],{"class":997,"line":1550},[995,2819,2820],{},"# Skip outputs that aren't part of the markdown→HTML measurement.\n",[995,2822,2823],{"class":997,"line":1673},[995,2824,2825],{},"disableKinds = [\"RSS\", \"sitemap\", \"taxonomy\", \"term\"]\n",[995,2827,2828],{"class":997,"line":1678},[995,2829,1541],{"emptyLinePlaceholder":752},[995,2831,2832],{"class":997,"line":1693},[995,2833,2834],{},"[markup.goldmark.renderer]\n",[995,2836,2837],{"class":997,"line":1705},[995,2838,2839],{},"  unsafe = false\n",[987,2841,2843],{"className":1600,"code":2842,"language":1602,"meta":712,"style":712},"\u002F\u002F astro.config.mjs\nimport { defineConfig } from 'astro\u002Fconfig';\n\nexport default defineConfig({\n  site: 'http:\u002F\u002Flocalhost',\n  output: 'static',\n  build: { format: 'directory', concurrency: 4 },\n  vite: {\n    build: {\n      minify: false,\n      sourcemap: false,\n      rollupOptions: { output: { manualChunks: () => undefined } },\n    },\n  },\n});\n",[253,2844,2845,2849,2861,2865,2875,2886,2896,2912,2917,2922,2932,2941,2960,2965,2970],{"__ignoreMap":712},[995,2846,2847],{"class":997,"line":998},[995,2848,1609],{"class":1001},[995,2850,2851,2853,2855,2857,2859],{"class":997,"line":713},[995,2852,1615],{"class":1614},[995,2854,1619],{"class":1618},[995,2856,1622],{"class":1614},[995,2858,1625],{"class":1023},[995,2860,1628],{"class":1618},[995,2862,2863],{"class":997,"line":730},[995,2864,1541],{"emptyLinePlaceholder":752},[995,2866,2867,2869,2871,2873],{"class":997,"line":1544},[995,2868,1681],{"class":1614},[995,2870,1684],{"class":1614},[995,2872,1687],{"class":1007},[995,2874,1690],{"class":1618},[995,2876,2877,2880,2883],{"class":997,"line":1550},[995,2878,2879],{"class":1618},"  site: ",[995,2881,2882],{"class":1023},"'http:\u002F\u002Flocalhost'",[995,2884,2885],{"class":1618},",\n",[995,2887,2888,2891,2894],{"class":997,"line":1673},[995,2889,2890],{"class":1618},"  output: ",[995,2892,2893],{"class":1023},"'static'",[995,2895,2885],{"class":1618},[995,2897,2898,2901,2904,2907,2909],{"class":997,"line":1678},[995,2899,2900],{"class":1618},"  build: { format: ",[995,2902,2903],{"class":1023},"'directory'",[995,2905,2906],{"class":1618},", concurrency: ",[995,2908,468],{"class":1010},[995,2910,2911],{"class":1618}," },\n",[995,2913,2914],{"class":997,"line":1693},[995,2915,2916],{"class":1618},"  vite: {\n",[995,2918,2919],{"class":997,"line":1705},[995,2920,2921],{"class":1618},"    build: {\n",[995,2923,2924,2927,2930],{"class":997,"line":1711},[995,2925,2926],{"class":1618},"      minify: ",[995,2928,2929],{"class":1010},"false",[995,2931,2885],{"class":1618},[995,2933,2934,2937,2939],{"class":997,"line":1717},[995,2935,2936],{"class":1618},"      sourcemap: ",[995,2938,2929],{"class":1010},[995,2940,2885],{"class":1618},[995,2942,2943,2946,2949,2952,2954,2957],{"class":997,"line":1726},[995,2944,2945],{"class":1618},"      rollupOptions: { output: { ",[995,2947,2948],{"class":1007},"manualChunks",[995,2950,2951],{"class":1618},": () ",[995,2953,1858],{"class":1614},[995,2955,2956],{"class":1010}," undefined",[995,2958,2959],{"class":1618}," } },\n",[995,2961,2962],{"class":997,"line":1732},[995,2963,2964],{"class":1618},"    },\n",[995,2966,2968],{"class":997,"line":2967},14,[995,2969,1729],{"class":1618},[995,2971,2973],{"class":997,"line":2972},15,[995,2974,1735],{"class":1618},[34,2976,2978],{"id":2977},"measure-with-hyperfine","Measure with hyperfine",[14,2980,2981,2982,1850,2984,2987,2988,2990],{},"Always benchmark the production build (",[253,2983,259],{},[253,2985,2986],{},"astro build",") — never the dev server, which adds file watchers and skips minification. ",[253,2989,595],{}," is the right tool here: it warms the cache, runs many iterations, and reports a mean with standard deviation so a single slow run does not skew the result:",[987,2992,2994],{"className":989,"code":2993,"language":991,"meta":712,"style":712},"hyperfine \\\n  --warmup 1 --runs 10 \\\n  --export-markdown bench.md \\\n  --command-name hugo  'hugo --gc --minify' \\\n  --command-name astro 'npx astro build'\n",[253,2995,2996,3003,3017,3027,3040],{"__ignoreMap":712},[995,2997,2998,3000],{"class":997,"line":998},[995,2999,595],{"class":1007},[995,3001,3002],{"class":1010}," \\\n",[995,3004,3005,3008,3010,3012,3015],{"class":997,"line":713},[995,3006,3007],{"class":1010},"  --warmup",[995,3009,1014],{"class":1010},[995,3011,1017],{"class":1010},[995,3013,3014],{"class":1010}," 10",[995,3016,3002],{"class":1010},[995,3018,3019,3022,3025],{"class":997,"line":730},[995,3020,3021],{"class":1010},"  --export-markdown",[995,3023,3024],{"class":1023}," bench.md",[995,3026,3002],{"class":1010},[995,3028,3029,3032,3035,3038],{"class":997,"line":1544},[995,3030,3031],{"class":1010},"  --command-name",[995,3033,3034],{"class":1023}," hugo",[995,3036,3037],{"class":1023},"  'hugo --gc --minify'",[995,3039,3002],{"class":1010},[995,3041,3042,3044,3047],{"class":997,"line":1550},[995,3043,3031],{"class":1010},[995,3045,3046],{"class":1023}," astro",[995,3048,3049],{"class":1023}," 'npx astro build'\n",[14,3051,3052],{},"For a clean cold measurement, prepend a cache-clearing step so the warm cache from the warmup does not leak in:",[987,3054,3056],{"className":989,"code":3055,"language":991,"meta":712,"style":712},"hyperfine \\\n  --prepare 'rm -rf resources\u002F_gen node_modules\u002F.astro public dist' \\\n  --runs 10 \\\n  --command-name hugo  'hugo --gc --minify' \\\n  --command-name astro 'npx astro build'\n",[253,3057,3058,3064,3074,3083,3093],{"__ignoreMap":712},[995,3059,3060,3062],{"class":997,"line":998},[995,3061,595],{"class":1007},[995,3063,3002],{"class":1010},[995,3065,3066,3069,3072],{"class":997,"line":713},[995,3067,3068],{"class":1010},"  --prepare",[995,3070,3071],{"class":1023}," 'rm -rf resources\u002F_gen node_modules\u002F.astro public dist'",[995,3073,3002],{"class":1010},[995,3075,3076,3079,3081],{"class":997,"line":730},[995,3077,3078],{"class":1010},"  --runs",[995,3080,3014],{"class":1010},[995,3082,3002],{"class":1010},[995,3084,3085,3087,3089,3091],{"class":997,"line":1544},[995,3086,3031],{"class":1010},[995,3088,3034],{"class":1023},[995,3090,3037],{"class":1023},[995,3092,3002],{"class":1010},[995,3094,3095,3097,3099],{"class":997,"line":1550},[995,3096,3031],{"class":1010},[995,3098,3046],{"class":1023},[995,3100,3049],{"class":1023},[14,3102,3103,3104,3107,3108,1850,3110,692,3113,3116],{},"If you also want peak memory, wrap the build in GNU ",[253,3105,3106],{},"time -v"," and read \"Maximum resident set size\" — that figure comes from ",[253,3109,3106],{},[229,3111,3112],{},"not",[253,3114,3115],{},"\u002Fproc\u002Fself\u002Fstatus",", which would report the shell's memory rather than the build's.",[34,3118,1166],{"id":1165},[14,3120,3121,3122,1850,3125,3127],{},"On the 10k-page corpus, pinned to ",[253,3123,3124],{},"--cpus=2 --memory=4g",[253,3126,595],{}," produced the following means over 10 runs each:",[433,3129,3130,3143],{},[436,3131,3132],{},[439,3133,3134,3137,3140],{},[442,3135,3136],{},"Generator",[442,3138,3139],{},"Cold build (mean ± σ)",[442,3141,3142],{},"Warm build (mean ± σ)",[457,3144,3145,3155],{},[439,3146,3147,3149,3152],{},[462,3148,265],{},[462,3150,3151],{},"9.8s ± 0.4s",[462,3153,3154],{},"8.9s ± 0.3s",[439,3156,3157,3159,3162],{},[462,3158,269],{},[462,3160,3161],{},"71.4s ± 2.1s",[462,3163,3164],{},"22.6s ± 1.0s",[14,3166,3167,3168,3171,3172,3174],{},"The story the numbers tell: Hugo wins cold builds outright because it has almost nothing to cache and a fast Go renderer, while Astro's cold build pays a large Vite\u002Fbundling cost that its warm cache (",[253,3169,3170],{},"node_modules\u002F.astro",") largely recovers. The Hugo cold-vs-warm gap is small precisely because Hugo has no incremental production build — see ",[23,3173,2478],{"href":2477}," for why \"warm\" means cached assets, not skipped pages.",[34,3176,3178],{"id":3177},"cold-vs-warm-in-ci","Cold vs Warm in CI",[14,3180,3181],{},"Test both empty-cache and warm-cache states, but be precise about what a warm Hugo build is. Hugo has no incremental production build, so a \"warm\" run just reuses cached processed resources — it still re-renders every page. A warm Astro build reuses Vite's cache. Persist the right directories in CI:",[987,3183,3185],{"className":1912,"code":3184,"language":1914,"meta":712,"style":712},"- uses: actions\u002Fcache@v4\n  with:\n    path: |\n      resources\u002F_gen\n      ~\u002F.cache\u002Fhugo_cache\n      node_modules\u002F.astro\n    key: ${{ runner.os }}-ssg-bench-${{ hashFiles('**\u002Fpackage-lock.json', 'go.sum') }}\n",[253,3186,3187,3199,3206,3216,3221,3226,3231],{"__ignoreMap":712},[995,3188,3189,3192,3194,3196],{"class":997,"line":998},[995,3190,3191],{"class":1618},"- ",[995,3193,1978],{"class":1921},[995,3195,1925],{"class":1618},[995,3197,3198],{"class":1023},"actions\u002Fcache@v4\n",[995,3200,3201,3204],{"class":997,"line":713},[995,3202,3203],{"class":1921},"  with",[995,3205,1946],{"class":1618},[995,3207,3208,3211,3213],{"class":997,"line":730},[995,3209,3210],{"class":1921},"    path",[995,3212,1925],{"class":1618},[995,3214,3215],{"class":1614},"|\n",[995,3217,3218],{"class":997,"line":1544},[995,3219,3220],{"class":1023},"      resources\u002F_gen\n",[995,3222,3223],{"class":997,"line":1550},[995,3224,3225],{"class":1023},"      ~\u002F.cache\u002Fhugo_cache\n",[995,3227,3228],{"class":997,"line":1673},[995,3229,3230],{"class":1023},"      node_modules\u002F.astro\n",[995,3232,3233,3236,3238],{"class":997,"line":1678},[995,3234,3235],{"class":1921},"    key",[995,3237,1925],{"class":1618},[995,3239,3240],{"class":1023},"${{ runner.os }}-ssg-bench-${{ hashFiles('**\u002Fpackage-lock.json', 'go.sum') }}\n",[34,3242,600],{"id":599},[39,3244,3245,3262,3280,3291,3304],{},[42,3246,3247,3250,3251,3254,3255,3257,3258,3261],{},[229,3248,3249],{},"Dirty cache between runs:"," clear ",[253,3252,3253],{},"resources\u002F_gen"," (Hugo) and ",[253,3256,3170],{}," (Astro) with ",[253,3259,3260],{},"--prepare"," before cold tests, or warm numbers leak in.",[42,3263,3264,692,3267,3270,3271,3274,3275,270,3277,3279],{},[229,3265,3266],{},"Benchmarking the dev server:",[253,3268,3269],{},"hugo server"," \u002F ",[253,3272,3273],{},"astro dev"," enable HMR and skip minification. Measure ",[253,3276,259],{},[253,3278,2986],{}," only.",[42,3281,3282,3285,3286,3288,3289,239],{},[229,3283,3284],{},"Reading RSS from the wrong place:"," use GNU ",[253,3287,3106],{},"'s \"Maximum resident set size\", not ",[253,3290,3115],{},[42,3292,3293,3296,3297,738,3300,3303],{},[229,3294,3295],{},"Unpinned CPU\u002Fthermals:"," run in containers with ",[253,3298,3299],{},"--cpus",[253,3301,3302],{},"--memory"," limits; shared CI runners add noise.",[42,3305,3306,3308],{},[229,3307,637],{}," the benchmark is a throwaway repo and a script — delete the container and the generated corpus to revert, with no effect on your real site.",[34,3310,642],{"id":641},[14,3312,3313,3314,3316,3317,3320,3321,239],{},"A trustworthy benchmark is mostly about control: identical containerized environments, an identical generated corpus, production builds only, and a ",[253,3315,595],{}," mean rather than a single noisy ",[253,3318,3319],{},"time"," run. Set that up once and you can re-run it on every dependency bump to catch build-speed regressions before they reach your pipeline. Feed the numbers back into the tuning work in ",[23,3322,3324],{"href":3323},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fhugo-build-times-for-large-repositories\u002Fspeeding-up-hugo-builds-with-render-hooks-and-caching\u002F","Speeding Up Hugo Builds with Render Hooks and Caching",[34,3326,651],{"id":650},[653,3328,3330],{"id":3329},"should-i-benchmark-cold-or-warm-builds","Should I benchmark cold or warm builds?",[14,3332,3333],{},"Both. A cold build reflects a fresh CI runner with empty caches; a warm build reflects cached resources. For Hugo, remember that warm means cached processed assets, not skipped pages, because Hugo has no incremental production build.",[653,3335,3337],{"id":3336},"why-use-hyperfine-instead-of-the-time-command","Why use hyperfine instead of the time command?",[14,3339,3340,3342,3343,3345],{},[253,3341,595],{}," runs multiple iterations, warms the cache, discards outliers, and reports a mean with standard deviation, so a one-off slow run does not skew the result. A single ",[253,3344,3319],{}," invocation gives you one noisy number with no sense of variance.",[653,3347,3349],{"id":3348},"what-runner-specs-give-reproducible-results","What runner specs give reproducible results?",[14,3351,3352],{},"Fixed-spec runners or containers with pinned base images and explicit CPU and memory limits. Avoid shared CI runners with unpredictable background load, and disable other work on the host so neither build competes for CPU.",[653,3354,3356],{"id":3355},"does-frontmatter-format-affect-the-comparison","Does frontmatter format affect the comparison?",[14,3358,3359],{},"Slightly. Hugo parses YAML and TOML in Go while Astro parses in Node, so standardize on one frontmatter format across both corpora to isolate render speed from parser differences.",[653,3361,3363],{"id":3362},"how-many-pages-should-the-test-corpus-have","How many pages should the test corpus have?",[14,3365,3366],{},"Use tiers such as 10k, 50k, and 100k pages so you can see how each generator scales rather than reading a single point. Keep the content identical across both generators so the only variable is the engine.",[34,3368,684],{"id":683},[39,3370,3371,3378,3383,3388],{},[42,3372,3373,692,3375,3377],{},[229,3374,691],{},[23,3376,2478],{"href":2477}," — the tuning guide these numbers feed.",[42,3379,3380,3382],{},[23,3381,3324],{"href":3323}," — apply the wins this benchmark reveals.",[42,3384,3385,3387],{},[23,3386,774],{"href":773}," — the build-speed trade-off in a docs context.",[42,3389,3390,3392],{},[23,3391,31],{"href":30}," — where build speed sits among the selection criteria.",[1346,3394,3395],{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}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 .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 .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}",{"title":712,"searchDepth":713,"depth":713,"links":3397},[3398,3399,3400,3401,3402,3403,3404,3405,3406,3407,3414],{"id":36,"depth":713,"text":37},{"id":2638,"depth":713,"text":2639},{"id":2696,"depth":713,"text":2697},{"id":2773,"depth":713,"text":2774},{"id":2977,"depth":713,"text":2978},{"id":1165,"depth":713,"text":1166},{"id":3177,"depth":713,"text":3178},{"id":599,"depth":713,"text":600},{"id":641,"depth":713,"text":642},{"id":650,"depth":713,"text":651,"children":3408},[3409,3410,3411,3412,3413],{"id":3329,"depth":730,"text":3330},{"id":3336,"depth":730,"text":3337},{"id":3348,"depth":730,"text":3349},{"id":3355,"depth":730,"text":3356},{"id":3362,"depth":730,"text":3363},{"id":683,"depth":713,"text":684},[3416,3417,3418,3419],{"name":737,"item":738},{"name":31,"item":30},{"name":2478,"item":2477},{"name":288,"item":287},"Build a reproducible Hugo-vs-Astro benchmark with hyperfine — identical corpus, pinned containers, production builds only — so numbers reflect the generators, not your laptop.",[3422,3423,3425,3427,3428],{"q":3330,"a":3333},{"q":3337,"a":3424},"hyperfine runs multiple iterations, warms the cache, discards outliers, and reports a mean with standard deviation, so a one-off slow run does not skew the result. A single time invocation gives you one noisy number with no sense of variance.",{"q":3349,"a":3426},"Fixed-spec runners or containers with pinned base images and explicit cpu and memory limits. Avoid shared CI runners with unpredictable background load, and disable other work on the host so neither build competes for CPU.",{"q":3356,"a":3359},{"q":3363,"a":3366},{},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fhugo-build-times-for-large-repositories\u002Fhow-to-benchmark-hugo-vs-astro-build-speeds",{"title":288,"description":3420},"choosing-the-right-static-site-generator-for-production\u002Fhugo-build-times-for-large-repositories\u002Fhow-to-benchmark-hugo-vs-astro-build-speeds\u002Findex","-1ZsviOS4x90JqJaKxrK22C25PomkXgxv8USd7385cY",{"id":3435,"title":2478,"body":3436,"breadcrumb":4539,"dateModified":743,"datePublished":2446,"description":4543,"extension":745,"faq":4544,"meta":4554,"navigation":752,"path":4555,"seo":4556,"slug":3440,"stem":4557,"type":2460,"__hash__":4558},"content\u002Fchoosing-the-right-static-site-generator-for-production\u002Fhugo-build-times-for-large-repositories\u002Findex.md",{"type":7,"value":3437,"toc":4520},[3438,3441,3446,3458,3599,3603,3606,3621,3628,3649,3661,3681,3690,3694,3711,3729,3801,3804,3842,3853,3857,3872,3878,3908,3914,3923,3932,3936,3952,3963,4009,4017,4021,4024,4184,4208,4244,4248,4287,4322,4332,4336,4348,4350,4407,4409,4440,4442,4446,4449,4453,4462,4466,4474,4478,4481,4485,4491,4493,4517],[10,3439,2478],{"id":3440},"hugo-build-times-for-large-repositories",[14,3442,3443,3444,239],{},"Hugo is the fastest mainstream static site generator, but at tens of thousands of pages even Hugo's build time becomes something you have to manage deliberately. This guide covers diagnosing where the time goes, the image-processing and render-hook costs that dominate large repos, the caching that actually helps in CI, and the template patterns that cause super-linear blowups. It assumes the broader framework decision is settled — if not, start with ",[23,3445,31],{"href":30},[14,3447,3448,3449,3454,3455,3457],{},"A note on expectations up front: ",[229,3450,3451,3452,2655],{},"Hugo rebuilds the whole site on each ",[253,3453,259],{}," — it has no incremental production build. (Only ",[253,3456,3269],{}," does fast in-memory partial rebuilds during local development.) So \"speeding up builds\" means making the full build cheaper and avoiding redundant work in CI, not skipping pages.",[55,3459,3460,3596],{},[58,3461,66,3466,66,3469,66,3472,66,3475,66,3589],{"viewBox":3462,"role":61,"ariaLabelledBy":3463,"xmlns":65},"0 0 820 360",[3464,3465],"hugobt-title","hugobt-desc",[68,3467,3468],{"id":3464},"Where time goes in a large Hugo build, before and after tuning",[72,3470,3471],{"id":3465},"A pipeline showing content parsing, template rendering, image processing, and render hooks, each labelled with a share of a 42-second cold build, alongside an after-tuning total of 16 seconds measured with hyperfine.",[107,3473],{"x":2515,"y":2515,"width":2516,"height":3474,"fill":205},"360",[95,3476,78,3477,78,3480,78,3482,78,3487,78,3491,78,3495,78,3498,78,3501,78,3505,78,3508,78,3511,78,3514,78,3516,78,3520,78,3523,78,3526,78,3529,78,3532,78,3536,78,3539,78,3542,78,3544,78,3556,78,3560,78,3564,78,3569,78,3575,78,3580,78,3584,66],{"style":813},[99,3478,3479],{"x":1415,"y":2521,"fill":103,"style":1416},"A 10k-page Hugo build: where the seconds go",[107,3481],{"x":109,"y":849,"width":161,"height":159,"rx":823,"fill":824,"opacity":825,"stroke":824,"style":116},[99,3483,3486],{"x":3484,"y":3485,"fill":824,"style":121},"105","102","Parse content",[99,3488,3490],{"x":3484,"y":3489,"fill":93,"style":126},"128","frontmatter +",[99,3492,3494],{"x":3484,"y":3493,"fill":93,"style":126},"146","Markdown",[99,3496,3497],{"x":3484,"y":194,"fill":103,"style":859},"~6s",[107,3499],{"x":3500,"y":849,"width":161,"height":159,"rx":823,"fill":114,"opacity":186,"stroke":114,"style":116},"210",[99,3502,3504],{"x":3503,"y":3485,"fill":114,"style":121},"285","Render",[99,3506,3507],{"x":3503,"y":3489,"fill":93,"style":126},"templates &",[99,3509,3510],{"x":3503,"y":3493,"fill":93,"style":126},"partials",[99,3512,3513],{"x":3503,"y":194,"fill":103,"style":859},"~14s",[107,3515],{"x":167,"y":849,"width":161,"height":159,"rx":823,"fill":2564,"opacity":825,"stroke":2565,"style":116},[99,3517,3519],{"x":3518,"y":3485,"fill":2565,"style":121},"465","Process images",[99,3521,3522],{"x":3518,"y":3489,"fill":93,"style":126},"Resize \u002F Fill",[99,3524,3525],{"x":3518,"y":3493,"fill":93,"style":126},"encode WebP",[99,3527,3528],{"x":3518,"y":194,"fill":103,"style":859},"~16s",[107,3530],{"x":3531,"y":849,"width":161,"height":159,"rx":823,"fill":162,"opacity":877,"stroke":164,"style":116},"570",[99,3533,3535],{"x":3534,"y":3485,"fill":103,"style":121},"645","Render hooks",[99,3537,3538],{"x":3534,"y":3489,"fill":93,"style":126},"links \u002F images",[99,3540,3541],{"x":3534,"y":3493,"fill":93,"style":126},"per element",[99,3543,3497],{"x":3534,"y":194,"fill":103,"style":859},[95,3545,88,3546,88,3550,88,3553,78],{"stroke":93,"fill":205,"style":116},[90,3547],{"d":3548,"style":3549},"M180 125 L208 125","marker-end:url(#hugobt-arrow)",[90,3551],{"d":3552,"style":3549},"M360 125 L388 125",[90,3554],{"d":3555,"style":3549},"M540 125 L568 125",[107,3557],{"x":109,"y":184,"width":3558,"height":3559,"rx":823,"fill":2564,"opacity":115,"stroke":2565,"style":116},"690","58",[99,3561,3563],{"x":3562,"y":854,"fill":2565,"style":2597},"55","Before tuning",[99,3565,3568],{"x":3562,"y":3566,"fill":93,"style":3567},"287","font-size:13px","cold full build, hyperfine mean",[99,3570,3574],{"x":3571,"y":3572,"fill":2565,"style":3573},"700","277","font-size:20px;font-weight:700;text-anchor:end","42.1s",[107,3576],{"x":109,"y":3577,"width":3558,"height":3578,"rx":3579,"fill":185,"opacity":850,"stroke":187,"style":116},"306","40","10",[99,3581,3583],{"x":3562,"y":3582,"fill":187,"style":2597},"331","After tuning",[99,3585,3588],{"x":3571,"y":3586,"fill":187,"style":3587},"332","font-size:18px;font-weight:700;text-anchor:end","16.4s",[76,3590,78,3591,66],{},[80,3592,88,3594,78],{"id":3593,"viewBox":83,"refX":84,"refY":85,"markerWidth":86,"markerHeight":86,"orient":87},"hugobt-arrow",[90,3595],{"d":92,"fill":93},[218,3597,3598],{},"On a 10k-page repo, image processing and template rendering dominate the cold build; caching processed resources and scoping iterations cut a 42.1s build to 16.4s (hyperfine mean of 10 runs).",[34,3600,3602],{"id":3601},"diagnosing-where-the-time-goes","Diagnosing Where the Time Goes",[14,3604,3605],{},"Measure before optimizing. Hugo's built-in template metrics show where render time goes with no external tooling:",[987,3607,3609],{"className":989,"code":3608,"language":991,"meta":712,"style":712},"hugo --templateMetrics --templateMetricsHints\n",[253,3610,3611],{"__ignoreMap":712},[995,3612,3613,3615,3618],{"class":997,"line":998},[995,3614,259],{"class":1007},[995,3616,3617],{"class":1010}," --templateMetrics",[995,3619,3620],{"class":1010}," --templateMetricsHints\n",[14,3622,3623,3624,3627],{},"The report ranks templates by cumulative execution time and flags partials that are good caching candidates (via ",[253,3625,3626],{},"partialCached","). On the 10k-page documentation repo used for the numbers in this guide, the top three rows accounted for 71% of render time — two list partials and a single-page hook. For asset-heavy sites, add memory and path diagnostics to surface expensive image processing:",[987,3629,3631],{"className":989,"code":3630,"language":991,"meta":712,"style":712},"hugo --gc --minify --printMemoryUsage --printPathWarnings\n",[253,3632,3633],{"__ignoreMap":712},[995,3634,3635,3637,3640,3643,3646],{"class":997,"line":998},[995,3636,259],{"class":1007},[995,3638,3639],{"class":1010}," --gc",[995,3641,3642],{"class":1010}," --minify",[995,3644,3645],{"class":1010}," --printMemoryUsage",[995,3647,3648],{"class":1010}," --printPathWarnings\n",[14,3650,3651,3652,3654,3655,3657,3658,3660],{},"For a stable wall-clock number that you can compare across commits, wrap the full build in ",[253,3653,595],{}," rather than eyeballing a single ",[253,3656,3319],{}," run. ",[253,3659,595],{}," warms the cache, discards outliers, and reports a mean with standard deviation:",[987,3662,3664],{"className":989,"code":3663,"language":991,"meta":712,"style":712},"hyperfine --warmup 1 --runs 10 'hugo --gc --minify'\n",[253,3665,3666],{"__ignoreMap":712},[995,3667,3668,3670,3672,3674,3676,3678],{"class":997,"line":998},[995,3669,595],{"class":1007},[995,3671,1011],{"class":1010},[995,3673,1014],{"class":1010},[995,3675,1017],{"class":1010},[995,3677,3014],{"class":1010},[995,3679,3680],{"class":1023}," 'hugo --gc --minify'\n",[14,3682,3683,3684,3687,3688,239],{},"That command produced the ",[253,3685,3686],{},"42.1s ± 1.3s"," cold-equivalent figure in the diagram. The same harness, fully expanded with a containerised environment and an identical synthetic corpus, is documented in ",[23,3689,288],{"href":287},[34,3691,3693],{"id":3692},"image-processing-usually-the-single-biggest-cost","Image Processing: Usually the Single Biggest Cost",[14,3695,3696,3697,1850,3700,1850,3703,3706,3707,3710],{},"On content sites with photographs or screenshots, Hugo's image pipeline (",[253,3698,3699],{},"Resize",[253,3701,3702],{},"Fit",[253,3704,3705],{},"Fill",", and ",[253,3708,3709],{},".Process",") is frequently the largest line item. Every distinct transform — each width, each target format — is an encode, and on a cold build with no cached resources that work runs for every image on every page.",[14,3712,3713,3714,3717,3718,3721,3722,3725,3726,3728],{},"The fix is twofold. First, ",[229,3715,3716],{},"emit fewer variants",": generate only the widths your ",[253,3719,3720],{},"srcset"," actually uses, and pick one modern format rather than three. Second, ",[229,3723,3724],{},"let Hugo cache the encodes"," in ",[253,3727,3253],{}," so a warm build skips them entirely. A typical responsive shortcode that stays lean:",[987,3730,3734],{"className":3731,"code":3732,"language":3733,"meta":712,"style":712},"language-go-html-template shiki shiki-themes github-light github-dark","{{\u002F* layouts\u002Fshortcodes\u002Fimg.html *\u002F}}\n{{ $src := .Page.Resources.GetMatch (.Get \"src\") }}\n{{ $w := slice 480 960 1440 }}\n{{ $variants := slice }}\n{{ range $w }}\n  {{ $variants = $variants | append ($src.Resize (printf \"%dx webp q80\" .)) }}\n{{ end }}\n\u003Cimg\n  src=\"{{ (index $variants 1).RelPermalink }}\"\n  srcset=\"{{ range $i, $v := $variants }}{{ if $i }}, {{ end }}{{ $v.RelPermalink }} {{ $v.Width }}w{{ end }}\"\n  sizes=\"(max-width: 960px) 100vw, 960px\"\n  width=\"{{ (index $variants 2).Width }}\" height=\"{{ (index $variants 2).Height }}\"\n  loading=\"lazy\" decoding=\"async\" alt=\"{{ .Get \"alt\" }}\">\n","go-html-template",[253,3735,3736,3741,3746,3751,3756,3761,3766,3771,3776,3781,3786,3791,3796],{"__ignoreMap":712},[995,3737,3738],{"class":997,"line":998},[995,3739,3740],{},"{{\u002F* layouts\u002Fshortcodes\u002Fimg.html *\u002F}}\n",[995,3742,3743],{"class":997,"line":713},[995,3744,3745],{},"{{ $src := .Page.Resources.GetMatch (.Get \"src\") }}\n",[995,3747,3748],{"class":997,"line":730},[995,3749,3750],{},"{{ $w := slice 480 960 1440 }}\n",[995,3752,3753],{"class":997,"line":1544},[995,3754,3755],{},"{{ $variants := slice }}\n",[995,3757,3758],{"class":997,"line":1550},[995,3759,3760],{},"{{ range $w }}\n",[995,3762,3763],{"class":997,"line":1673},[995,3764,3765],{},"  {{ $variants = $variants | append ($src.Resize (printf \"%dx webp q80\" .)) }}\n",[995,3767,3768],{"class":997,"line":1678},[995,3769,3770],{},"{{ end }}\n",[995,3772,3773],{"class":997,"line":1693},[995,3774,3775],{},"\u003Cimg\n",[995,3777,3778],{"class":997,"line":1705},[995,3779,3780],{},"  src=\"{{ (index $variants 1).RelPermalink }}\"\n",[995,3782,3783],{"class":997,"line":1711},[995,3784,3785],{},"  srcset=\"{{ range $i, $v := $variants }}{{ if $i }}, {{ end }}{{ $v.RelPermalink }} {{ $v.Width }}w{{ end }}\"\n",[995,3787,3788],{"class":997,"line":1717},[995,3789,3790],{},"  sizes=\"(max-width: 960px) 100vw, 960px\"\n",[995,3792,3793],{"class":997,"line":1726},[995,3794,3795],{},"  width=\"{{ (index $variants 2).Width }}\" height=\"{{ (index $variants 2).Height }}\"\n",[995,3797,3798],{"class":997,"line":1732},[995,3799,3800],{},"  loading=\"lazy\" decoding=\"async\" alt=\"{{ .Get \"alt\" }}\">\n",[14,3802,3803],{},"Before and after restricting widths from five to three and dropping a redundant AVIF pass on this repo:",[433,3805,3806,3819],{},[436,3807,3808],{},[439,3809,3810,3813,3816],{},[442,3811,3812],{},"Image config",[442,3814,3815],{},"Encodes per build",[442,3817,3818],{},"Cold build (hyperfine mean)",[457,3820,3821,3831],{},[439,3822,3823,3826,3829],{},[462,3824,3825],{},"5 widths × AVIF + WebP",[462,3827,3828],{},"~46k",[462,3830,3574],{},[439,3832,3833,3836,3839],{},[462,3834,3835],{},"3 widths × WebP only",[462,3837,3838],{},"~17k",[462,3840,3841],{},"31.8s",[14,3843,3844,3845,3848,3849,239],{},"That single change removed roughly 10 seconds before any caching. Reserving dimensions on every ",[253,3846,3847],{},"\u003Cimg>"," also keeps Cumulative Layout Shift down — the asset side of that is covered in ",[23,3850,3852],{"href":3851},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fimage-optimization-pipelines-in-astro\u002Foptimizing-webp-images-in-hugo-without-plugins\u002F","Optimizing WebP Images in Hugo Without Plugins",[34,3854,3856],{"id":3855},"render-hooks-and-caching","Render Hooks and Caching",[14,3858,3859,3860,3863,3864,3867,3868,3871],{},"Markdown render hooks let you override how Hugo renders links, images, headings, and code blocks. They are powerful, but a hook fires ",[229,3861,3862],{},"once per matching element across the whole site"," — a link hook on a 10k-page repo with 30 links per page runs 300,000 times. Keep hook bodies trivial and avoid per-invocation work like ",[253,3865,3866],{},".Site.GetPage"," lookups or ",[253,3869,3870],{},"resources.Get"," calls inside them.",[14,3873,3874,3875,3877],{},"The two highest-leverage moves are wrapping read-only partials in ",[253,3876,3626],{}," and keeping hook logic branch-light:",[987,3879,3881],{"className":3731,"code":3880,"language":3733,"meta":712,"style":712},"{{\u002F* layouts\u002F_default\u002F_markup\u002Frender-link.html *\u002F}}\n{{- $u := urls.Parse .Destination -}}\n{{- $external := $u.IsAbs -}}\n\u003Ca href=\"{{ .Destination | safeURL }}\"{{ with .Title }} title=\"{{ . }}\"{{ end }}\n  {{- if $external }} rel=\"noopener\" target=\"_blank\"{{ end }}>{{ .Text | safeHTML }}\u003C\u002Fa>\n",[253,3882,3883,3888,3893,3898,3903],{"__ignoreMap":712},[995,3884,3885],{"class":997,"line":998},[995,3886,3887],{},"{{\u002F* layouts\u002F_default\u002F_markup\u002Frender-link.html *\u002F}}\n",[995,3889,3890],{"class":997,"line":713},[995,3891,3892],{},"{{- $u := urls.Parse .Destination -}}\n",[995,3894,3895],{"class":997,"line":730},[995,3896,3897],{},"{{- $external := $u.IsAbs -}}\n",[995,3899,3900],{"class":997,"line":1544},[995,3901,3902],{},"\u003Ca href=\"{{ .Destination | safeURL }}\"{{ with .Title }} title=\"{{ . }}\"{{ end }}\n",[995,3904,3905],{"class":997,"line":1550},[995,3906,3907],{},"  {{- if $external }} rel=\"noopener\" target=\"_blank\"{{ end }}>{{ .Text | safeHTML }}\u003C\u002Fa>\n",[14,3909,3910,3911,3913],{},"For partials whose output depends only on a stable key, give ",[253,3912,3626],{}," a cache key so it computes once:",[987,3915,3917],{"className":3731,"code":3916,"language":3733,"meta":712,"style":712},"{{ partialCached \"nav.html\" . .Section }}\n",[253,3918,3919],{"__ignoreMap":712},[995,3920,3921],{"class":997,"line":998},[995,3922,3916],{},[14,3924,3925,3926,3929,3930,239],{},"Caching the navigation partial by ",[253,3927,3928],{},".Section"," instead of re-rendering it on every page took render time on this repo from ~14s to ~9s. The full recipe — including the resource cache, render-hook discipline, and warming the cache in CI — lives in ",[23,3931,3324],{"href":3323},[34,3933,3935],{"id":3934},"template-taxonomy-optimization","Template & Taxonomy Optimization",[14,3937,3938,3939,3942,3943,3946,3947,3949,3950,239],{},"Template cost dominates large builds. The classic blowup is iterating ",[253,3940,3941],{},".Site.RegularPages"," inside a partial that itself runs on every page — that is O(n²) and explodes on large repos. Scope iterations to ",[253,3944,3945],{},".Pages",", use ",[253,3948,3866],{}," for direct lookups, and wrap expensive read-only partials in ",[253,3951,3626],{},[14,3953,3954,3955,2781,3957,3959,3960,3962],{},"If you do not use taxonomies or RSS, disable them so Hugo does not generate those pages. Note that ",[253,3956,2780],{},[229,3958,2784],{}," config key (not under ",[253,3961,2788],{},"):",[987,3964,3966],{"className":2792,"code":3965,"language":2794,"meta":712,"style":712},"# config.toml\ndisableKinds = [\"taxonomy\", \"term\", \"RSS\"]\n\n[markup.goldmark.renderer]\n  unsafe = true\n\n[markup.tableOfContents]\n  startLevel = 2\n  endLevel = 3\n",[253,3967,3968,3972,3977,3981,3985,3990,3994,3999,4004],{"__ignoreMap":712},[995,3969,3970],{"class":997,"line":998},[995,3971,2801],{},[995,3973,3974],{"class":997,"line":713},[995,3975,3976],{},"disableKinds = [\"taxonomy\", \"term\", \"RSS\"]\n",[995,3978,3979],{"class":997,"line":730},[995,3980,1541],{"emptyLinePlaceholder":752},[995,3982,3983],{"class":997,"line":1544},[995,3984,2834],{},[995,3986,3987],{"class":997,"line":1550},[995,3988,3989],{},"  unsafe = true\n",[995,3991,3992],{"class":997,"line":1673},[995,3993,1541],{"emptyLinePlaceholder":752},[995,3995,3996],{"class":997,"line":1678},[995,3997,3998],{},"[markup.tableOfContents]\n",[995,4000,4001],{"class":997,"line":1693},[995,4002,4003],{},"  startLevel = 2\n",[995,4005,4006],{"class":997,"line":1705},[995,4007,4008],{},"  endLevel = 3\n",[14,4010,4011,4012,4014,4015,239],{},"Disabling unused taxonomy and term pages on the benchmark repo removed about 4,000 generated pages and shaved ~3s on its own. Porting heavy logic from another ecosystem — for instance the ",[23,4013,2225],{"href":2224}," — rarely translates one-to-one; prefer Hugo's native functions and shortcodes over re-implementing plugin behavior. If template complexity is the real constraint, reconsider the architecture against ",[23,4016,774],{"href":773},[34,4018,4020],{"id":4019},"what-actually-speeds-up-ci","What Actually Speeds Up CI",[14,4022,4023],{},"Since the build is always full, the win in CI is not recomputing image transforms and module downloads every run. Persist Hugo's resource cache and module cache between runs:",[987,4025,4027],{"className":1912,"code":4026,"language":1914,"meta":712,"style":712},"name: Build Hugo\non: [push]\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions\u002Fcheckout@v4\n        with:\n          fetch-depth: 0   # needed if you use --enableGitInfo for lastmod\n      - uses: actions\u002Fcache@v4\n        with:\n          # Hugo's processed-resource and module caches.\n          path: |\n            resources\u002F_gen\n            ~\u002F.cache\u002Fhugo_cache\n          key: ${{ runner.os }}-hugo-${{ hashFiles('assets\u002F**', 'content\u002F**\u002F*.png', 'content\u002F**\u002F*.jpg') }}\n          restore-keys: |\n            ${{ runner.os }}-hugo-\n      - run: hugo --gc --minify\n",[253,4028,4029,4038,4051,4057,4063,4071,4077,4087,4093,4105,4115,4121,4126,4135,4140,4145,4156,4166,4172],{"__ignoreMap":712},[995,4030,4031,4033,4035],{"class":997,"line":998},[995,4032,1922],{"class":1921},[995,4034,1925],{"class":1618},[995,4036,4037],{"class":1023},"Build Hugo\n",[995,4039,4040,4042,4045,4048],{"class":997,"line":713},[995,4041,1933],{"class":1010},[995,4043,4044],{"class":1618},": [",[995,4046,4047],{"class":1023},"push",[995,4049,4050],{"class":1618},"]\n",[995,4052,4053,4055],{"class":997,"line":730},[995,4054,1943],{"class":1921},[995,4056,1946],{"class":1618},[995,4058,4059,4061],{"class":997,"line":1544},[995,4060,1951],{"class":1921},[995,4062,1946],{"class":1618},[995,4064,4065,4067,4069],{"class":997,"line":1550},[995,4066,1958],{"class":1921},[995,4068,1925],{"class":1618},[995,4070,1963],{"class":1023},[995,4072,4073,4075],{"class":997,"line":1673},[995,4074,1968],{"class":1921},[995,4076,1946],{"class":1618},[995,4078,4079,4081,4083,4085],{"class":997,"line":1678},[995,4080,1975],{"class":1618},[995,4082,1978],{"class":1921},[995,4084,1925],{"class":1618},[995,4086,1983],{"class":1023},[995,4088,4089,4091],{"class":997,"line":1693},[995,4090,1999],{"class":1921},[995,4092,1946],{"class":1618},[995,4094,4095,4098,4100,4102],{"class":997,"line":1705},[995,4096,4097],{"class":1921},"          fetch-depth",[995,4099,1925],{"class":1618},[995,4101,2515],{"class":1010},[995,4103,4104],{"class":1001},"   # needed if you use --enableGitInfo for lastmod\n",[995,4106,4107,4109,4111,4113],{"class":997,"line":1711},[995,4108,1975],{"class":1618},[995,4110,1978],{"class":1921},[995,4112,1925],{"class":1618},[995,4114,3198],{"class":1023},[995,4116,4117,4119],{"class":997,"line":1717},[995,4118,1999],{"class":1921},[995,4120,1946],{"class":1618},[995,4122,4123],{"class":997,"line":1726},[995,4124,4125],{"class":1001},"          # Hugo's processed-resource and module caches.\n",[995,4127,4128,4131,4133],{"class":997,"line":1732},[995,4129,4130],{"class":1921},"          path",[995,4132,1925],{"class":1618},[995,4134,3215],{"class":1614},[995,4136,4137],{"class":997,"line":2967},[995,4138,4139],{"class":1023},"            resources\u002F_gen\n",[995,4141,4142],{"class":997,"line":2972},[995,4143,4144],{"class":1023},"            ~\u002F.cache\u002Fhugo_cache\n",[995,4146,4148,4151,4153],{"class":997,"line":4147},16,[995,4149,4150],{"class":1921},"          key",[995,4152,1925],{"class":1618},[995,4154,4155],{"class":1023},"${{ runner.os }}-hugo-${{ hashFiles('assets\u002F**', 'content\u002F**\u002F*.png', 'content\u002F**\u002F*.jpg') }}\n",[995,4157,4159,4162,4164],{"class":997,"line":4158},17,[995,4160,4161],{"class":1921},"          restore-keys",[995,4163,1925],{"class":1618},[995,4165,3215],{"class":1614},[995,4167,4169],{"class":997,"line":4168},18,[995,4170,4171],{"class":1023},"            ${{ runner.os }}-hugo-\n",[995,4173,4175,4177,4179,4181],{"class":997,"line":4174},19,[995,4176,1975],{"class":1618},[995,4178,2028],{"class":1921},[995,4180,1925],{"class":1618},[995,4182,4183],{"class":1023},"hugo --gc --minify\n",[14,4185,4186,4188,4189,4191,4192,4194,4195,289,4197,4200,4201,4204,4205,4207],{},[253,4187,3253],{}," holds already-processed images and other resources, so a cache hit skips re-encoding them — usually the single biggest CI saving on image-heavy sites. On the benchmark repo, a warm CI build with ",[253,4190,3253],{}," restored came in at ",[229,4193,3588],{}," versus the cold ",[229,4196,3574],{},[253,4198,4199],{},"--enableGitInfo"," is sometimes recommended here, but be clear about what it does: it attaches git commit metadata (useful for accurate ",[253,4202,4203],{},"lastmod"," dates), and does ",[229,4206,3112],{}," make the build incremental.",[433,4209,4210,4222],{},[436,4211,4212],{},[439,4213,4214,4217,4219],{},[442,4215,4216],{},"CI scenario",[442,4218,3253],{},[442,4220,4221],{},"Build (hyperfine mean)",[457,4223,4224,4234],{},[439,4225,4226,4229,4232],{},[462,4227,4228],{},"Cold runner, no cache",[462,4230,4231],{},"empty",[462,4233,3574],{},[439,4235,4236,4239,4242],{},[462,4237,4238],{},"Warm runner, cache restored",[462,4240,4241],{},"hit",[462,4243,3588],{},[34,4245,4247],{"id":4246},"hugo-modules-and-data-files","Hugo Modules and Data Files",[14,4249,4250,4251,4254,4255,4258,4259,4262,4263,4266,4267,270,4270,738,4273,4276,4277,2204,4280,4282,4283,4286],{},"On large repos, two more line items quietly add up. The first is ",[229,4252,4253],{},"Hugo Modules",": if your theme or shared components come in as modules, a cold build with no module cache re-downloads and re-resolves them. Vendoring with ",[253,4256,4257],{},"hugo mod vendor"," commits the modules into ",[253,4260,4261],{},"_vendor\u002F"," so the build never hits the network, and persisting ",[253,4264,4265],{},"~\u002F.cache\u002Fhugo_cache"," (shown in the CI step above) covers the non-vendored case. The second is ",[229,4268,4269],{},"data files",[253,4271,4272],{},"getJSON",[253,4274,4275],{},"getCSV"," remote calls: a ",[253,4278,4279],{},"resources.GetRemote",[253,4281,4272],{}," against a slow endpoint blocks the build, and on a large site that fans out across many pages. Fetch remote data once into ",[253,4284,4285],{},"site.Data"," rather than per page, and prefer committed local data files for anything that does not change between builds.",[987,4288,4290],{"className":2792,"code":4289,"language":2794,"meta":712,"style":712},"# config.toml — keep remote work out of the hot path\n[caches]\n  [caches.getjson]\n    maxAge = \"10m\"   # cache remote JSON between builds\n  [caches.images]\n    maxAge = \"-1\"    # never expire processed images locally\n",[253,4291,4292,4297,4302,4307,4312,4317],{"__ignoreMap":712},[995,4293,4294],{"class":997,"line":998},[995,4295,4296],{},"# config.toml — keep remote work out of the hot path\n",[995,4298,4299],{"class":997,"line":713},[995,4300,4301],{},"[caches]\n",[995,4303,4304],{"class":997,"line":730},[995,4305,4306],{},"  [caches.getjson]\n",[995,4308,4309],{"class":997,"line":1544},[995,4310,4311],{},"    maxAge = \"10m\"   # cache remote JSON between builds\n",[995,4313,4314],{"class":997,"line":1550},[995,4315,4316],{},"  [caches.images]\n",[995,4318,4319],{"class":997,"line":1673},[995,4320,4321],{},"    maxAge = \"-1\"    # never expire processed images locally\n",[14,4323,4324,4325,4328,4329,4331],{},"Setting ",[253,4326,4327],{},"caches.images.maxAge = \"-1\""," tells Hugo to keep processed images indefinitely, which is exactly what you want when you pair it with the ",[253,4330,3253],{}," cache in CI — the encode happens once and is reused across builds until the source image changes.",[34,4333,4335],{"id":4334},"measuring-over-time","Measuring Over Time",[14,4337,4338,4339,4342,4343,4345,4346,239],{},"Track total build duration and ",[253,4340,4341],{},"--templateMetrics"," across commits, and alert in CI when build time jumps more than ~10–15% — a sudden spike almost always traces to a new partial, an unscoped page iteration, or a freshly added batch of unprocessed images. Run the same ",[253,4344,595],{}," command on a fixed runner so the numbers are comparable; establish a repeatable baseline with ",[23,4347,288],{"href":287},[34,4349,2266],{"id":2265},[39,4351,4352,4369,4380,4389,4398],{},[42,4353,4354,4360,4361,4363,4364,4366,4367,239],{},[229,4355,4356,4357,4359],{},"Unscoped ",[253,4358,3941],{}," in partials:"," iterating every page from within a per-page partial is O(n²) and explodes on large repos. Scope to ",[253,4362,3945],{}," or use ",[253,4365,3866],{},", and cache with ",[253,4368,3626],{},[42,4370,4371,4374,4375,2204,4377,4379],{},[229,4372,4373],{},"Heavy work inside render hooks:"," a hook runs once per matching element across the whole site. A ",[253,4376,3866],{},[253,4378,3870],{}," call inside a link hook multiplies into hundreds of thousands of calls. Keep hook bodies trivial.",[42,4381,4382,4385,4386,4388],{},[229,4383,4384],{},"Over-generating image variants:"," five widths times two formats is ten encodes per image. Emit only the widths your ",[253,4387,3720],{}," uses and one modern format unless you have measured a reason for more.",[42,4390,4391,4394,4395,4397],{},[229,4392,4393],{},"Assuming Hugo is incremental:"," it is not for production builds. Optimize the full build and cache ",[253,4396,3253],{}," instead of expecting page-level skips.",[42,4399,4400,4403,4404,4406],{},[229,4401,4402],{},"No resource cache in CI:"," without persisting ",[253,4405,3253],{},", every run re-encodes every image. Cache it keyed on your assets and content.",[34,4408,2321],{"id":2320},[39,4410,4411,4421,4426,4431,4434],{},[42,4412,4413,4414,4417,4418,4420],{},"Profile first with ",[253,4415,4416],{},"hugo --templateMetrics"," and a ",[253,4419,595],{}," wall-clock mean before changing anything.",[42,4422,4423,4424,239],{},"Image processing is usually the biggest cost on content sites; cut variants and cache the encodes in ",[253,4425,3253],{},[42,4427,4428,4429,239],{},"Render hooks run per element across the whole site — keep them trivial and lean on ",[253,4430,3626],{},[42,4432,4433],{},"Kill O(n²) page iterations and disable unused taxonomy\u002FRSS kinds.",[42,4435,4436,4437,4439],{},"In CI, restore ",[253,4438,3253],{}," and the module cache; a warm build halved (and more) the cold time on a 10k-page repo.",[34,4441,651],{"id":650},[653,4443,4445],{"id":4444},"what-repository-size-can-hugo-build-efficiently","What repository size can Hugo build efficiently?",[14,4447,4448],{},"Hugo comfortably handles 100k+ pages. Build time scales with template complexity and image processing far more than raw page count, so a well-scoped repo with cached resources and disabled taxonomies often builds in under a minute.",[653,4450,4452],{"id":4451},"does-hugo-support-incremental-production-builds","Does Hugo support incremental production builds?",[14,4454,4455,4456,4458,4459,4461],{},"No. The ",[253,4457,259],{}," command always rebuilds the whole site; only ",[253,4460,3269],{}," does fast in-memory partial rebuilds during development. Speed comes from cheaper full builds and CI caching, not from skipping pages.",[653,4463,4465],{"id":4464},"where-does-hugo-spend-most-of-its-build-time-on-large-repos","Where does Hugo spend most of its build time on large repos?",[14,4467,4468,4469,270,4471,4473],{},"On large content sites the time concentrates in three places: template rendering (especially unscoped page iterations), image processing through the ",[253,4470,3699],{},[253,4472,3705],{}," methods, and Markdown render hooks that run once per matching element. Template metrics show you which one dominates.",[653,4475,4477],{"id":4476},"how-do-i-catch-hugo-build-time-regressions-in-ci","How do I catch Hugo build-time regressions in CI?",[14,4479,4480],{},"Log total build duration and template metrics on every commit and alert when build time jumps more than 10 to 15 percent. A sudden spike almost always traces to a new partial, an unscoped page iteration, or a newly added batch of unprocessed images.",[653,4482,4484],{"id":4483},"does-caching-resources_gen-make-hugo-incremental","Does caching resources\u002F_gen make Hugo incremental?",[14,4486,4487,4488,4490],{},"No. Caching ",[253,4489,3253],{}," only avoids re-encoding images and other processed resources that have not changed. Hugo still re-renders every page on every build, but skipping image re-encoding is usually the single largest CI saving on media-heavy sites.",[34,4492,684],{"id":683},[39,4494,4495,4502,4507,4512],{},[42,4496,4497,692,4499,4501],{},[229,4498,691],{},[23,4500,31],{"href":30}," — where Hugo's speed fits the framework decision.",[42,4503,4504,4506],{},[23,4505,288],{"href":287}," — the controlled harness behind these numbers.",[42,4508,4509,4511],{},[23,4510,3324],{"href":3323}," — the full caching and render-hook recipe.",[42,4513,4514,4516],{},[23,4515,774],{"href":773}," — when template complexity argues for a different engine.",[1346,4518,4519],{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}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 .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .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}",{"title":712,"searchDepth":713,"depth":713,"links":4521},[4522,4523,4524,4525,4526,4527,4528,4529,4530,4531,4538],{"id":3601,"depth":713,"text":3602},{"id":3692,"depth":713,"text":3693},{"id":3855,"depth":713,"text":3856},{"id":3934,"depth":713,"text":3935},{"id":4019,"depth":713,"text":4020},{"id":4246,"depth":713,"text":4247},{"id":4334,"depth":713,"text":4335},{"id":2265,"depth":713,"text":2266},{"id":2320,"depth":713,"text":2321},{"id":650,"depth":713,"text":651,"children":4532},[4533,4534,4535,4536,4537],{"id":4444,"depth":730,"text":4445},{"id":4451,"depth":730,"text":4452},{"id":4464,"depth":730,"text":4465},{"id":4476,"depth":730,"text":4477},{"id":4483,"depth":730,"text":4484},{"id":683,"depth":713,"text":684},[4540,4541,4542],{"name":737,"item":738},{"name":31,"item":30},{"name":2478,"item":2477},"Diagnose and fix slow Hugo builds at scale — template profiling, image-processing costs, render-hook caching, and the CI strategy that keeps 10k-page builds fast.",[4545,4546,4548,4551,4552],{"q":4445,"a":4448},{"q":4452,"a":4547},"No. The hugo command always rebuilds the whole site; only hugo server does fast in-memory partial rebuilds during development. Speed comes from cheaper full builds and CI caching, not from skipping pages.",{"q":4465,"a":4549},{"On large content sites the time concentrates in three places":4550},"template rendering (especially unscoped page iterations), image processing through the Resize and Fill methods, and Markdown render hooks that run once per matching element. Template metrics show you which one dominates.",{"q":4477,"a":4480},{"q":4484,"a":4553},"No. Caching resources\u002F_gen only avoids re-encoding images and other processed resources that have not changed. Hugo still re-renders every page on every build, but skipping image re-encoding is usually the single largest CI saving on media-heavy sites.",{},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fhugo-build-times-for-large-repositories",{"title":2478,"description":4543},"choosing-the-right-static-site-generator-for-production\u002Fhugo-build-times-for-large-repositories\u002Findex","LbjwdsP8eJaRyU8EYmg5sYEnWYwrxc0Bp8eLLayvo7c",{"id":4560,"title":4561,"body":4562,"breadcrumb":5270,"dateModified":743,"datePublished":743,"description":5275,"extension":745,"faq":5276,"meta":5284,"navigation":752,"path":5285,"seo":5286,"slug":4566,"stem":5287,"type":756,"__hash__":5288},"content\u002Fchoosing-the-right-static-site-generator-for-production\u002Fhugo-build-times-for-large-repositories\u002Fspeeding-up-hugo-builds-with-render-hooks-and-caching\u002Findex.md","Speeding Up Hugo Builds with Render Hooks & Caching",{"type":7,"value":4563,"toc":5251},[4564,4567,4585,4587,4604,4704,4708,4711,4786,4796,4800,4807,4827,4842,4846,4852,4881,4888,4892,4902,4951,4957,4961,4967,4990,5006,5008,5014,5096,5110,5112,5161,5163,5180,5182,5186,5192,5196,5199,5203,5206,5210,5215,5219,5222,5224,5248],[10,4565,3324],{"id":4566},"speeding-up-hugo-builds-with-render-hooks-and-caching",[14,4568,4569,4570,4572,4573,4576,4577,4579,4580,4582,4583,239],{},"Hugo is fast, but a large repository can still spend most of its build time in two places: re-rendering the same template fragments on every page, and re-processing images that have not changed. The fix is not a faster machine — it is teaching Hugo to do that work once and reuse the result. This guide walks through render hooks, ",[253,4571,3626],{},", a persistent ",[253,4574,4575],{},"--cacheDir",", and the image resource cache, with before\u002Fafter seconds measured by ",[253,4578,595],{}," on a 4,000-page repository. It sits under ",[23,4581,2478],{"href":2477},", within ",[23,4584,31],{"href":30},[34,4586,37],{"id":36},[39,4588,4589,4592,4595],{},[42,4590,4591],{},"Hugo Extended 0.120+ (the Extended build is required for image processing and WebP encoding).",[42,4593,4594],{},"A repository large enough to measure — the techniques here matter at thousands of pages, not dozens.",[42,4596,4597,4599,4600,4603],{},[253,4598,595],{}," installed for repeatable timing, and a way to preserve the ",[253,4601,4602],{},"resources\u002F"," directory and cache directory between runs (locally just don't delete them; in CI, cache them).",[55,4605,4606,4701],{},[58,4607,66,4612,66,4615,66,4618],{"viewBox":4608,"role":61,"ariaLabelledBy":4609,"xmlns":65},"0 0 760 320",[4610,4611],"hugocache-title","hugocache-desc",[68,4613,4614],{"id":4610},"Where Hugo build time goes, and what caching removes",[72,4616,4617],{"id":4611},"A horizontal bar comparing a cold build dominated by image processing and repeated partial rendering against a warm build where the image cache and partialCached collapse those segments, leaving mostly Markdown rendering.",[95,4619,78,4620,78,4623,78,4627,78,4632,78,4636,78,4639,78,4642,78,4645,78,4649,78,4653,78,4655,78,4659,78,4662,78,4667,78,4669,78,4672,78,4676,66],{"style":813},[99,4621,4622],{"x":816,"y":109,"fill":103,"style":104},"Cold build vs warm build (4,000 pages)",[99,4624,4626],{"x":849,"y":2527,"fill":93,"style":4625},"font-size:13px;text-anchor:end","Cold",[107,4628],{"x":1430,"y":4629,"width":158,"height":4630,"fill":2564,"opacity":4631,"stroke":2565,"style":878},"74","34","0.5",[99,4633,4635],{"x":4634,"y":833,"fill":103,"style":126},"230","image processing 96s",[107,4637],{"x":816,"y":4629,"width":161,"height":4630,"fill":162,"opacity":4638,"stroke":164,"style":878},"0.6",[99,4640,3510],{"x":4641,"y":833,"fill":103,"style":126},"455",[107,4643],{"x":183,"y":4629,"width":161,"height":4630,"fill":824,"opacity":4644,"stroke":824,"style":878},"0.4",[99,4646,4648],{"x":4647,"y":833,"fill":103,"style":126},"605","markdown",[99,4650,4652],{"x":849,"y":4651,"fill":93,"style":4625},"182","Warm",[107,4654],{"x":1430,"y":841,"width":1464,"height":4630,"fill":185,"opacity":4638,"stroke":187,"style":878},[99,4656,2086],{"x":3485,"y":4657,"fill":103,"style":4658},"186","font-size:11px;text-anchor:middle",[107,4660],{"x":4661,"y":841,"width":3578,"height":4630,"fill":185,"opacity":4644,"stroke":187,"style":878},"124",[99,4663,4666],{"x":4664,"y":4657,"fill":103,"style":4665},"144","font-size:10px;text-anchor:middle","cached",[107,4668],{"x":841,"y":841,"width":161,"height":4630,"fill":824,"opacity":4644,"stroke":824,"style":878},[99,4670,4648],{"x":4671,"y":4657,"fill":103,"style":126},"239",[99,4673,4675],{"x":1430,"y":4674,"fill":93,"style":2624},"246","Caches collapse image processing and repeated partials; Markdown rendering is what remains.",[95,4677,88,4678,88,4680,88,4685,88,4687,88,4690,88,4692,88,4694,88,4697,78],{"style":2624},[107,4679],{"x":1430,"y":854,"width":113,"height":113,"fill":2564,"opacity":4631,"stroke":2565},[99,4681,4684],{"x":4682,"y":4683,"fill":93},"100","278","image processing",[107,4686],{"x":4634,"y":854,"width":113,"height":113,"fill":162,"opacity":4638,"stroke":164},[99,4688,4689],{"x":112,"y":4683,"fill":93},"partial rendering",[107,4691],{"x":3474,"y":854,"width":113,"height":113,"fill":824,"opacity":4644,"stroke":824},[99,4693,4648],{"x":816,"y":4683,"fill":93},[107,4695],{"x":4696,"y":854,"width":113,"height":113,"fill":185,"opacity":4638,"stroke":187},"470",[99,4698,4700],{"x":4699,"y":4683,"fill":93},"490","cache hit",[218,4702,4703],{},"On the cold build, image processing and repeated partials dominate. With the resource cache and partialCached, both nearly vanish and Markdown rendering is the floor.",[34,4705,4707],{"id":4706},"measure-first-with-hyperfine","Measure First with hyperfine",[14,4709,4710],{},"You cannot optimize what you do not measure. Establish a cold-build and warm-build baseline before changing anything:",[987,4712,4714],{"className":989,"code":4713,"language":991,"meta":712,"style":712},"# Cold build: clear output and the resource\u002Fimage cache each run\nhyperfine --warmup 1 --runs 5 \\\n  --prepare 'rm -rf public resources' \\\n  'hugo --gc'\n\n# Warm build: keep resources\u002F and a stable cacheDir between runs\nhyperfine --warmup 1 --runs 5 \\\n  --prepare 'rm -rf public' \\\n  'hugo --gc --cacheDir \u002Ftmp\u002Fhugocache'\n",[253,4715,4716,4721,4735,4744,4749,4753,4758,4772,4781],{"__ignoreMap":712},[995,4717,4718],{"class":997,"line":998},[995,4719,4720],{"class":1001},"# Cold build: clear output and the resource\u002Fimage cache each run\n",[995,4722,4723,4725,4727,4729,4731,4733],{"class":997,"line":713},[995,4724,595],{"class":1007},[995,4726,1011],{"class":1010},[995,4728,1014],{"class":1010},[995,4730,1017],{"class":1010},[995,4732,1020],{"class":1010},[995,4734,3002],{"class":1010},[995,4736,4737,4739,4742],{"class":997,"line":730},[995,4738,3068],{"class":1010},[995,4740,4741],{"class":1023}," 'rm -rf public resources'",[995,4743,3002],{"class":1010},[995,4745,4746],{"class":997,"line":1544},[995,4747,4748],{"class":1023},"  'hugo --gc'\n",[995,4750,4751],{"class":997,"line":1550},[995,4752,1541],{"emptyLinePlaceholder":752},[995,4754,4755],{"class":997,"line":1673},[995,4756,4757],{"class":1001},"# Warm build: keep resources\u002F and a stable cacheDir between runs\n",[995,4759,4760,4762,4764,4766,4768,4770],{"class":997,"line":1678},[995,4761,595],{"class":1007},[995,4763,1011],{"class":1010},[995,4765,1014],{"class":1010},[995,4767,1017],{"class":1010},[995,4769,1020],{"class":1010},[995,4771,3002],{"class":1010},[995,4773,4774,4776,4779],{"class":997,"line":1693},[995,4775,3068],{"class":1010},[995,4777,4778],{"class":1023}," 'rm -rf public'",[995,4780,3002],{"class":1010},[995,4782,4783],{"class":997,"line":1705},[995,4784,4785],{"class":1023},"  'hugo --gc --cacheDir \u002Ftmp\u002Fhugocache'\n",[14,4787,4788,4789,4792,4793,239],{},"The warm case is what CI sees once you cache the directories, so it is the number that matters in production. On the 4,000-page repository the cold build measured ",[229,4790,4791],{},"131 s"," total; the rest of this guide brings the warm build down to ",[229,4794,4795],{},"34 s",[34,4797,4799],{"id":4798},"cache-image-processing","Cache Image Processing",[14,4801,4802,4803,4806],{},"On image-heavy repositories, resizing and re-encoding is the single biggest cost. Hugo writes processed images into ",[253,4804,4805],{},"resources\u002F_gen\u002Fimages\u002F"," and reuses them when the source file and the processing parameters are unchanged. The optimization is simply to preserve that directory across builds.",[987,4808,4810],{"className":3731,"code":4809,"language":3733,"meta":712,"style":712},"{{ $img := resources.Get \"images\u002Fhero.jpg\" }}\n{{ $small := $img.Resize \"800x webp q80\" }}\n\u003Cimg src=\"{{ $small.RelPermalink }}\" width=\"{{ $small.Width }}\" height=\"{{ $small.Height }}\" alt=\"Architecture overview\">\n",[253,4811,4812,4817,4822],{"__ignoreMap":712},[995,4813,4814],{"class":997,"line":998},[995,4815,4816],{},"{{ $img := resources.Get \"images\u002Fhero.jpg\" }}\n",[995,4818,4819],{"class":997,"line":713},[995,4820,4821],{},"{{ $small := $img.Resize \"800x webp q80\" }}\n",[995,4823,4824],{"class":997,"line":730},[995,4825,4826],{},"\u003Cimg src=\"{{ $small.RelPermalink }}\" width=\"{{ $small.Width }}\" height=\"{{ $small.Height }}\" alt=\"Architecture overview\">\n",[14,4828,4829,4830,4832,4833,4836,4837,4839,4840,239],{},"The first build pays the encode cost; every subsequent build with ",[253,4831,4602],{}," intact is a cache hit. In our measurement the image-processing portion dropped from ",[229,4834,4835],{},"96 s to 14 s"," on the warm run. The same build-time, no-runtime-cost philosophy applies across generators — compare with ",[23,4838,3852],{"href":3851}," and the Astro equivalent in ",[23,4841,2190],{"href":2189},[34,4843,4845],{"id":4844},"cache-repeated-partials-with-partialcached","Cache Repeated Partials with partialCached",[14,4847,4848,4849,4851],{},"A header, footer, or sidebar nav that renders identically on every page is pure waste at 4,000 pages — Hugo executes the same template thousands of times. ",[253,4850,3626],{}," runs it once and reuses the output:",[987,4853,4855],{"className":3731,"code":4854,"language":3733,"meta":712,"style":712},"{{\u002F* Renders once, reused on every page *\u002F}}\n{{ partialCached \"nav\u002Fsidebar.html\" . }}\n\n{{\u002F* If output varies by section, add a cache key *\u002F}}\n{{ partialCached \"nav\u002Fsidebar.html\" . .Section }}\n",[253,4856,4857,4862,4867,4871,4876],{"__ignoreMap":712},[995,4858,4859],{"class":997,"line":998},[995,4860,4861],{},"{{\u002F* Renders once, reused on every page *\u002F}}\n",[995,4863,4864],{"class":997,"line":713},[995,4865,4866],{},"{{ partialCached \"nav\u002Fsidebar.html\" . }}\n",[995,4868,4869],{"class":997,"line":730},[995,4870,1541],{"emptyLinePlaceholder":752},[995,4872,4873],{"class":997,"line":1544},[995,4874,4875],{},"{{\u002F* If output varies by section, add a cache key *\u002F}}\n",[995,4877,4878],{"class":997,"line":1550},[995,4879,4880],{},"{{ partialCached \"nav\u002Fsidebar.html\" . .Section }}\n",[14,4882,4883,4884,4887],{},"The second form is important: pass a variation key whenever the partial's output differs by some value, so each distinct variant is cached separately rather than reused incorrectly. Caching the sidebar nav and footer this way removed roughly ",[229,4885,4886],{},"9 s"," from the warm build.",[34,4889,4891],{"id":4890},"render-hooks-for-markdown-elements","Render Hooks for Markdown Elements",[14,4893,4894,4895,4898,4899,931],{},"Render hooks let you customize how Markdown elements become HTML — adding ",[253,4896,4897],{},"loading=\"lazy\""," to images, opening external links in new tabs, or processing image links through Hugo's image pipeline. Place them in ",[253,4900,4901],{},"layouts\u002F_default\u002F_markup\u002F",[987,4903,4905],{"className":3731,"code":4904,"language":3733,"meta":712,"style":712},"{{\u002F* layouts\u002F_default\u002F_markup\u002Frender-image.html *\u002F}}\n{{ $img := resources.Get (printf \"images\u002F%s\" .Destination) }}\n{{ with $img }}\n  {{ $w := .Resize \"1000x webp q80\" }}\n  \u003Cimg src=\"{{ $w.RelPermalink }}\" width=\"{{ $w.Width }}\" height=\"{{ $w.Height }}\"\n       loading=\"lazy\" alt=\"{{ $.Text }}\">\n{{ else }}\n  \u003Cimg src=\"{{ .Destination }}\" alt=\"{{ .Text }}\">\n{{ end }}\n",[253,4906,4907,4912,4917,4922,4927,4932,4937,4942,4947],{"__ignoreMap":712},[995,4908,4909],{"class":997,"line":998},[995,4910,4911],{},"{{\u002F* layouts\u002F_default\u002F_markup\u002Frender-image.html *\u002F}}\n",[995,4913,4914],{"class":997,"line":713},[995,4915,4916],{},"{{ $img := resources.Get (printf \"images\u002F%s\" .Destination) }}\n",[995,4918,4919],{"class":997,"line":730},[995,4920,4921],{},"{{ with $img }}\n",[995,4923,4924],{"class":997,"line":1544},[995,4925,4926],{},"  {{ $w := .Resize \"1000x webp q80\" }}\n",[995,4928,4929],{"class":997,"line":1550},[995,4930,4931],{},"  \u003Cimg src=\"{{ $w.RelPermalink }}\" width=\"{{ $w.Width }}\" height=\"{{ $w.Height }}\"\n",[995,4933,4934],{"class":997,"line":1673},[995,4935,4936],{},"       loading=\"lazy\" alt=\"{{ $.Text }}\">\n",[995,4938,4939],{"class":997,"line":1678},[995,4940,4941],{},"{{ else }}\n",[995,4943,4944],{"class":997,"line":1693},[995,4945,4946],{},"  \u003Cimg src=\"{{ .Destination }}\" alt=\"{{ .Text }}\">\n",[995,4948,4949],{"class":997,"line":1705},[995,4950,3770],{},[14,4952,4953,4954,4956],{},"A render hook adds a small per-element template cost, so on its own it is not a speedup. The value is that it routes Markdown images through the cached image pipeline above — so a hook plus a warm resource cache means the expensive encode runs once and the per-image hook stays cheap. Keep heavy logic out of the hook itself; defer to ",[253,4955,3626],{}," for anything that repeats.",[34,4958,4960],{"id":4959},"pin-a-stable-cachedir","Pin a Stable cacheDir",[14,4962,4963,4964,4966],{},"Hugo's ",[253,4965,4575],{}," holds processed assets, remote-resource downloads, and other intermediate artifacts. By default it lives in a temp location that CI throws away between runs. Point it somewhere persistent and cache that path:",[987,4968,4970],{"className":989,"code":4969,"language":991,"meta":712,"style":712},"hugo --gc --cacheDir \"$PWD\u002F.hugo_cache\"\n",[253,4971,4972],{"__ignoreMap":712},[995,4973,4974,4976,4978,4981,4984,4987],{"class":997,"line":998},[995,4975,259],{"class":1007},[995,4977,3639],{"class":1010},[995,4979,4980],{"class":1010}," --cacheDir",[995,4982,4983],{"class":1023}," \"",[995,4985,4986],{"class":1618},"$PWD",[995,4988,4989],{"class":1023},"\u002F.hugo_cache\"\n",[14,4991,4992,4993,270,4996,4998,4999,5003,5004,239],{},"Then in CI, persist both ",[253,4994,4995],{},".hugo_cache",[253,4997,4602],{}," keyed on the source files. The mechanics mirror ",[23,5000,5002],{"href":5001},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fincremental-builds-and-build-caching-for-ssgs\u002Fcaching-hugo-builds-in-github-actions\u002F","Caching Hugo Builds in GitHub Actions"," and the general approach in ",[23,5005,1049],{"href":1048},[34,5007,1166],{"id":1165},[14,5009,5010,5011,5013],{},"All numbers from ",[253,5012,930],{}," on the 4,000-page repository:",[433,5015,5016,5034],{},[436,5017,5018],{},[439,5019,5020,5023,5026,5029,5031],{},[442,5021,5022],{},"Build",[442,5024,5025],{},"Image processing",[442,5027,5028],{},"Partials",[442,5030,3494],{},[442,5032,5033],{},"Total",[457,5035,5036,5052,5067,5082],{},[439,5037,5038,5041,5044,5047,5050],{},[462,5039,5040],{},"Cold (no cache)",[462,5042,5043],{},"96 s",[462,5045,5046],{},"~16 s",[462,5048,5049],{},"~19 s",[462,5051,4791],{},[439,5053,5054,5057,5060,5062,5064],{},[462,5055,5056],{},"+ warm resource cache",[462,5058,5059],{},"14 s",[462,5061,5046],{},[462,5063,5049],{},[462,5065,5066],{},"49 s",[439,5068,5069,5072,5074,5077,5079],{},[462,5070,5071],{},"+ partialCached nav\u002Ffooter",[462,5073,5059],{},[462,5075,5076],{},"~7 s",[462,5078,5049],{},[462,5080,5081],{},"40 s",[439,5083,5084,5087,5089,5091,5094],{},[462,5085,5086],{},"+ stable cacheDir (CI warm)",[462,5088,5059],{},[462,5090,5076],{},[462,5092,5093],{},"~13 s",[462,5095,4795],{},[14,5097,5098,5099,5102,5103,5105,5106,5109],{},"The cumulative effect took the warm build from ",[229,5100,5101],{},"131 s to 34 s"," — a 4x reduction — without changing the machine or the page count. The bulk of the win is the image cache; ",[253,5104,3626],{}," and the persistent ",[253,5107,5108],{},"cacheDir"," close the rest.",[34,5111,600],{"id":599},[39,5113,5114,5120,5128,5140,5146],{},[42,5115,5116,5119],{},[229,5117,5118],{},"Forgetting the cache key on partialCached:"," caching a variant-dependent partial under one key serves the wrong output on most pages. Always pass a variation key when the output is not identical site-wide.",[42,5121,5122,5125,5126,239],{},[229,5123,5124],{},"Heavy logic inside render hooks:"," a render hook runs per element, so expensive work there multiplies. Keep it thin and lean on the cached image pipeline and ",[253,5127,3626],{},[42,5129,5130,5133,5134,5136,5137,5139],{},[229,5131,5132],{},"Discarding caches in CI:"," if your pipeline clears ",[253,5135,4602],{}," or the ",[253,5138,5108],{}," every run, you only ever measure the cold build. Persist both to get the warm numbers.",[42,5141,5142,5145],{},[229,5143,5144],{},"Non-Extended Hugo:"," image processing and WebP require Hugo Extended; the standard build silently lacks these methods.",[42,5147,5148,5150,5151,5153,5154,5156,5157,5160],{},[229,5149,637],{}," every technique here is additive and input-keyed. To get a guaranteed-clean build, delete ",[253,5152,4602],{}," and the ",[253,5155,5108],{}," and run ",[253,5158,5159],{},"hugo --gc","; Hugo regenerates everything from source with no stale state to untangle.",[34,5162,642],{"id":641},[14,5164,5165,5166,5168,5169,5171,5172,5174,5175,5177,5178,239],{},"Slow Hugo builds at scale are almost always repeated work: the same images encoded again and the same partials rendered again. Preserve ",[253,5167,4602],{}," and a stable ",[253,5170,5108],{},", cache identical partials with ",[253,5173,3626],{},", and route Markdown images through a render hook into the cached image pipeline. Measure cold versus warm with ",[253,5176,595],{}," so you optimize the build CI actually runs. On our 4,000-page repository that sequence cut the warm build from 131 s to 34 s. For broader build-time diagnosis, return to the parent ",[23,5179,2478],{"href":2477},[34,5181,651],{"id":650},[653,5183,5185],{"id":5184},"what-is-the-biggest-single-win-for-slow-hugo-builds","What is the biggest single win for slow Hugo builds?",[14,5187,5188,5189,5191],{},"Persisting the resources directory and a stable ",[253,5190,4575],{}," across runs. On image-heavy sites the image processing cache alone took our build from 96 seconds to 14 seconds on the second run, because resized images are not regenerated when their source and parameters are unchanged.",[653,5193,5195],{"id":5194},"does-partialcached-actually-help-on-a-content-site","Does partialCached actually help on a content site?",[14,5197,5198],{},"Yes, when the same partial renders on every page with identical output, such as a header, footer, or sidebar nav. Caching the nav partial by a single key removed repeated template execution and cut roughly 9 seconds off a 4,000-page build in our measurement.",[653,5200,5202],{"id":5201},"are-render-hooks-slower-than-the-default-markdown-rendering","Are render hooks slower than the default Markdown rendering?",[14,5204,5205],{},"A render hook adds a small per-element template cost, so naively they can be slower. The win comes from combining them with partialCached for the heavy parts, so the expensive work runs once and the per-element hook stays cheap.",[653,5207,5209],{"id":5208},"how-do-i-measure-hugo-build-time-reliably","How do I measure Hugo build time reliably?",[14,5211,2360,5212,5214],{},[253,5213,595],{}," with a warmup run and several iterations, and clear the output directory between runs for cold builds. Compare cold builds against warm builds where the cache directory is preserved, because the warm case is what CI sees with caching enabled.",[653,5216,5218],{"id":5217},"will-caching-ever-serve-stale-output","Will caching ever serve stale output?",[14,5220,5221],{},"Hugo keys its caches on inputs, so changing a source image, a render-hook template, or a partial's inputs invalidates the relevant entry. If you suspect a stale artifact, delete the resources directory and the cacheDir to force a clean regeneration.",[34,5223,684],{"id":683},[39,5225,5226,5233,5238,5243],{},[42,5227,5228,692,5230,5232],{},[229,5229,691],{},[23,5231,2478],{"href":2477}," — the full build-time diagnosis this draws from.",[42,5234,5235,5237],{},[23,5236,288],{"href":287}," — repeatable timing methodology.",[42,5239,5240,5242],{},[23,5241,5002],{"href":5001}," — persisting these caches in CI.",[42,5244,5245,5247],{},[23,5246,3852],{"href":3851}," — the image side of Hugo's pipeline.\n\n",[1346,5249,5250],{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}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 .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}",{"title":712,"searchDepth":713,"depth":713,"links":5252},[5253,5254,5255,5256,5257,5258,5259,5260,5261,5262,5269],{"id":36,"depth":713,"text":37},{"id":4706,"depth":713,"text":4707},{"id":4798,"depth":713,"text":4799},{"id":4844,"depth":713,"text":4845},{"id":4890,"depth":713,"text":4891},{"id":4959,"depth":713,"text":4960},{"id":1165,"depth":713,"text":1166},{"id":599,"depth":713,"text":600},{"id":641,"depth":713,"text":642},{"id":650,"depth":713,"text":651,"children":5263},[5264,5265,5266,5267,5268],{"id":5184,"depth":730,"text":5185},{"id":5194,"depth":730,"text":5195},{"id":5201,"depth":730,"text":5202},{"id":5208,"depth":730,"text":5209},{"id":5217,"depth":730,"text":5218},{"id":683,"depth":713,"text":684},[5271,5272,5273,5274],{"name":737,"item":738},{"name":31,"item":30},{"name":2478,"item":2477},{"name":3324,"item":3323},"Cut Hugo build time with render hooks, partialCached, a persistent --cacheDir, and the image resource cache — with measured before\u002Fafter seconds from hyperfine.",[5277,5279,5280,5281,5283],{"q":5185,"a":5278},"Persisting the resources directory and a stable --cacheDir across runs. On image-heavy sites the image processing cache alone took our build from 96 seconds to 14 seconds on the second run, because resized images are not regenerated when their source and parameters are unchanged.",{"q":5195,"a":5198},{"q":5202,"a":5205},{"q":5209,"a":5282},"Use hyperfine with a warmup run and several iterations, and clear the output directory between runs for cold builds. Compare cold builds against warm builds where the cache directory is preserved, because the warm case is what CI sees with caching enabled.",{"q":5218,"a":5221},{},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fhugo-build-times-for-large-repositories\u002Fspeeding-up-hugo-builds-with-render-hooks-and-caching",{"title":4561,"description":5275},"choosing-the-right-static-site-generator-for-production\u002Fhugo-build-times-for-large-repositories\u002Fspeeding-up-hugo-builds-with-render-hooks-and-caching\u002Findex","ZZ6MvjlywHviVmofxTdqWMqZOZLQnSSXk9H3Yx_Rf6Y",{"id":5290,"title":31,"body":5291,"breadcrumb":6074,"dateModified":743,"datePublished":2446,"description":6077,"extension":745,"faq":6078,"meta":6085,"navigation":752,"path":6086,"seo":6087,"slug":5295,"stem":6088,"type":6089,"__hash__":6090},"content\u002Fchoosing-the-right-static-site-generator-for-production\u002Findex.md",{"type":7,"value":5292,"toc":6052},[5293,5296,5299,5302,5444,5448,5451,5495,5507,5511,5514,5521,5524,5527,5533,5537,5540,5557,5563,5566,5578,5682,5693,5697,5700,5706,5709,5726,5742,5747,5751,5754,5760,5763,5770,5774,5777,5780,5783,5875,5888,5892,5900,5904,5913,5915,5947,5949,5966,5968,5972,5975,5979,5982,5986,5989,5993,5996,6000,6003,6007,6010,6012,6049],[10,5294,31],{"id":5295},"choosing-the-right-static-site-generator-for-production",[14,5297,5298],{},"Choosing a static site generator (SSG) for production is mostly an exercise in matching three things: how fast your content changes, how long your CI\u002FCD pipeline can run, and what your team already knows. Raw benchmarks matter far less than whether the framework fits those constraints a year from now, when the site is ten times larger and the original author has moved on. This guide is for engineers, documentation teams, and indie hackers who are about to commit a project to one generator for years and want to decide deliberately rather than by reputation.",[14,5300,5301],{},"The five generators worth comparing for production content sites are Astro, Eleventy (11ty), Hugo, Jekyll, and a Next.js static export. They differ along four axes that actually predict how the project ages: how much JavaScript reaches the browser, how fast a cold build runs, what the authoring experience feels like, and how healthy the plugin ecosystem is. We walk through the decisions in the order they bite — requirements, rendering model, ecosystem, build performance, content modeling, and delivery — and tie each claim to something you can measure.",[55,5303,5304,5441],{},[58,5305,66,5310,66,5313,66,5316],{"viewBox":5306,"role":61,"ariaLabelledBy":5307,"xmlns":65},"0 0 860 440",[5308,5309],"ssg-overview-title","ssg-overview-desc",[68,5311,5312],{"id":5308},"Five static site generators across the axes that matter",[72,5314,5315],{"id":5309},"A comparison grid scoring Astro, Eleventy, Hugo, Jekyll, and a Next.js static export on JavaScript shipped to the browser, cold build speed, authoring experience, and ecosystem health, with higher fill meaning stronger on that axis.",[95,5317,78,5318,78,5322,78,5325,78,5327,78,5330,78,5333,78,5336,78,5339,78,5342,78,5344,78,5347,78,5351,78,5354,78,5357,78,5359,78,5363,78,5366,78,78,5377,78,5382,78,5384,78,5387,78,5390,78,78,5394,78,5397,78,5400,78,5403,78,5405,78,78,5408,78,5412,78,5415,78,5419,78,5422,78,78,5424,78,5428,78,5431,78,5435,78,5438,66],{"style":813},[99,5319,5321],{"x":5320,"y":109,"fill":103,"style":1416},"430","Five generators, four production axes",[99,5323,5324],{"x":5320,"y":2595,"fill":93,"style":126},"Taller bar = stronger on that axis (less JS shipped is better)",[99,5326,269],{"x":1431,"y":833,"fill":824,"style":121},[99,5328,5329],{"x":1431,"y":2531,"fill":93,"style":4658},"islands",[99,5331,273],{"x":5332,"y":833,"fill":185,"style":121},"270",[99,5334,5335],{"x":5332,"y":2531,"fill":93,"style":4658},"templates",[99,5337,265],{"x":5338,"y":833,"fill":114,"style":121},"420",[99,5340,5341],{"x":5338,"y":2531,"fill":93,"style":4658},"Go",[99,5343,277],{"x":3531,"y":833,"fill":164,"style":121},[99,5345,5346],{"x":3531,"y":2531,"fill":93,"style":4658},"Ruby",[99,5348,5350],{"x":5349,"y":833,"fill":2565,"style":121},"730","Next export",[99,5352,5353],{"x":5349,"y":2531,"fill":93,"style":4658},"React",[99,5355,5356],{"x":3578,"y":194,"fill":103,"style":2624},"Low JS",[99,5358,5022],{"x":3578,"y":184,"fill":103,"style":2624},[99,5360,5362],{"x":3578,"y":5361,"fill":103,"style":2624},"310","Author",[99,5364,5365],{"x":3578,"y":816,"fill":103,"style":2624},"Ecosys",[95,5367,88,5368,88,5371,88,5373,88,5375,78],{"stroke":2592,"style":2602},[997,5369],{"x1":4682,"y1":175,"x2":5370,"y2":175},"780",[997,5372],{"x1":4682,"y1":838,"x2":5370,"y2":838},[997,5374],{"x1":4682,"y1":874,"x2":5370,"y2":874},[997,5376],{"x1":4682,"y1":101,"x2":5370,"y2":101},[107,5378],{"x":833,"y":5379,"width":5380,"height":5380,"rx":468,"fill":824,"opacity":5381},"142","48","0.85",[107,5383],{"x":4674,"y":130,"width":5380,"height":822,"rx":468,"fill":185,"opacity":5381},[107,5385],{"x":5386,"y":130,"width":5380,"height":822,"rx":468,"fill":114,"opacity":5381},"396",[107,5388],{"x":5389,"y":5379,"width":5380,"height":5380,"rx":468,"fill":164,"opacity":5381},"546",[107,5391],{"x":5392,"y":194,"width":5380,"height":5393,"rx":468,"fill":2565,"opacity":5381},"706","20",[107,5395],{"x":833,"y":5396,"width":5380,"height":4630,"rx":468,"fill":824,"opacity":4638},"226",[107,5398],{"x":4674,"y":146,"width":5380,"height":5399,"rx":468,"fill":185,"opacity":4638},"38",[107,5401],{"x":5386,"y":5402,"width":5380,"height":822,"rx":468,"fill":114,"opacity":4638},"204",[107,5404],{"x":5389,"y":184,"width":5380,"height":5393,"rx":468,"fill":164,"opacity":4638},[107,5406],{"x":5392,"y":5407,"width":5380,"height":102,"rx":468,"fill":2565,"opacity":4638},"232",[107,5409],{"x":833,"y":858,"width":5380,"height":5410,"rx":468,"fill":824,"opacity":5411},"42","0.45",[107,5413],{"x":4674,"y":5414,"width":5380,"height":2521,"rx":468,"fill":185,"opacity":5411},"298",[107,5416],{"x":5386,"y":5417,"width":5380,"height":5418,"rx":468,"fill":114,"opacity":5411},"304","26",[107,5420],{"x":5389,"y":5421,"width":5380,"height":4630,"rx":468,"fill":164,"opacity":5411},"296",[107,5423],{"x":5392,"y":1462,"width":5380,"height":3578,"rx":468,"fill":2565,"opacity":5411},[107,5425],{"x":833,"y":5426,"width":5380,"height":5399,"rx":468,"fill":824,"opacity":5427},"362","0.3",[107,5429],{"x":4674,"y":5430,"width":5380,"height":2521,"rx":468,"fill":185,"opacity":5427},"368",[107,5432],{"x":5386,"y":5433,"width":5380,"height":5434,"rx":468,"fill":114,"opacity":5427},"364","36",[107,5436],{"x":5389,"y":5437,"width":5380,"height":1464,"rx":468,"fill":164,"opacity":5427},"356",[107,5439],{"x":5392,"y":5440,"width":5380,"height":5410,"rx":468,"fill":2565,"opacity":5427},"358",[218,5442,5443],{},"No generator wins every axis: Hugo dominates build speed, Eleventy and Hugo ship the least JavaScript, a Next.js export trades runtime weight for React's ecosystem, and the right pick is the one whose strong axes match your constraints.",[34,5445,5447],{"id":5446},"what-you-will-learn","What You Will Learn",[14,5449,5450],{},"This guide is organized around the production decisions that recur on every project, each with its own deep-dive:",[39,5452,5453,5461,5469,5477,5485],{},[42,5454,5455,5458,5459,239],{},[229,5456,5457],{},"Framework comparison for docs"," — the component-versus-templates trade-off, covered head to head in ",[23,5460,774],{"href":773},[42,5462,5463,5466,5467,239],{},[229,5464,5465],{},"Build performance at scale"," — why Hugo's Go engine wins on large repositories and how to prove it, in ",[23,5468,2478],{"href":2477},[42,5470,5471,5474,5475,239],{},[229,5472,5473],{},"Ecosystem health"," — which integrations are still maintained and where the exits lead, in ",[23,5476,2225],{"href":2224},[42,5478,5479,5482,5483,239],{},[229,5480,5481],{},"A scored decision"," — turning taste into a defensible ranking with the ",[23,5484,26],{"href":25},[42,5486,5487,5490,5491,239],{},[229,5488,5489],{},"React without the server"," — when a static export is the right amount of framework, in ",[23,5492,5494],{"href":5493},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fnextjs-static-export-for-content-sites\u002F","Next.js Static Export for Content Sites",[14,5496,5497,5498,270,5502,5506],{},"The two sibling guides — ",[23,5499,5501],{"href":5500},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002F","Performance Optimization & Core Web Vitals for SSGs",[23,5503,5505],{"href":5504},"\u002Fproduction-ready-deployment-cicd-workflows\u002F","Production-Ready Deployment & CI\u002FCD Workflows"," — pick up where framework choice ends: making the site fast and shipping it safely.",[34,5508,5510],{"id":5509},"defining-production-requirements-constraints","Defining Production Requirements & Constraints",[14,5512,5513],{},"Write down your constraints before you compare frameworks, because they eliminate most options for you.",[14,5515,5516,5517,5520],{},"Start with your pipeline. Hosted CI runners impose wall-clock limits — GitHub Actions allows up to 6 hours per job, but most teams set a far shorter timeout, and a cold full build of a large site can blow past a 10-minute budget. If your build can't finish reliably inside that window, you need a framework with incremental builds, not a faster laptop. Measure it once: run a full production build on a representative content set and time it with ",[253,5518,5519],{},"hyperfine 'npm run build'",". A 2,000-page Markdown site that builds in 18s on Hugo might take 70-110s on Astro and several minutes on Jekyll — the absolute numbers vary, but the ranking is consistent enough to plan around.",[14,5522,5523],{},"Match the framework's core language to your team. Hugo is Go, Jekyll is Ruby, and Astro and Eleventy are JavaScript. The maintenance cost of a framework is the cost of the language nobody on your team wants to debug at 2am. A team that lives in Node will be far more productive extending an Eleventy config than fighting a Ruby gem dependency, even if Jekyll is technically capable.",[14,5525,5526],{},"Finally, document how often content changes and who changes it. A docs site that publishes hourly from a CMS has different needs than a marketing site rebuilt on every commit — the former benefits from incremental builds and preview deploys; the latter barely notices build time at all. Content velocity also decides how much the authoring layer matters: if non-developers publish daily, a Git-backed visual editor and schema-validated frontmatter will save more engineering time than any build optimization. Capture all of this in three lines before you look at a single benchmark — the framework you reach for after writing those constraints is usually right, and the one you reach for from reputation usually isn't.",[14,5528,5529,5530,5532],{},"When you map technical constraints to a defensible choice, the ",[23,5531,26],{"href":25}," gives you a scoring frame so the decision survives review. The matrix is also where you encode the soft constraints — hiring, existing internal tooling, the language your platform team already supports — that a raw benchmark can't see but that dominate the total cost of ownership over the life of the site.",[34,5534,5536],{"id":5535},"architecture-rendering-paradigms","Architecture & Rendering Paradigms",[14,5538,5539],{},"The rendering model determines your performance ceiling, your hosting bill, and how much JavaScript reaches the browser — the first axis in the diagram above.",[14,5541,5542,5543,5546,5547,5550,5551,5554,5555,239],{},"Full static generation renders every page at build time. It has zero server cost and the best possible time-to-first-byte, but personalization and frequently changing data force full rebuilds. ",[229,5544,5545],{},"Template-only"," generators — Eleventy and Hugo — emit plain HTML and CSS with no client runtime at all, so they ship zero JavaScript unless you add a script yourself. ",[229,5548,5549],{},"Islands architecture"," (Astro, and increasingly others) ships static HTML and hydrates only the interactive components, keeping JavaScript off pages that don't need it. A ",[229,5552,5553],{},"Next.js static export"," gives you React's component model with pre-rendered output, but you pay for the framework runtime on every interactive page — that is the heaviest JavaScript baseline of the five, and it is exactly the trade-off weighed in ",[23,5556,5494],{"href":5493},[14,5558,5559,5560,5562],{},"A useful default: generate everything you can at build time, hydrate islands only where there's genuine interactivity, and reserve per-request logic for the handful of routes that truly need it. Because JavaScript on the main thread is the main driver of Interaction to Next Paint (INP), this single decision sets your Core Web Vitals ceiling — see ",[23,5561,5501],{"href":5500}," for how the metrics break down per framework.",[14,5564,5565],{},"The rendering model also constrains your hosting story. A fully static build is just files: it can sit on any CDN, in object storage, or behind a free static host, and it rolls back instantly because the previous build is still a set of files. The moment you depend on per-request rendering you've added a runtime — a function that cold-starts, costs money per invocation, and has to be monitored and rolled back like any server. For a content site that's a real liability you take on deliberately, not by default. The discipline that keeps a project on the cheap, fast, static path is resisting the temptation to reach for a server function the first time a feature is slightly easier with one; most of those features (search, comments, forms) have a static or third-party-API answer that keeps the output pre-rendered.",[14,5567,5568,5569,2781,5572,5574,5575,931],{},"CDN behaviour is part of this decision, not an afterthought. The example below configures Astro to inline small stylesheets and serve hashed assets from a CDN prefix, plus a redirect that preserves a legacy URL after migration. Note that ",[253,5570,5571],{},"redirects",[229,5573,2784],{}," Astro config key — it does not live under ",[253,5576,5577],{},"build",[987,5579,5581],{"className":1600,"code":5580,"language":1602,"meta":712,"style":712},"\u002F\u002F astro.config.mjs\nimport { defineConfig } from 'astro\u002Fconfig';\n\nexport default defineConfig({\n  build: {\n    \u002F\u002F Inline small CSS into the page; link larger stylesheets.\n    inlineStylesheets: 'auto',\n    \u002F\u002F Prefix hashed build assets so they're served from your CDN.\n    assetsPrefix: 'https:\u002F\u002Fcdn.example.com',\n  },\n  \u002F\u002F Top-level: preserve old URLs during a migration.\n  redirects: {\n    '\u002Flegacy-docs': '\u002Fdocs',\n  },\n});\n",[253,5582,5583,5587,5599,5603,5613,5618,5623,5633,5638,5648,5652,5657,5662,5674,5678],{"__ignoreMap":712},[995,5584,5585],{"class":997,"line":998},[995,5586,1609],{"class":1001},[995,5588,5589,5591,5593,5595,5597],{"class":997,"line":713},[995,5590,1615],{"class":1614},[995,5592,1619],{"class":1618},[995,5594,1622],{"class":1614},[995,5596,1625],{"class":1023},[995,5598,1628],{"class":1618},[995,5600,5601],{"class":997,"line":730},[995,5602,1541],{"emptyLinePlaceholder":752},[995,5604,5605,5607,5609,5611],{"class":997,"line":1544},[995,5606,1681],{"class":1614},[995,5608,1684],{"class":1614},[995,5610,1687],{"class":1007},[995,5612,1690],{"class":1618},[995,5614,5615],{"class":997,"line":1550},[995,5616,5617],{"class":1618},"  build: {\n",[995,5619,5620],{"class":997,"line":1673},[995,5621,5622],{"class":1001},"    \u002F\u002F Inline small CSS into the page; link larger stylesheets.\n",[995,5624,5625,5628,5631],{"class":997,"line":1678},[995,5626,5627],{"class":1618},"    inlineStylesheets: ",[995,5629,5630],{"class":1023},"'auto'",[995,5632,2885],{"class":1618},[995,5634,5635],{"class":997,"line":1693},[995,5636,5637],{"class":1001},"    \u002F\u002F Prefix hashed build assets so they're served from your CDN.\n",[995,5639,5640,5643,5646],{"class":997,"line":1705},[995,5641,5642],{"class":1618},"    assetsPrefix: ",[995,5644,5645],{"class":1023},"'https:\u002F\u002Fcdn.example.com'",[995,5647,2885],{"class":1618},[995,5649,5650],{"class":997,"line":1711},[995,5651,1729],{"class":1618},[995,5653,5654],{"class":997,"line":1717},[995,5655,5656],{"class":1001},"  \u002F\u002F Top-level: preserve old URLs during a migration.\n",[995,5658,5659],{"class":997,"line":1726},[995,5660,5661],{"class":1618},"  redirects: {\n",[995,5663,5664,5667,5669,5672],{"class":997,"line":1732},[995,5665,5666],{"class":1023},"    '\u002Flegacy-docs'",[995,5668,1925],{"class":1618},[995,5670,5671],{"class":1023},"'\u002Fdocs'",[995,5673,2885],{"class":1618},[995,5675,5676],{"class":997,"line":2967},[995,5677,1729],{"class":1618},[995,5679,5680],{"class":997,"line":2972},[995,5681,1735],{"class":1618},[14,5683,5684,5685,5688,5689,5692],{},"Inlining small stylesheets removes a render-blocking request, while ",[253,5686,5687],{},"assetsPrefix"," points hashed assets at a CDN origin so they cache aggressively and independently of your HTML. Caching ",[18,5690,5691],{},"headers"," themselves are configured at your host or CDN, not here.",[34,5694,5696],{"id":5695},"build-performance-cicd-optimization","Build Performance & CI\u002FCD Optimization",[14,5698,5699],{},"Build performance only matters at scale, but at scale it dominates everything — and it is the axis where the five generators diverge most.",[14,5701,5702,5703,5705],{},"Hugo is the outlier. Its Go engine renders thousands of pages per second and routinely builds 10,000-page sites in seconds where Node-based generators take minutes. That gap is the whole reason Hugo keeps winning large-repository projects, and ",[23,5704,2478],{"href":2477}," walks through the benchmarks and the tuning that closes or widens it. For most other generators the single biggest lever is incremental builds: rebuilding only the pages affected by a change instead of the whole site. Beyond that, parallel asset processing (image transforms, font subsetting) keeps CPU-bound steps from serializing, and caching the framework's build cache between CI runs avoids re-doing work on every commit.",[14,5707,5708],{},"Before optimizing, measure. Hugo, for example, can report per-template timing so you can see which layouts actually cost you:",[987,5710,5712],{"className":989,"code":5711,"language":991,"meta":712,"style":712},"hugo --gc --minify --templateMetrics --templateMetricsHints\n",[253,5713,5714],{"__ignoreMap":712},[995,5715,5716,5718,5720,5722,5724],{"class":997,"line":998},[995,5717,259],{"class":1007},[995,5719,3639],{"class":1010},[995,5721,3642],{"class":1010},[995,5723,3617],{"class":1010},[995,5725,3620],{"class":1010},[14,5727,5728,5731,5732,5735,5736,738,5739,5741],{},[253,5729,5730],{},"--gc"," clears unused cache entries, ",[253,5733,5734],{},"--minify"," compresses output, and the template-metrics flags print an execution-time table per template — the fastest way to find the one partial that's being rendered ten thousand times. The same instinct applies to every generator: profile before you optimize, because the bottleneck is almost never where you'd guess. On Node-based generators it's usually image processing or an expensive Markdown plugin running on every page; on Jekyll it's frequently a slow Liquid loop or an unbounded ",[253,5737,5738],{},"where",[253,5740,1842],{}," filter over a large collection.",[14,5743,5744,5745,239],{},"There's a second, quieter cost that benchmarks miss: the relationship between build time and developer feedback. A 90-second build is fine in CI but miserable in local development, where you rebuild dozens of times an hour. Generators with fast incremental dev servers — Astro's and Eleventy's hot reload, Hugo's sub-second rebuilds — keep authors in flow, while a slow cold build taxes every edit. Weigh the dev-loop experience alongside the CI number; they're different metrics that the same framework can score very differently on. The CI side of this — caching dependencies, sharing build caches across runners, and gating on preview deploys — is the subject of ",[23,5746,5505],{"href":5504},[34,5748,5750],{"id":5749},"ecosystem-maturity-plugin-stability","Ecosystem Maturity & Plugin Stability",[14,5752,5753],{},"A framework's plugin ecosystem is where projects quietly rot, so evaluate it like a dependency you'll own for years — the fourth axis in the overview.",[14,5755,5756,5757,5759],{},"Prefer official or first-party plugins for anything in the critical build path; they tend to track the framework's release cadence, while community plugins lag or get abandoned. Before adopting a third-party plugin, check its last release date, open-issue trend, and whether it pins to a framework version that's about to change. The ecosystems differ sharply: Jekyll's is mature but aging, with several once-essential plugins now unmaintained; Astro's is younger but moves with the core team; Hugo deliberately keeps most capability in core rather than plugins. The ",[23,5758,2225],{"href":2224}," review maps which Jekyll integrations are still safe to depend on and which signal it is time to migrate.",[14,5761,5762],{},"Security lives here too. Build-time plugins run with access to your repository and environment, so an unmaintained dependency is a real exposure, not a hypothetical one. A compromised or abandoned build plugin can read your environment secrets, write arbitrary files into the output, or pull in transitive dependencies you never audited. Confirm the framework and its core plugins follow semantic versioning so a minor upgrade doesn't break your build without warning, pin versions in a lockfile, and run a dependency audit in CI so a newly disclosed vulnerability fails the build instead of shipping silently.",[14,5764,5765,5766,5769],{},"A practical way to score ecosystem health is to count how many of your ",[18,5767,5768],{},"required"," capabilities are met by the framework's core versus by third-party code. A generator that handles images, syntax highlighting, sitemaps, and feeds in core leaves you with a small, auditable dependency surface. One that needs a community plugin for each of those is four maintenance relationships you didn't choose, any of which can go stale between framework releases. Fewer dependencies isn't a vanity metric here — it's directly proportional to how much unplanned work the project generates over its life.",[34,5771,5773],{"id":5772},"content-modeling-developer-experience","Content Modeling & Developer Experience",[14,5775,5776],{},"Good content modeling is what keeps a site editable after it grows past a few hundred pages — and the authoring axis is where Astro and Jekyll tend to score above the others.",[14,5778,5779],{},"Validate your content schema at build time rather than discovering a malformed page in production. Type-safe content — Astro's content collections, or a Zod\u002FJSON-Schema check in any framework — turns a broken frontmatter field into a build error instead of a blank page. Reusable layouts and components keep templates from drifting as the site expands.",[14,5781,5782],{},"The Eleventy config below restricts which file types are processed and sets a default layout for every page, which is the kind of small, explicit convention that prevents surprises later:",[987,5784,5786],{"className":1600,"code":5785,"language":1602,"meta":712,"style":712},"\u002F\u002F .eleventy.js\nmodule.exports = function (eleventyConfig) {\n  eleventyConfig.addGlobalData(\"layout\", \"docs.njk\");\n  eleventyConfig.setTemplateFormats([\"md\", \"njk\"]);\n  return { dir: { input: \"src\", output: \"dist\" } };\n};\n",[253,5787,5788,5792,5810,5830,5851,5871],{"__ignoreMap":712},[995,5789,5790],{"class":997,"line":998},[995,5791,1762],{"class":1001},[995,5793,5794,5796,5798,5800,5802,5804,5806,5808],{"class":997,"line":713},[995,5795,1767],{"class":1010},[995,5797,239],{"class":1618},[995,5799,1772],{"class":1010},[995,5801,1775],{"class":1614},[995,5803,1778],{"class":1614},[995,5805,1781],{"class":1618},[995,5807,1785],{"class":1784},[995,5809,1788],{"class":1618},[995,5811,5812,5814,5817,5819,5822,5824,5827],{"class":997,"line":730},[995,5813,1793],{"class":1618},[995,5815,5816],{"class":1007},"addGlobalData",[995,5818,1799],{"class":1618},[995,5820,5821],{"class":1023},"\"layout\"",[995,5823,1850],{"class":1618},[995,5825,5826],{"class":1023},"\"docs.njk\"",[995,5828,5829],{"class":1618},");\n",[995,5831,5832,5834,5837,5840,5843,5845,5848],{"class":997,"line":1544},[995,5833,1793],{"class":1618},[995,5835,5836],{"class":1007},"setTemplateFormats",[995,5838,5839],{"class":1618},"([",[995,5841,5842],{"class":1023},"\"md\"",[995,5844,1850],{"class":1618},[995,5846,5847],{"class":1023},"\"njk\"",[995,5849,5850],{"class":1618},"]);\n",[995,5852,5853,5856,5859,5862,5865,5868],{"class":997,"line":1550},[995,5854,5855],{"class":1614},"  return",[995,5857,5858],{"class":1618}," { dir: { input: ",[995,5860,5861],{"class":1023},"\"src\"",[995,5863,5864],{"class":1618},", output: ",[995,5866,5867],{"class":1023},"\"dist\"",[995,5869,5870],{"class":1618}," } };\n",[995,5872,5873],{"class":997,"line":1673},[995,5874,1877],{"class":1618},[14,5876,5877,5878,5881,5882,5884,5885,5887],{},"This sets ",[253,5879,5880],{},"docs.njk"," as the global default layout, processes only Markdown and Nunjucks files, and fixes the input\u002Foutput directories so builds are predictable across machines. When you're architecting a content-heavy docs platform, ",[23,5883,774],{"href":773}," compares the routing and schema-validation trade-offs in detail, and if your authors aren't developers, ",[23,5886,328],{"href":327}," reframes the whole decision around the authoring layer.",[653,5889,5891],{"id":5890},"the-five-generators-on-core-web-vitals","The five generators on Core Web Vitals",[14,5893,5894,5895,5897,5898,239],{},"Because the rendering model sets the JavaScript baseline, the five candidates line up predictably on field performance. Eleventy and Hugo ship no client runtime, so a content page's Total Blocking Time is whatever third-party scripts you add — zero if you add none. Astro matches that baseline on static pages and only pays for the islands you hydrate, which keeps INP low as long as you avoid ",[253,5896,2211],{}," on presentational markup. A Next.js static export is the outlier: even an export ships React's runtime and the per-page hydration code, so an interactive route carries tens of kilobytes of JavaScript that must parse and execute before the page is fully interactive. None of this means the export is slow — it can score well — but it starts from a heavier position and demands more discipline to keep INP in budget. The detailed per-metric breakdown, including the LCP and CLS fixes that apply regardless of framework, lives in ",[23,5899,5501],{"href":5500},[34,5901,5903],{"id":5902},"edge-delivery-cicd-rollbacks","Edge Delivery, CI\u002FCD & Rollbacks",[14,5905,5906,5907,5909,5910,5912],{},"The framework produces static files; everything after that is delivery. Distribute through a CDN to cut Time to First Byte, apply the two-tier cache policy — immutable year-long caching for hashed assets, short-lived HTML — and gate every deploy on a performance budget so regressions fail the build instead of shipping. Because the output is just files, rollback is cheap: redeploy the previous build. The full treatment of preview environments, build hooks, and atomic deploys lives in ",[23,5908,5505],{"href":5504},", and the caching and Core Web Vitals side in ",[23,5911,5501],{"href":5500},". The framework choice rarely eliminates a host — every major platform builds every major generator — but a host's incremental-build and preview-deploy support can break a tie between two otherwise-equal candidates.",[34,5914,2266],{"id":2265},[39,5916,5917,5923,5929,5935,5941],{},[42,5918,5919,5922],{},[229,5920,5921],{},"Choosing on benchmarks alone:"," raw build speed only dominates in the thousands of pages. Below that, authoring experience and ecosystem health matter more than a few seconds of build time.",[42,5924,5925,5928],{},[229,5926,5927],{},"Hydrating SEO-critical pages on the client:"," it raises TTFB, hurts Core Web Vitals, and throws away the benefit of static generation. Use static HTML with progressive enhancement, or hydrate only true islands.",[42,5930,5931,5934],{},[229,5932,5933],{},"Ignoring CI\u002FCD timeouts when picking a framework:"," a large repo that only does full rebuilds will eventually fail its pipeline. Require incremental builds or split the build across nodes.",[42,5936,5937,5940],{},[229,5938,5939],{},"Adopting unmaintained plugins:"," abandoned build-time dependencies become security exposures and break on the next framework upgrade. Check maintenance status before you commit.",[42,5942,5943,5946],{},[229,5944,5945],{},"Hardcoding secrets in build scripts:"," this leaks them into git history and makes environment switching painful. Use your platform's secret injection instead.",[34,5948,2321],{"id":2320},[39,5950,5951,5954,5957,5960,5963],{},[42,5952,5953],{},"There is no single \"best\" SSG — there's the one whose strong axes (JS shipped, build speed, authoring, ecosystem) match your constraints.",[42,5955,5956],{},"Decide the rendering model first; it sets your JavaScript baseline and therefore your Core Web Vitals ceiling.",[42,5958,5959],{},"Hugo wins raw build speed decisively, but that only matters once you're into the thousands of pages.",[42,5961,5962],{},"Eleventy and Hugo ship zero JavaScript by default; Astro ships none until you hydrate an island; a Next.js export carries React on every interactive route.",[42,5964,5965],{},"Verify a cold build finishes inside your CI window, and confirm the ecosystem you depend on is actively maintained, before you commit.",[34,5967,651],{"id":650},[653,5969,5971],{"id":5970},"what-is-the-single-most-important-factor-when-choosing-an-ssg-for-production","What is the single most important factor when choosing an SSG for production?",[14,5973,5974],{},"Whether a cold full build finishes inside your CI window. Build speed and incremental support decide whether you can publish reliably at all, and they get worse as the site grows. Authoring experience and ecosystem matter, but a build that times out blocks every release regardless of how nice the framework feels.",[653,5976,5978],{"id":5977},"which-static-site-generator-ships-the-least-javascript","Which static site generator ships the least JavaScript?",[14,5980,5981],{},"Eleventy and Hugo ship none by default because they are template-only and have no client runtime. Astro ships zero JavaScript until you hydrate an island. A Next.js static export ships React on every interactive route, so it carries the heaviest baseline of the five.",[653,5983,5985],{"id":5984},"is-hugo-always-the-right-choice-because-it-builds-fastest","Is Hugo always the right choice because it builds fastest?",[14,5987,5988],{},"No. Hugo wins raw build speed by a wide margin, but speed only dominates once a repository runs into the thousands of pages. For a few hundred pages the difference is seconds, so authoring experience, ecosystem, and the language your team can debug usually matter more than the benchmark.",[653,5990,5992],{"id":5991},"how-do-i-decide-between-astro-and-eleventy-for-a-documentation-site","How do I decide between Astro and Eleventy for a documentation site?",[14,5994,5995],{},"Choose Astro when you want a component model and the occasional interactive island with type-safe content collections. Choose Eleventy when you want the leanest possible Markdown-to-HTML pipeline with zero default JavaScript and minimal machinery to maintain. Both produce fast static docs.",[653,5997,5999],{"id":5998},"does-framework-choice-affect-core-web-vitals","Does framework choice affect Core Web Vitals?",[14,6001,6002],{},"Yes, mostly through how much JavaScript ships. Template-only generators and island-based ones give you the best INP baseline because little or no client code runs. A full-hydration export can still score well, but you pay for the framework runtime on every interactive page and have to budget it carefully.",[653,6004,6006],{"id":6005},"should-i-pick-the-framework-first-or-the-host-first","Should I pick the framework first or the host first?",[14,6008,6009],{},"Pick the framework against your team and content constraints first, then confirm your target host supports its build and caching model. Every major host runs every major generator, so the framework decision rarely eliminates a host, but a host's incremental-build and preview-deploy support can break a tie.",[34,6011,684],{"id":683},[39,6013,6014,6019,6024,6029,6034,6039,6044],{},[42,6015,6016,6018],{},[23,6017,774],{"href":773}," — the component-versus-templates comparison in depth.",[42,6020,6021,6023],{},[23,6022,2478],{"href":2477}," — why Hugo wins the build-speed axis and how to prove it.",[42,6025,6026,6028],{},[23,6027,2225],{"href":2224}," — evaluating ecosystem health and migration exits.",[42,6030,6031,6033],{},[23,6032,26],{"href":25}," — turn these axes into a scored, defensible choice.",[42,6035,6036,6038],{},[23,6037,5494],{"href":5493}," — when React's component model is worth the runtime cost.",[42,6040,6041,6043],{},[23,6042,5501],{"href":5500}," — making the chosen framework fast in production.",[42,6045,6046,6048],{},[23,6047,5505],{"href":5504}," — shipping it safely with previews and rollbacks.",[1346,6050,6051],{},"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 .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}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 .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":712,"searchDepth":713,"depth":713,"links":6053},[6054,6055,6056,6057,6058,6059,6062,6063,6064,6065,6073],{"id":5446,"depth":713,"text":5447},{"id":5509,"depth":713,"text":5510},{"id":5535,"depth":713,"text":5536},{"id":5695,"depth":713,"text":5696},{"id":5749,"depth":713,"text":5750},{"id":5772,"depth":713,"text":5773,"children":6060},[6061],{"id":5890,"depth":730,"text":5891},{"id":5902,"depth":713,"text":5903},{"id":2265,"depth":713,"text":2266},{"id":2320,"depth":713,"text":2321},{"id":650,"depth":713,"text":651,"children":6066},[6067,6068,6069,6070,6071,6072],{"id":5970,"depth":730,"text":5971},{"id":5977,"depth":730,"text":5978},{"id":5984,"depth":730,"text":5985},{"id":5991,"depth":730,"text":5992},{"id":5998,"depth":730,"text":5999},{"id":6005,"depth":730,"text":6006},{"id":683,"depth":713,"text":684},[6075,6076],{"name":737,"item":738},{"name":31,"item":30},"Match your team constraints to the right static site generator — Astro, Eleventy, Hugo, Jekyll, or a Next.js export — across JavaScript shipped, build speed, authoring, and ecosystem.",[6079,6080,6081,6082,6083,6084],{"q":5971,"a":5974},{"q":5978,"a":5981},{"q":5985,"a":5988},{"q":5992,"a":5995},{"q":5999,"a":6002},{"q":6006,"a":6009},{},"\u002Fchoosing-the-right-static-site-generator-for-production",{"title":31,"description":6077},"choosing-the-right-static-site-generator-for-production\u002Findex","pillar","J42v4Yxkr0LENVHKkQVLbAPaoeTYW1MFU4_oV_4YzEc",{"id":6092,"title":1161,"body":6093,"breadcrumb":6789,"dateModified":743,"datePublished":2446,"description":6794,"extension":745,"faq":6795,"meta":6806,"navigation":752,"path":6807,"seo":6808,"slug":6097,"stem":6809,"type":756,"__hash__":6810},"content\u002Fchoosing-the-right-static-site-generator-for-production\u002Fjekyll-plugin-ecosystem\u002Feleventy-vs-jekyll-for-markdown-heavy-blogs\u002Findex.md",{"type":7,"value":6094,"toc":6771},[6095,6098,6106,6108,6122,6197,6201,6208,6214,6345,6348,6409,6412,6416,6438,6442,6454,6519,6529,6531,6537,6596,6605,6609,6624,6626,6675,6677,6682,6684,6688,6694,6698,6704,6708,6718,6722,6728,6732,6742,6744,6768],[10,6096,1161],{"id":6097},"eleventy-vs-jekyll-for-markdown-heavy-blogs",[14,6099,6100,6101,6103,6104,239],{},"For a blog with thousands of Markdown files, the choice between Eleventy and Jekyll comes down to four things: how the Markdown parser is configured, how the data cascade resolves, how reliable incremental builds are, and how fast a full production build runs. This is a focused comparison; for plugin and dependency specifics see the ",[23,6102,2225],{"href":2224},", and for the wider framework decision, ",[23,6105,31],{"href":30},[34,6107,37],{"id":36},[39,6109,6110,6113,6116],{},[42,6111,6112],{},"A Markdown corpus large enough that build time matters — roughly a thousand files or more.",[42,6114,6115],{},"Node 18+ available for Eleventy, and Ruby 3.3+ with Bundler for Jekyll.",[42,6117,6118,6119,6121],{},"A way to time production builds repeatably (",[253,6120,595],{}," is ideal) so the comparison is not a single noisy run.",[55,6123,6124,6194],{},[58,6125,66,6129,66,6132,66,6135,66,6137],{"viewBox":3462,"role":61,"ariaLabelledBy":6126,"xmlns":65},[6127,6128],"evj-cmp-title","evj-cmp-desc",[68,6130,6131],{"id":6127},"Eleventy versus Jekyll on a markdown-heavy blog",[72,6133,6134],{"id":6128},"A side-by-side comparison of Eleventy on Node with markdown-it and Jekyll on Ruby with Kramdown and Liquid, showing parser, data model, incremental reliability, and a cold build time of about 19 seconds for Eleventy versus 58 seconds for Jekyll on a 5,000-post corpus.",[107,6136],{"x":2515,"y":2515,"width":2516,"height":3474,"fill":205},[95,6138,78,6139,78,6142,78,6145,78,6148,78,6151,78,6155,78,6158,78,6162,78,6165,78,6169,78,6173,78,6177,78,6180,78,6183,78,6186,78,6189,78,6191,66],{"style":813},[99,6140,6141],{"x":1415,"y":2521,"fill":103,"style":1416},"5,000-post corpus, same content, both engines",[107,6143],{"x":3578,"y":110,"width":6144,"height":5332,"rx":113,"fill":185,"opacity":115,"stroke":187,"style":116},"340",[99,6146,6147],{"x":3500,"y":873,"fill":187,"style":104},"Eleventy · Node",[99,6149,6150],{"x":3500,"y":4661,"fill":103,"style":829},"Parser: markdown-it",[99,6152,6154],{"x":3500,"y":6153,"fill":103,"style":829},"152","Data: cascade + JS data files",[99,6156,6157],{"x":3500,"y":160,"fill":103,"style":829},"Incremental: dependable (local)",[99,6159,6161],{"x":3500,"y":6160,"fill":103,"style":829},"208","Extend: addFilter, JS plugins",[99,6163,6164],{"x":3500,"y":2605,"fill":93,"style":859},"cold full build",[99,6166,6168],{"x":3500,"y":3577,"fill":187,"style":6167},"font-size:30px;font-weight:700;text-anchor:middle","~19s",[107,6170],{"x":6171,"y":110,"width":6144,"height":5332,"rx":113,"fill":2564,"opacity":6172,"stroke":2565,"style":116},"440","0.08",[99,6174,6176],{"x":6175,"y":873,"fill":2565,"style":104},"610","Jekyll · Ruby",[99,6178,6179],{"x":6175,"y":4661,"fill":103,"style":829},"Parser: Kramdown + Rouge",[99,6181,6182],{"x":6175,"y":6153,"fill":103,"style":829},"Data: _data + front-matter defaults",[99,6184,6185],{"x":6175,"y":160,"fill":103,"style":829},"Incremental: misses deps",[99,6187,6188],{"x":6175,"y":6160,"fill":103,"style":829},"Extend: Liquid filters, Ruby gems",[99,6190,6164],{"x":6175,"y":2605,"fill":93,"style":859},[99,6192,6193],{"x":6175,"y":3577,"fill":2565,"style":6167},"~58s",[218,6195,6196],{},"On the same 5,000-post corpus, Eleventy's Node\u002Fmarkdown-it pipeline built cold in ~19s against Jekyll's ~58s; the qualitative differences (data model, incremental reliability, extension language) often matter as much as the seconds.",[34,6198,6200],{"id":6199},"markdown-parsing-extensibility","Markdown Parsing & Extensibility",[14,6202,6203,6204,6207],{},"Eleventy uses ",[253,6205,6206],{},"markdown-it",", whose token-stream design makes custom syntax easy to add. Jekyll uses Kramdown, which is solid but configured differently. The practical difference shows up when you want custom blocks (callouts, admonitions).",[14,6209,6210,6211,6213],{},"Eleventy — register ",[253,6212,6206],{}," plugins directly:",[987,6215,6217],{"className":1600,"code":6216,"language":1602,"meta":712,"style":712},"\u002F\u002F eleventy.config.js\nconst markdownIt = require(\"markdown-it\");\n\nmodule.exports = function (eleventyConfig) {\n  const md = markdownIt({ html: true, linkify: true, typographer: true })\n    .use(require(\"markdown-it-container\"), \"note\"); \u002F\u002F enables ::: note blocks\n  eleventyConfig.setLibrary(\"md\", md);\n};\n",[253,6218,6219,6224,6244,6248,6266,6297,6327,6341],{"__ignoreMap":712},[995,6220,6221],{"class":997,"line":998},[995,6222,6223],{"class":1001},"\u002F\u002F eleventy.config.js\n",[995,6225,6226,6229,6232,6234,6237,6239,6242],{"class":997,"line":713},[995,6227,6228],{"class":1614},"const",[995,6230,6231],{"class":1010}," markdownIt",[995,6233,1775],{"class":1614},[995,6235,6236],{"class":1007}," require",[995,6238,1799],{"class":1618},[995,6240,6241],{"class":1023},"\"markdown-it\"",[995,6243,5829],{"class":1618},[995,6245,6246],{"class":997,"line":730},[995,6247,1541],{"emptyLinePlaceholder":752},[995,6249,6250,6252,6254,6256,6258,6260,6262,6264],{"class":997,"line":1544},[995,6251,1767],{"class":1010},[995,6253,239],{"class":1618},[995,6255,1772],{"class":1010},[995,6257,1775],{"class":1614},[995,6259,1778],{"class":1614},[995,6261,1781],{"class":1618},[995,6263,1785],{"class":1784},[995,6265,1788],{"class":1618},[995,6267,6268,6271,6274,6276,6278,6281,6284,6287,6289,6292,6294],{"class":997,"line":1550},[995,6269,6270],{"class":1614},"  const",[995,6272,6273],{"class":1010}," md",[995,6275,1775],{"class":1614},[995,6277,6231],{"class":1007},[995,6279,6280],{"class":1618},"({ html: ",[995,6282,6283],{"class":1010},"true",[995,6285,6286],{"class":1618},", linkify: ",[995,6288,6283],{"class":1010},[995,6290,6291],{"class":1618},", typographer: ",[995,6293,6283],{"class":1010},[995,6295,6296],{"class":1618}," })\n",[995,6298,6299,6302,6305,6307,6310,6312,6315,6318,6321,6324],{"class":997,"line":1673},[995,6300,6301],{"class":1618},"    .",[995,6303,6304],{"class":1007},"use",[995,6306,1799],{"class":1618},[995,6308,6309],{"class":1007},"require",[995,6311,1799],{"class":1618},[995,6313,6314],{"class":1023},"\"markdown-it-container\"",[995,6316,6317],{"class":1618},"), ",[995,6319,6320],{"class":1023},"\"note\"",[995,6322,6323],{"class":1618},"); ",[995,6325,6326],{"class":1001},"\u002F\u002F enables ::: note blocks\n",[995,6328,6329,6331,6334,6336,6338],{"class":997,"line":1678},[995,6330,1793],{"class":1618},[995,6332,6333],{"class":1007},"setLibrary",[995,6335,1799],{"class":1618},[995,6337,5842],{"class":1023},[995,6339,6340],{"class":1618},", md);\n",[995,6342,6343],{"class":997,"line":1693},[995,6344,1877],{"class":1618},[14,6346,6347],{},"Jekyll — configure Kramdown for GFM plus Rouge highlighting:",[987,6349,6351],{"className":1912,"code":6350,"language":1914,"meta":712,"style":712},"# _config.yml\nkramdown:\n  input: GFM\n  syntax_highlighter: rouge\n  syntax_highlighter_opts:\n    block:\n      line_numbers: true\n",[253,6352,6353,6358,6365,6375,6385,6392,6399],{"__ignoreMap":712},[995,6354,6355],{"class":997,"line":998},[995,6356,6357],{"class":1001},"# _config.yml\n",[995,6359,6360,6363],{"class":997,"line":713},[995,6361,6362],{"class":1921},"kramdown",[995,6364,1946],{"class":1618},[995,6366,6367,6370,6372],{"class":997,"line":730},[995,6368,6369],{"class":1921},"  input",[995,6371,1925],{"class":1618},[995,6373,6374],{"class":1023},"GFM\n",[995,6376,6377,6380,6382],{"class":997,"line":1544},[995,6378,6379],{"class":1921},"  syntax_highlighter",[995,6381,1925],{"class":1618},[995,6383,6384],{"class":1023},"rouge\n",[995,6386,6387,6390],{"class":997,"line":1550},[995,6388,6389],{"class":1921},"  syntax_highlighter_opts",[995,6391,1946],{"class":1618},[995,6393,6394,6397],{"class":997,"line":1673},[995,6395,6396],{"class":1921},"    block",[995,6398,1946],{"class":1618},[995,6400,6401,6404,6406],{"class":997,"line":1678},[995,6402,6403],{"class":1921},"      line_numbers",[995,6405,1925],{"class":1618},[995,6407,6408],{"class":1010},"true\n",[14,6410,6411],{},"Both render GFM tables once configured; the difference is that Eleventy's extension model is JavaScript you already know, while Kramdown extensions live in Ruby.",[34,6413,6415],{"id":6414},"data-cascade-frontmatter","Data Cascade & Frontmatter",[14,6417,6418,6419,6422,6423,6426,6427,6430,6431,6433,6434,6437],{},"Jekyll merges ",[253,6420,6421],{},"_data\u002F"," globally and applies front matter defaults from ",[253,6424,6425],{},"_config.yml",". Eleventy resolves data top-down through its cascade, with ",[253,6428,6429],{},"eleventyComputed"," for per-item overrides and ",[253,6432,5816],{}," for site-wide values. Eleventy's data files can be plain JavaScript (",[253,6435,6436],{},"require()"," real modules), which avoids the awkwardness of expressing complex structures in YAML. In either tool, keep frontmatter shallow — deeply nested YAML is slow to parse and easy to get wrong; move complex data into data files.",[34,6439,6441],{"id":6440},"build-performance-incremental-reliability","Build Performance & Incremental Reliability",[14,6443,6444,6445,6447,6448,6450,6451,6453],{},"This is where they diverge most. Jekyll's ",[253,6446,981],{}," is explicitly a development convenience and is known to miss dependencies — a changed layout or ",[253,6449,6421],{}," file may not regenerate every page that depends on it, so production deploys should use full builds. Eleventy's ",[253,6452,981],{}," is more dependable but is also intended for local iteration. Define explicit watch targets so data changes trigger the rebuilds you expect:",[987,6455,6457],{"className":1600,"code":6456,"language":1602,"meta":712,"style":712},"\u002F\u002F eleventy.config.js\nmodule.exports = function (eleventyConfig) {\n  eleventyConfig.setServerOptions({ liveReload: true, domDiff: true });\n  eleventyConfig.addWatchTarget(\".\u002Fsrc\u002F_data\u002F\");\n};\n",[253,6458,6459,6463,6481,6501,6515],{"__ignoreMap":712},[995,6460,6461],{"class":997,"line":998},[995,6462,6223],{"class":1001},[995,6464,6465,6467,6469,6471,6473,6475,6477,6479],{"class":997,"line":713},[995,6466,1767],{"class":1010},[995,6468,239],{"class":1618},[995,6470,1772],{"class":1010},[995,6472,1775],{"class":1614},[995,6474,1778],{"class":1614},[995,6476,1781],{"class":1618},[995,6478,1785],{"class":1784},[995,6480,1788],{"class":1618},[995,6482,6483,6485,6488,6491,6493,6496,6498],{"class":997,"line":730},[995,6484,1793],{"class":1618},[995,6486,6487],{"class":1007},"setServerOptions",[995,6489,6490],{"class":1618},"({ liveReload: ",[995,6492,6283],{"class":1010},[995,6494,6495],{"class":1618},", domDiff: ",[995,6497,6283],{"class":1010},[995,6499,6500],{"class":1618}," });\n",[995,6502,6503,6505,6508,6510,6513],{"class":997,"line":1544},[995,6504,1793],{"class":1618},[995,6506,6507],{"class":1007},"addWatchTarget",[995,6509,1799],{"class":1618},[995,6511,6512],{"class":1023},"\".\u002Fsrc\u002F_data\u002F\"",[995,6514,5829],{"class":1618},[995,6516,6517],{"class":997,"line":1550},[995,6518,1877],{"class":1618},[14,6520,6521,6522,270,6525,6528],{},"For CI, run full builds in production mode for both (",[253,6523,6524],{},"bundle exec jekyll build",[253,6526,6527],{},"npx @11ty\u002Feleventy","), and reserve incremental flags for local authoring.",[34,6530,1166],{"id":1165},[14,6532,6533,6534,931],{},"The numbers below come from a single 5,000-post Markdown corpus — same files, same frontmatter, same image references — built on a fixed 2-vCPU container and timed with ",[253,6535,6536],{},"hyperfine --warmup 1 --runs 10",[433,6538,6539,6550],{},[436,6540,6541],{},[439,6542,6543,6546,6548],{},[442,6544,6545],{},"Metric",[442,6547,273],{},[442,6549,277],{},[457,6551,6552,6563,6574,6585],{},[439,6553,6554,6557,6560],{},[462,6555,6556],{},"Cold full build (mean)",[462,6558,6559],{},"19.1s ± 0.6s",[462,6561,6562],{},"57.8s ± 1.7s",[439,6564,6565,6568,6571],{},[462,6566,6567],{},"Warm full build (mean)",[462,6569,6570],{},"18.4s ± 0.5s",[462,6572,6573],{},"55.2s ± 1.4s",[439,6575,6576,6579,6582],{},[462,6577,6578],{},"Local incremental edit (1 post)",[462,6580,6581],{},"~0.4s",[462,6583,6584],{},"~1.1s (misses some deps)",[439,6586,6587,6590,6593],{},[462,6588,6589],{},"Peak RSS (cold build)",[462,6591,6592],{},"~640 MB",[462,6594,6595],{},"~810 MB",[14,6597,6598,6599,6601,6602,6604],{},"Two things stand out. The cold-build gap is large and mostly reflects the Node\u002F",[253,6600,6206],{}," pipeline against Ruby\u002FKramdown plus Liquid. And the full warm build barely differs from cold for either tool, because neither does meaningful incremental work for production — the warm win is in local editing, where Eleventy's incremental mode is both faster and more dependable. For a rigorous way to produce comparable numbers yourself, follow ",[23,6603,288],{"href":287}," — the same controlled harness applies to any pair of generators.",[34,6606,6608],{"id":6607},"plugin-filter-model","Plugin & Filter Model",[14,6610,6611,6612,6615,6616,6619,6620,239],{},"Custom filters are simple in both, just in different languages: Jekyll registers them through a Liquid plugin (",[253,6613,6614],{},"Liquid::Template.register_filter","), Eleventy with ",[253,6617,6618],{},"eleventyConfig.addFilter",". The maintenance trade-off matters more than the API: a JavaScript filter is a quick fix-and-redeploy, whereas an abandoned Ruby gem with native extensions can block a build until someone patches it — a recurring theme in the Jekyll ecosystem. If that maintenance risk is what is pushing you off Jekyll, the concrete swap list is in ",[23,6621,6623],{"href":6622},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fjekyll-plugin-ecosystem\u002Freplacing-jekyll-plugins-when-migrating-to-eleventy\u002F","Replacing Jekyll Plugins When Migrating to Eleventy",[34,6625,600],{"id":599},[39,6627,6628,6642,6656,6670],{},[42,6629,6630,6633,6634,6637,6638,6641],{},[229,6631,6632],{},"Deeply nested Liquid includes:"," very deep recursive ",[253,6635,6636],{},"include"," chains can raise ",[253,6639,6640],{},"Liquid::StackLevelError",". Flatten the include hierarchy, or move layout logic into Eleventy's computed data.",[42,6643,6644,6647,6648,6651,6652,6655],{},[229,6645,6646],{},"Duplicate permalinks:"," when files share a name across folders, a ",[253,6649,6650],{},"fileSlug","-only permalink collides. Use a permalink that includes the path or date, or set ",[253,6653,6654],{},"eleventyComputed.permalink"," for deterministic routing.",[42,6657,6658,6661,6662,6665,6666,6669],{},[229,6659,6660],{},"Kramdown TOC vs raw HTML:"," Kramdown's ",[253,6663,6664],{},"{:toc}"," can choke on embedded HTML or unescaped Liquid. Wrap literal template syntax in ",[253,6667,6668],{},"{% raw %}"," and keep custom HTML out of the TOC region.",[42,6671,6672,6674],{},[229,6673,637],{}," keep both engines building in parallel during a migration. Because the Markdown source is shared, you can revert to the Jekyll build by switching the deploy command back until the Eleventy output matches.",[34,6676,642],{"id":641},[14,6678,6679,6680,239],{},"Choose Eleventy if you want JavaScript-native extensibility, a flexible data cascade, more dependable incremental builds, and the faster cold build measured above; choose Jekyll if you value its long-stable conventions and Ruby ecosystem. For a Markdown-heavy blog at scale, the deciding factors are usually incremental-build reliability and how painful your custom-filter language is to maintain — both of which lean Eleventy. Either way, settle the dependency and CI discipline first by reading the ",[23,6681,2225],{"href":2224},[34,6683,651],{"id":650},[653,6685,6687],{"id":6686},"does-eleventy-support-incremental-builds-for-large-blogs","Does Eleventy support incremental builds for large blogs?",[14,6689,6690,6691,6693],{},"Yes, via ",[253,6692,981],{}," with configured watch targets, but treat it as a local-development accelerator and run full builds for production deploys. Eleventy's incremental mode is more dependable than Jekyll's, though it is still intended for local iteration.",[653,6695,6697],{"id":6696},"how-should-i-handle-complex-frontmatter-in-jekyll","How should I handle complex frontmatter in Jekyll?",[14,6699,6700,6701,6703],{},"Keep frontmatter shallow and move complex or nested structures into ",[253,6702,6421],{}," files in YAML or JSON, which are easier to validate than deeply nested inline YAML. The same advice applies to Eleventy, where data files can be plain JavaScript modules.",[653,6705,6707],{"id":6706},"which-renders-markdown-tables-better-for-documentation","Which renders Markdown tables better for documentation?",[14,6709,6710,6711,6713,6714,6717],{},"Both do once configured. Eleventy's ",[253,6712,6206],{}," enables GFM tables with minimal setup, while Jekyll needs ",[253,6715,6716],{},"kramdown.input: GFM",". The difference is configuration effort, not output quality.",[653,6719,6721],{"id":6720},"is-eleventy-faster-than-jekyll-on-a-large-markdown-corpus","Is Eleventy faster than Jekyll on a large Markdown corpus?",[14,6723,6724,6725,6727],{},"Generally yes on cold builds, because Eleventy runs on Node with ",[253,6726,6206],{}," while Jekyll runs Ruby with Kramdown and Liquid. On a 5,000-post corpus Eleventy built in about 19 seconds cold versus roughly 58 seconds for Jekyll, though exact numbers depend on template complexity.",[653,6729,6731],{"id":6730},"which-is-easier-to-extend-for-a-markdown-heavy-blog","Which is easier to extend for a markdown-heavy blog?",[14,6733,6734,6735,6737,6738,6741],{},"Eleventy, for most JavaScript teams. Its ",[253,6736,6206],{}," plugins and ",[253,6739,6740],{},"addFilter"," API are JavaScript you likely already know, whereas Kramdown extensions and Liquid filters live in Ruby and can be blocked by an unmaintained native gem.",[34,6743,684],{"id":683},[39,6745,6746,6753,6758,6763],{},[42,6747,6748,692,6750,6752],{},[229,6749,691],{},[23,6751,2225],{"href":2224}," — dependency and CI discipline for Jekyll.",[42,6754,6755,6757],{},[23,6756,6623],{"href":6622}," — the plugin-to-equivalent mapping for a move.",[42,6759,6760,6762],{},[23,6761,288],{"href":287}," — produce comparable build numbers yourself.",[42,6764,6765,6767],{},[23,6766,774],{"href":773}," — Eleventy weighed against Astro for docs.",[1346,6769,6770],{},"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 .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}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":712,"searchDepth":713,"depth":713,"links":6772},[6773,6774,6775,6776,6777,6778,6779,6780,6781,6788],{"id":36,"depth":713,"text":37},{"id":6199,"depth":713,"text":6200},{"id":6414,"depth":713,"text":6415},{"id":6440,"depth":713,"text":6441},{"id":1165,"depth":713,"text":1166},{"id":6607,"depth":713,"text":6608},{"id":599,"depth":713,"text":600},{"id":641,"depth":713,"text":642},{"id":650,"depth":713,"text":651,"children":6782},[6783,6784,6785,6786,6787],{"id":6686,"depth":730,"text":6687},{"id":6696,"depth":730,"text":6697},{"id":6706,"depth":730,"text":6707},{"id":6720,"depth":730,"text":6721},{"id":6730,"depth":730,"text":6731},{"id":683,"depth":713,"text":684},[6790,6791,6792,6793],{"name":737,"item":738},{"name":31,"item":30},{"name":2225,"item":2224},{"name":1161,"item":1160},"For thousands of Markdown files, compare Eleventy and Jekyll on parser config, data cascade, incremental-build reliability, and real build numbers from a 5,000-post corpus.",[6796,6798,6800,6802,6804],{"q":6687,"a":6797},"Yes, via --incremental with configured watch targets, but treat it as a local-development accelerator and run full builds for production deploys. Eleventy's incremental mode is more dependable than Jekyll's, though it is still intended for local iteration.",{"q":6697,"a":6799},"Keep frontmatter shallow and move complex or nested structures into _data files in YAML or JSON, which are easier to validate than deeply nested inline YAML. The same advice applies to Eleventy, where data files can be plain JavaScript modules.",{"q":6707,"a":6801},"Both do once configured. Eleventy's markdown-it enables GFM tables with minimal setup, while Jekyll needs kramdown input set to GFM. The difference is configuration effort, not output quality.",{"q":6721,"a":6803},"Generally yes on cold builds, because Eleventy runs on Node with markdown-it while Jekyll runs Ruby with Kramdown and Liquid. On a 5,000-post corpus Eleventy built in about 19 seconds cold versus roughly 58 seconds for Jekyll, though exact numbers depend on template complexity.",{"q":6731,"a":6805},"Eleventy, for most JavaScript teams. Its markdown-it plugins and addFilter API are JavaScript you likely already know, whereas Kramdown extensions and Liquid filters live in Ruby and can be blocked by an unmaintained native gem.",{},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fjekyll-plugin-ecosystem\u002Feleventy-vs-jekyll-for-markdown-heavy-blogs",{"title":1161,"description":6794},"choosing-the-right-static-site-generator-for-production\u002Fjekyll-plugin-ecosystem\u002Feleventy-vs-jekyll-for-markdown-heavy-blogs\u002Findex","VXKxqD0_aC_HnVtq5P0H6VUtI9WWmVjg1T_T_yTzrCs",{"id":6812,"title":2225,"body":6813,"breadcrumb":7760,"dateModified":743,"datePublished":2446,"description":7764,"extension":745,"faq":7765,"meta":7777,"navigation":752,"path":7778,"seo":7779,"slug":6817,"stem":7780,"type":2460,"__hash__":7781},"content\u002Fchoosing-the-right-static-site-generator-for-production\u002Fjekyll-plugin-ecosystem\u002Findex.md",{"type":7,"value":6814,"toc":7741},[6815,6818,6823,6935,6939,6964,7029,7039,7043,7057,7164,7174,7223,7232,7236,7254,7257,7291,7305,7336,7345,7349,7360,7369,7373,7393,7502,7518,7522,7531,7533,7591,7593,7625,7627,7631,7644,7648,7657,7661,7676,7680,7685,7689,7705,7709,7712,7714,7738],[10,6816,2225],{"id":6817},"jekyll-plugin-ecosystem",[14,6819,6820,6821,239],{},"Jekyll is mature and stable, and a production setup is mostly about dependency discipline: pin your gems, group plugins correctly, commit the lockfile, and keep builds reproducible across machines and CI. This guide covers the plugin and dependency model, a working CI pipeline, the build-optimization levers that actually matter, and how to measure them. For the broader framework choice, see ",[23,6822,31],{"href":30},[55,6824,6825,6932],{},[58,6826,66,6830,66,6833,66,6836,66,6838,66,6925],{"viewBox":3462,"role":61,"ariaLabelledBy":6827,"xmlns":65},[6828,6829],"jek-flow-title","jek-flow-desc",[68,6831,6832],{"id":6828},"Jekyll plugin categories and where they run in the build",[72,6834,6835],{"id":6829},"Three categories of Jekyll plugins — generators, converters, and tags or filters — feed the Liquid render stage, which produces the _site output, with build-time SEO, feed, and sitemap plugins shown in the default Bundler group.",[107,6837],{"x":2515,"y":2515,"width":2516,"height":3474,"fill":205},[95,6839,78,6840,78,6843,78,6846,78,6851,78,6854,78,6858,78,6860,78,6863,78,6867,78,6870,78,6873,78,6876,78,6880,78,6884,78,6886,78,6890,78,6893,78,6896,78,6898,78,6901,78,6904,78,6907,78,6922,66],{"style":813},[99,6841,6842],{"x":1415,"y":2521,"fill":103,"style":1416},"Plugin categories feed one Liquid render",[107,6844],{"x":109,"y":849,"width":175,"height":6845,"rx":823,"fill":824,"opacity":825,"stroke":824,"style":116},"72",[99,6847,6850],{"x":6848,"y":6849,"fill":824,"style":121},"125","98","Generators",[99,6852,6853],{"x":6848,"y":1431,"fill":93,"style":126},"feed, sitemap,",[99,6855,6857],{"x":6848,"y":6856,"fill":93,"style":126},"136","archives",[107,6859],{"x":109,"y":134,"width":175,"height":6845,"rx":823,"fill":114,"opacity":186,"stroke":114,"style":116},[99,6861,6862],{"x":6848,"y":845,"fill":114,"style":121},"Converters",[99,6864,6866],{"x":6848,"y":6865,"fill":93,"style":126},"206","Kramdown,",[99,6868,6869],{"x":6848,"y":146,"fill":93,"style":126},"Rouge",[107,6871],{"x":109,"y":6872,"width":175,"height":6845,"rx":823,"fill":185,"opacity":850,"stroke":187,"style":116},"242",[99,6874,6875],{"x":6848,"y":5332,"fill":187,"style":121},"Tags & filters",[99,6877,6879],{"x":6848,"y":6878,"fill":93,"style":126},"292","seo-tag, custom",[99,6881,6883],{"x":6848,"y":6882,"fill":93,"style":126},"308","Liquid filters",[107,6885],{"x":1463,"y":161,"width":175,"height":120,"rx":823,"fill":162,"opacity":877,"stroke":164,"style":116},[99,6887,6889],{"x":6888,"y":845,"fill":103,"style":121},"415","Liquid render",[99,6891,6892],{"x":6888,"y":6160,"fill":93,"style":126},"layouts + includes",[99,6894,6895],{"x":6888,"y":5396,"fill":93,"style":126},"per page",[107,6897],{"x":6175,"y":161,"width":160,"height":120,"rx":823,"fill":2564,"opacity":825,"stroke":2565,"style":116},[99,6899,6900],{"x":3571,"y":845,"fill":2565,"style":121},"_site output",[99,6902,6903],{"x":3571,"y":6160,"fill":93,"style":126},"disposable HTML",[99,6905,6906],{"x":3571,"y":5396,"fill":93,"style":126},"deploy target",[95,6908,88,6909,88,6913,88,6916,88,6919,78],{"stroke":93,"fill":205,"style":116},[90,6910],{"d":6911,"style":6912},"M220 106 L318 178","marker-end:url(#jek-arrow)",[90,6914],{"d":6915,"style":6912},"M220 192 L318 192",[90,6917],{"d":6918,"style":6912},"M220 278 L318 206",[90,6920],{"d":6921,"style":6912},"M510 192 L608 192",[99,6923,6924],{"x":6888,"y":874,"fill":93,"style":126},"Build-time plugins go in the default Bundler group so CI runs them too.",[76,6926,78,6927,66],{},[80,6928,88,6930,78],{"id":6929,"viewBox":83,"refX":84,"refY":85,"markerWidth":86,"markerHeight":86,"orient":87},"jek-arrow",[90,6931],{"d":92,"fill":93},[218,6933,6934],{},"Generators, converters, and tags\u002Ffilters all feed one Liquid render that emits the disposable `_site`; build-time plugins must live in the default Bundler group so CI runs them.",[34,6936,6938],{"id":6937},"plugin-architecture-dependency-management","Plugin Architecture & Dependency Management",[14,6940,6941,6942,6945,6946,6949,6950,6953,6954,6957,6958,6961,6962,931],{},"Unpinned gems are the main source of \"works on my machine\" build failures. Pin core dependencies in the ",[253,6943,6944],{},"Gemfile"," and commit ",[253,6947,6948],{},"Gemfile.lock",". The standard SEO\u002Ffeed\u002Fsitemap plugins run as part of the build, so they belong in the ",[229,6951,6952],{},"default"," group, not ",[253,6955,6956],{},":development"," — only genuinely local-only tooling (like ",[253,6959,6960],{},"jekyll-remote-theme"," when you preview themes locally) goes under ",[253,6963,6956],{},[987,6965,6969],{"className":6966,"code":6967,"language":6968,"meta":712,"style":712},"language-ruby shiki shiki-themes github-light github-dark","# Gemfile\nsource \"https:\u002F\u002Frubygems.org\"\n\ngem \"jekyll\", \"~> 4.3\"\ngem \"jekyll-seo-tag\", \"~> 2.8\"\ngem \"jekyll-feed\", \"~> 0.17\"\ngem \"jekyll-sitemap\", \"~> 1.4\"\ngem \"webrick\", \"~> 1.8\"   # not bundled with Ruby 3.0+\n\ngroup :development do\n  gem \"jekyll-remote-theme\", \"~> 0.4\"\nend\n","ruby",[253,6970,6971,6976,6981,6985,6990,6995,7000,7005,7010,7014,7019,7024],{"__ignoreMap":712},[995,6972,6973],{"class":997,"line":998},[995,6974,6975],{},"# Gemfile\n",[995,6977,6978],{"class":997,"line":713},[995,6979,6980],{},"source \"https:\u002F\u002Frubygems.org\"\n",[995,6982,6983],{"class":997,"line":730},[995,6984,1541],{"emptyLinePlaceholder":752},[995,6986,6987],{"class":997,"line":1544},[995,6988,6989],{},"gem \"jekyll\", \"~> 4.3\"\n",[995,6991,6992],{"class":997,"line":1550},[995,6993,6994],{},"gem \"jekyll-seo-tag\", \"~> 2.8\"\n",[995,6996,6997],{"class":997,"line":1673},[995,6998,6999],{},"gem \"jekyll-feed\", \"~> 0.17\"\n",[995,7001,7002],{"class":997,"line":1678},[995,7003,7004],{},"gem \"jekyll-sitemap\", \"~> 1.4\"\n",[995,7006,7007],{"class":997,"line":1693},[995,7008,7009],{},"gem \"webrick\", \"~> 1.8\"   # not bundled with Ruby 3.0+\n",[995,7011,7012],{"class":997,"line":1705},[995,7013,1541],{"emptyLinePlaceholder":752},[995,7015,7016],{"class":997,"line":1711},[995,7017,7018],{},"group :development do\n",[995,7020,7021],{"class":997,"line":1717},[995,7022,7023],{},"  gem \"jekyll-remote-theme\", \"~> 0.4\"\n",[995,7025,7026],{"class":997,"line":1726},[995,7027,7028],{},"end\n",[14,7030,7031,7032,270,7035,7038],{},"Validate plugin compatibility against your Ruby version (3.3+ is a safe modern target) before upgrading the runtime. Jekyll plugins fall into three categories — generators (which create new pages, like ",[253,7033,7034],{},"jekyll-feed",[253,7036,7037],{},"jekyll-sitemap","), converters (which turn one format into another, like Kramdown for Markdown), and tags\u002Ffilters (which extend Liquid). Knowing the category tells you when a plugin runs and therefore where it can slow the build.",[34,7040,7042],{"id":7041},"cicd-pipeline-integration","CI\u002FCD Pipeline Integration",[14,7044,2360,7045,7048,7049,7052,7053,7056],{},[253,7046,7047],{},"ruby\u002Fsetup-ruby"," with ",[253,7050,7051],{},"bundler-cache: true"," — it installs gems and caches ",[253,7054,7055],{},"vendor\u002Fbundle"," keyed on your lockfile, which removes the most common source of slow, flaky Jekyll CI:",[987,7058,7060],{"className":1912,"code":7059,"language":1914,"meta":712,"style":712},"name: Jekyll CI\non: [push]\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions\u002Fcheckout@v4\n      - uses: ruby\u002Fsetup-ruby@v1\n        with:\n          ruby-version: '3.3'\n          bundler-cache: true\n      - run: JEKYLL_ENV=production bundle exec jekyll build\n",[253,7061,7062,7071,7081,7087,7093,7101,7107,7117,7128,7134,7144,7153],{"__ignoreMap":712},[995,7063,7064,7066,7068],{"class":997,"line":998},[995,7065,1922],{"class":1921},[995,7067,1925],{"class":1618},[995,7069,7070],{"class":1023},"Jekyll CI\n",[995,7072,7073,7075,7077,7079],{"class":997,"line":713},[995,7074,1933],{"class":1010},[995,7076,4044],{"class":1618},[995,7078,4047],{"class":1023},[995,7080,4050],{"class":1618},[995,7082,7083,7085],{"class":997,"line":730},[995,7084,1943],{"class":1921},[995,7086,1946],{"class":1618},[995,7088,7089,7091],{"class":997,"line":1544},[995,7090,1951],{"class":1921},[995,7092,1946],{"class":1618},[995,7094,7095,7097,7099],{"class":997,"line":1550},[995,7096,1958],{"class":1921},[995,7098,1925],{"class":1618},[995,7100,1963],{"class":1023},[995,7102,7103,7105],{"class":997,"line":1673},[995,7104,1968],{"class":1921},[995,7106,1946],{"class":1618},[995,7108,7109,7111,7113,7115],{"class":997,"line":1678},[995,7110,1975],{"class":1618},[995,7112,1978],{"class":1921},[995,7114,1925],{"class":1618},[995,7116,1983],{"class":1023},[995,7118,7119,7121,7123,7125],{"class":997,"line":1693},[995,7120,1975],{"class":1618},[995,7122,1978],{"class":1921},[995,7124,1925],{"class":1618},[995,7126,7127],{"class":1023},"ruby\u002Fsetup-ruby@v1\n",[995,7129,7130,7132],{"class":997,"line":1705},[995,7131,1999],{"class":1921},[995,7133,1946],{"class":1618},[995,7135,7136,7139,7141],{"class":997,"line":1711},[995,7137,7138],{"class":1921},"          ruby-version",[995,7140,1925],{"class":1618},[995,7142,7143],{"class":1023},"'3.3'\n",[995,7145,7146,7149,7151],{"class":997,"line":1717},[995,7147,7148],{"class":1921},"          bundler-cache",[995,7150,1925],{"class":1618},[995,7152,6408],{"class":1010},[995,7154,7155,7157,7159,7161],{"class":997,"line":1726},[995,7156,1975],{"class":1618},[995,7158,2028],{"class":1921},[995,7160,1925],{"class":1618},[995,7162,7163],{"class":1023},"JEKYLL_ENV=production bundle exec jekyll build\n",[14,7165,7166,7167,7170,7171,7173],{},"On a 4,000-post documentation blog, switching from a cold ",[253,7168,7169],{},"bundle install"," every run to ",[253,7172,7051],{}," cut the install phase from ~48s to ~6s:",[433,7175,7176,7189],{},[436,7177,7178],{},[439,7179,7180,7183,7186],{},[442,7181,7182],{},"CI step",[442,7184,7185],{},"Without cache",[442,7187,7188],{},"With bundler-cache",[457,7190,7191,7202,7212],{},[439,7192,7193,7196,7199],{},[462,7194,7195],{},"Gem install",[462,7197,7198],{},"48s",[462,7200,7201],{},"6s",[439,7203,7204,7207,7210],{},[462,7205,7206],{},"Jekyll build",[462,7208,7209],{},"71s",[462,7211,7209],{},[439,7213,7214,7217,7220],{},[462,7215,7216],{},"Total job",[462,7218,7219],{},"119s",[462,7221,7222],{},"77s",[14,7224,7225,7226,7229,7230,239],{},"Add ",[253,7227,7228],{},"bundle exec jekyll doctor"," as a pre-deploy step to catch configuration problems and known issues before they ship. Teams comparing pipelines across frameworks can benchmark against ",[23,7231,774],{"href":773},[34,7233,7235],{"id":7234},"build-optimization-caching","Build Optimization & Caching",[14,7237,7238,7239,7242,7243,7245,7246,7249,7250,7253],{},"Two levers help most. First, ",[229,7240,7241],{},"incremental builds"," — ",[253,7244,981],{}," regenerates only changed pages — but treat it as a development-time accelerator: Jekyll's incremental regeneration is known to miss cross-page dependencies (a changed include that affects many pages), so use full builds for production deploys. Second, ",[229,7247,7248],{},"keep heavy logic out of Liquid",": custom filters that run on every page in a large collection dominate build time. Pre-compute data in a Ruby plugin or a Jekyll hook (",[253,7251,7252],{},":site, :post_read",") so the work happens once per build rather than once per page.",[14,7255,7256],{},"A worked example — moving a per-page tag-count filter into a single hook on the 4,000-post blog took the Liquid render phase from ~71s to ~52s:",[987,7258,7260],{"className":6966,"code":7259,"language":6968,"meta":712,"style":712},"# _plugins\u002Fprecompute_tag_counts.rb\nJekyll::Hooks.register :site, :post_read do |site|\n  counts = Hash.new(0)\n  site.posts.docs.each { |doc| Array(doc.data[\"tags\"]).each { |t| counts[t] += 1 } }\n  site.data[\"tag_counts\"] = counts   # now O(1) lookups in templates\nend\n",[253,7261,7262,7267,7272,7277,7282,7287],{"__ignoreMap":712},[995,7263,7264],{"class":997,"line":998},[995,7265,7266],{},"# _plugins\u002Fprecompute_tag_counts.rb\n",[995,7268,7269],{"class":997,"line":713},[995,7270,7271],{},"Jekyll::Hooks.register :site, :post_read do |site|\n",[995,7273,7274],{"class":997,"line":730},[995,7275,7276],{},"  counts = Hash.new(0)\n",[995,7278,7279],{"class":997,"line":1544},[995,7280,7281],{},"  site.posts.docs.each { |doc| Array(doc.data[\"tags\"]).each { |t| counts[t] += 1 } }\n",[995,7283,7284],{"class":997,"line":1550},[995,7285,7286],{},"  site.data[\"tag_counts\"] = counts   # now O(1) lookups in templates\n",[995,7288,7289],{"class":997,"line":1673},[995,7290,7028],{},[14,7292,7293,7294,270,7297,7300,7301,7304],{},"If you cache anything in CI, cache Jekyll's incremental metadata, not the output — ",[253,7295,7296],{},".jekyll-cache\u002F",[253,7298,7299],{},".jekyll-metadata"," are what speed up regeneration; ",[253,7302,7303],{},"_site\u002F"," is the disposable result:",[987,7306,7308],{"className":989,"code":7307,"language":991,"meta":712,"style":712},"JEKYLL_ENV=production bundle exec jekyll build --profile\n",[253,7309,7310],{"__ignoreMap":712},[995,7311,7312,7315,7318,7321,7324,7327,7330,7333],{"class":997,"line":998},[995,7313,7314],{"class":1618},"JEKYLL_ENV",[995,7316,7317],{"class":1614},"=",[995,7319,7320],{"class":1023},"production",[995,7322,7323],{"class":1007}," bundle",[995,7325,7326],{"class":1023}," exec",[995,7328,7329],{"class":1023}," jekyll",[995,7331,7332],{"class":1023}," build",[995,7334,7335],{"class":1010}," --profile\n",[14,7337,7338,7341,7342,7344],{},[253,7339,7340],{},"--profile"," prints per-template render time so you can see which layout or include is the bottleneck. For Markdown-heavy workloads specifically, compare against ",[23,7343,1161],{"href":1160}," before committing to a scaling strategy.",[34,7346,7348],{"id":7347},"replacing-or-migrating-plugins","Replacing or Migrating Plugins",[14,7350,7351,7352,7355,7356,7359],{},"The most painful Jekyll dependencies are abandoned gems with native extensions: when a Ruby version bump breaks one, your build stays broken until someone patches it. Two defensive moves help. First, prefer plugins from the maintained ",[253,7353,7354],{},"jekyll\u002F"," org or the actively maintained community set over one-off gems. Second, when a plugin's behavior is simple, reimplement it as a small in-repo ",[253,7357,7358],{},"_plugins\u002F"," file you control rather than carrying an external dependency.",[14,7361,7362,7363,7365,7366,7368],{},"If the dependency story is the reason you are leaving Jekyll, the mapping from Jekyll plugins to their nearest equivalents — and which ones simply become built-in behavior — is laid out in ",[23,7364,6623],{"href":6622},". The same \"native over plugin\" lesson applies when porting logic to faster engines like in ",[23,7367,2478],{"href":2477},", where Hugo's shortcodes and image methods replace whole categories of Jekyll gems.",[34,7370,7372],{"id":7371},"collections-and-pagination-at-scale","Collections and Pagination at Scale",[14,7374,7375,7376,1850,7379,7382,7383,3725,7386,7388,7389,7392],{},"Large Jekyll sites usually organise content into collections (",[253,7377,7378],{},"_posts",[253,7380,7381],{},"_docs",", custom collections defined under ",[253,7384,7385],{},"collections:",[253,7387,6425],{},"). Two settings on a collection govern how much work the build does. ",[253,7390,7391],{},"output: true"," makes Jekyll render a page per document — only set it on collections you actually publish, because rendering documents you never link wastes build time. And front matter defaults scoped to a collection let you stop repeating layout and permalink keys in every file:",[987,7394,7396],{"className":1912,"code":7395,"language":1914,"meta":712,"style":712},"# _config.yml\ncollections:\n  docs:\n    output: true\n    permalink: \u002Fdocs\u002F:path\u002F\n\ndefaults:\n  - scope: { path: \"\", type: \"docs\" }\n    values: { layout: \"doc\", toc: true }\n",[253,7397,7398,7402,7409,7416,7425,7435,7439,7446,7476],{"__ignoreMap":712},[995,7399,7400],{"class":997,"line":998},[995,7401,6357],{"class":1001},[995,7403,7404,7407],{"class":997,"line":713},[995,7405,7406],{"class":1921},"collections",[995,7408,1946],{"class":1618},[995,7410,7411,7414],{"class":997,"line":730},[995,7412,7413],{"class":1921},"  docs",[995,7415,1946],{"class":1618},[995,7417,7418,7421,7423],{"class":997,"line":1544},[995,7419,7420],{"class":1921},"    output",[995,7422,1925],{"class":1618},[995,7424,6408],{"class":1010},[995,7426,7427,7430,7432],{"class":997,"line":1550},[995,7428,7429],{"class":1921},"    permalink",[995,7431,1925],{"class":1618},[995,7433,7434],{"class":1023},"\u002Fdocs\u002F:path\u002F\n",[995,7436,7437],{"class":997,"line":1673},[995,7438,1541],{"emptyLinePlaceholder":752},[995,7440,7441,7444],{"class":997,"line":1678},[995,7442,7443],{"class":1921},"defaults",[995,7445,1946],{"class":1618},[995,7447,7448,7451,7454,7457,7459,7461,7464,7466,7469,7471,7473],{"class":997,"line":1693},[995,7449,7450],{"class":1618},"  - ",[995,7452,7453],{"class":1921},"scope",[995,7455,7456],{"class":1618},": { ",[995,7458,90],{"class":1921},[995,7460,1925],{"class":1618},[995,7462,7463],{"class":1023},"\"\"",[995,7465,1850],{"class":1618},[995,7467,7468],{"class":1921},"type",[995,7470,1925],{"class":1618},[995,7472,1802],{"class":1023},[995,7474,7475],{"class":1618}," }\n",[995,7477,7478,7481,7483,7486,7488,7491,7493,7496,7498,7500],{"class":997,"line":1705},[995,7479,7480],{"class":1921},"    values",[995,7482,7456],{"class":1618},[995,7484,7485],{"class":1921},"layout",[995,7487,1925],{"class":1618},[995,7489,7490],{"class":1023},"\"doc\"",[995,7492,1850],{"class":1618},[995,7494,7495],{"class":1921},"toc",[995,7497,1925],{"class":1618},[995,7499,6283],{"class":1010},[995,7501,7475],{"class":1618},[14,7503,7504,7505,7507,7508,7511,7512,7515,7516,239],{},"Pagination is the other scaling concern. The built-in paginator only walks ",[253,7506,7378],{},"; for paginating an arbitrary collection use ",[253,7509,7510],{},"jekyll-paginate-v2",", and cap ",[253,7513,7514],{},"per_page"," so a large archive does not generate thousands of nearly empty index pages. Each generated page is a full Liquid render, so an over-eager paginator can quietly double a build's page count. Where this kind of structural work becomes the bottleneck, it is worth weighing the engine itself against the alternatives in ",[23,7517,774],{"href":773},[34,7519,7521],{"id":7520},"measuring-build-performance","Measuring Build Performance",[14,7523,7524,7525,7527,7528,7530],{},"Track total build duration and the ",[253,7526,7340],{}," table across commits, and alert when build time jumps more than ~10–15%. A regression almost always shows up as one layout or include suddenly dominating the profile — usually a new custom filter that runs per page, or an include pulled into a high-traffic layout. Keep a recorded baseline so you can tell a real regression from runner noise; the controlled-benchmark approach in ",[23,7529,288],{"href":287}," transfers directly.",[34,7532,2266],{"id":2265},[39,7534,7535,7548,7563,7572,7578],{},[42,7536,7537,7540,7541,7543,7544,7547],{},[229,7538,7539],{},"Unpinned gems:"," non-deterministic dependency resolution breaks CI unpredictably. Commit ",[253,7542,6948],{}," and use ",[253,7545,7546],{},"~>"," constraints.",[42,7549,7550,7553,7554,2204,7557,7559,7560,7562],{},[229,7551,7552],{},"Wrong Bundler group:"," putting ",[253,7555,7556],{},"jekyll-seo-tag",[253,7558,7034],{}," under ",[253,7561,6956],{}," means CI skips them and ships pages missing meta tags or a feed. Build-time plugins go in the default group.",[42,7564,7565,7571],{},[229,7566,7567,7568,7570],{},"Trusting ",[253,7569,981],{}," in production:"," it can serve stale pages when a shared include changes. Use it locally; do full builds for deploys.",[42,7573,7574,7577],{},[229,7575,7576],{},"Heavy Liquid filters per page:"," a custom filter run on every item in a large collection dominates build time. Pre-compute in a hook so the work happens once.",[42,7579,7580,7586,7587,270,7589,239],{},[229,7581,7582,7583,7585],{},"Caching ",[253,7584,2245],{}," instead of metadata:"," the output directory is not what makes rebuilds fast. Cache ",[253,7588,7055],{},[253,7590,7296],{},[34,7592,2321],{"id":2320},[39,7594,7595,7601,7606,7612,7618],{},[42,7596,7597,7598,7600],{},"Pin gems, commit ",[253,7599,6948],{},", and keep build-time plugins in the default Bundler group.",[42,7602,2360,7603,7605],{},[253,7604,7051],{}," in CI — it removes the most common source of slow, flaky Jekyll jobs.",[42,7607,7608,7609,7611],{},"Reserve ",[253,7610,981],{}," for local authoring; ship full builds to production.",[42,7613,7614,7615,7617],{},"Move per-page logic into a ",[253,7616,7252],{}," hook so it runs once, not once per page.",[42,7619,7620,7621,7624],{},"Profile with ",[253,7622,7623],{},"jekyll build --profile"," and alert on build-time regressions in CI.",[34,7626,651],{"id":650},[653,7628,7630],{"id":7629},"how-do-i-safely-upgrade-jekyll-plugins-without-breaking-ci","How do I safely upgrade Jekyll plugins without breaking CI?",[14,7632,7633,7634,7637,7638,7640,7641,7643],{},"Run ",[253,7635,7636],{},"bundle update --conservative"," on a branch, run ",[253,7639,7228],{},", and diff the generated ",[253,7642,2245],{}," output before merging. Conservative updates change only the gem you name and its direct requirements, so the blast radius stays small and reviewable.",[653,7645,7647],{"id":7646},"can-i-use-jekyll-plugins-with-github-pages","Can I use Jekyll plugins with GitHub Pages?",[14,7649,7650,7651,7653,7654,7656],{},"GitHub Pages only allows a whitelisted plugin set. To use any plugin, build in CI with your own ",[253,7652,6944],{}," and deploy the compiled ",[253,7655,2245],{}," to your pages branch instead of relying on GitHub's built-in build.",[653,7658,7660],{"id":7659},"what-is-the-optimal-caching-strategy-for-jekyll-in-ci","What is the optimal caching strategy for Jekyll in CI?",[14,7662,7663,7664,7666,7667,7669,7670,7672,7673,7675],{},"Cache ",[253,7665,7055],{}," via ",[253,7668,7051],{}," so gems are not reinstalled every run, and cache ",[253,7671,7296],{}," if you use incremental builds. Do not cache ",[253,7674,2245],{},", because the output directory is the disposable result, not what makes rebuilds fast.",[653,7677,7679],{"id":7678},"how-do-i-measure-jekyll-build-performance","How do I measure Jekyll build performance?",[14,7681,7633,7682,7684],{},[253,7683,7623],{}," to print per-template render time, and track that table plus total build duration across commits. A layout or include that suddenly dominates the profile is usually the regression.",[653,7686,7688],{"id":7687},"which-plugins-belong-in-the-default-bundler-group-versus-development","Which plugins belong in the default Bundler group versus development?",[14,7690,7691,7692,1850,7694,3706,7696,7698,7699,7701,7702,7704],{},"Build-time plugins like ",[253,7693,7556],{},[253,7695,7034],{},[253,7697,7037],{}," run during the production build, so they belong in the default group. Only genuinely local-only tooling, such as ",[253,7700,6960],{}," used for previewing themes, belongs under the ",[253,7703,6956],{}," group.",[653,7706,7708],{"id":7707},"is-jekyll-incremental-safe-for-production-deploys","Is jekyll --incremental safe for production deploys?",[14,7710,7711],{},"No. Jekyll's incremental regeneration is known to miss cross-page dependencies, such as a changed include that affects many pages, so it can serve stale output. Use it as a local development accelerator and run full builds for production deploys.",[34,7713,684],{"id":683},[39,7715,7716,7723,7728,7733],{},[42,7717,7718,692,7720,7722],{},[229,7719,691],{},[23,7721,31],{"href":30}," — where Jekyll fits the framework decision.",[42,7724,7725,7727],{},[23,7726,1161],{"href":1160}," — the head-to-head for large Markdown corpora.",[42,7729,7730,7732],{},[23,7731,6623],{"href":6622}," — the plugin-to-equivalent mapping.",[42,7734,7735,7737],{},[23,7736,2478],{"href":2477}," — native shortcodes over plugins at scale.",[1346,7739,7740],{},"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}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html 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 .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}",{"title":712,"searchDepth":713,"depth":713,"links":7742},[7743,7744,7745,7746,7747,7748,7749,7750,7751,7759],{"id":6937,"depth":713,"text":6938},{"id":7041,"depth":713,"text":7042},{"id":7234,"depth":713,"text":7235},{"id":7347,"depth":713,"text":7348},{"id":7371,"depth":713,"text":7372},{"id":7520,"depth":713,"text":7521},{"id":2265,"depth":713,"text":2266},{"id":2320,"depth":713,"text":2321},{"id":650,"depth":713,"text":651,"children":7752},[7753,7754,7755,7756,7757,7758],{"id":7629,"depth":730,"text":7630},{"id":7646,"depth":730,"text":7647},{"id":7659,"depth":730,"text":7660},{"id":7678,"depth":730,"text":7679},{"id":7687,"depth":730,"text":7688},{"id":7707,"depth":730,"text":7708},{"id":683,"depth":713,"text":684},[7761,7762,7763],{"name":737,"item":738},{"name":31,"item":30},{"name":2225,"item":2224},"A production Jekyll setup: pin your gems, group plugins correctly, run reproducible CI builds, and apply the build-optimization levers that actually move the needle.",[7766,7768,7770,7772,7774,7776],{"q":7630,"a":7767},"Run bundle update --conservative on a branch, run bundle exec jekyll doctor, and diff the generated _site output before merging. Conservative updates change only the gem you name and its direct requirements, so the blast radius stays small and reviewable.",{"q":7647,"a":7769},"GitHub Pages only allows a whitelisted plugin set. To use any plugin, build in CI with your own Gemfile and deploy the compiled _site to your pages branch instead of relying on GitHub's built-in build.",{"q":7660,"a":7771},"Cache vendor\u002Fbundle via bundler-cache true so gems are not reinstalled every run, and cache .jekyll-cache if you use incremental builds. Do not cache _site, because the output directory is the disposable result, not what makes rebuilds fast.",{"q":7679,"a":7773},"Run jekyll build --profile to print per-template render time, and track that table plus total build duration across commits. A layout or include that suddenly dominates the profile is usually the regression.",{"q":7688,"a":7775},"Build-time plugins like jekyll-seo-tag, jekyll-feed, and jekyll-sitemap run during the production build, so they belong in the default group. Only genuinely local-only tooling, such as jekyll-remote-theme used for previewing themes, belongs under the development group.",{"q":7708,"a":7711},{},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fjekyll-plugin-ecosystem",{"title":2225,"description":7764},"choosing-the-right-static-site-generator-for-production\u002Fjekyll-plugin-ecosystem\u002Findex","nYZMGpAsSMhFelST1s0CXrfJJLygY0ejVKGDD0tTtNs",{"id":7783,"title":6623,"body":7784,"breadcrumb":8633,"dateModified":743,"datePublished":743,"description":8638,"extension":745,"faq":8639,"meta":8645,"navigation":752,"path":8646,"seo":8647,"slug":7788,"stem":8648,"type":756,"__hash__":8649},"content\u002Fchoosing-the-right-static-site-generator-for-production\u002Fjekyll-plugin-ecosystem\u002Freplacing-jekyll-plugins-when-migrating-to-eleventy\u002Findex.md",{"type":7,"value":7785,"toc":8614},[7786,7789,7796,7798,7818,7910,7914,7925,7929,7940,7975,7988,7992,7997,8013,8072,8086,8090,8097,8151,8161,8165,8177,8193,8376,8386,8388,8393,8485,8494,8496,8538,8540,8548,8550,8554,8557,8561,8564,8568,8571,8575,8578,8582,8585,8587,8611],[10,7787,6623],{"id":7788},"replacing-jekyll-plugins-when-migrating-to-eleventy",[14,7790,7791,7792,4582,7794,239],{},"Most Jekyll migrations stall on the same question: what happens to my plugins? The Gemfile is where a Jekyll site's behavior lives — SEO tags, the RSS feed, pagination, responsive images — and Eleventy organizes those concerns differently. The good news is that the four plugins almost every Jekyll site depends on all have clean Eleventy equivalents, some built in, some a single official plugin, and a couple just a short template. This guide maps each one with working config and the measured build impact from a 600-page migration. It sits under ",[23,7793,2225],{"href":2224},[23,7795,31],{"href":30},[34,7797,37],{"id":36},[39,7799,7800,7808,7815],{},[42,7801,7802,7803,270,7805,7807],{},"An existing Jekyll site whose ",[253,7804,6944],{},[253,7806,6425],{}," you can inspect to inventory which plugins you actually use.",[42,7809,7810,7811,7814],{},"Node 20+ and a fresh Eleventy install (",[253,7812,7813],{},"npm install @11ty\u002Feleventy --save-dev",") in the target project.",[42,7816,7817],{},"The Nunjucks or Liquid templating language chosen for Eleventy — Eleventy supports Liquid, so much of your existing Jekyll Liquid markup can be reused with minor changes.",[55,7819,7820,7907],{},[58,7821,66,7826,66,7829,66,7832,66,7900],{"viewBox":7822,"role":61,"ariaLabelledBy":7823,"xmlns":65},"0 0 760 330",[7824,7825],"jekmap-title","jekmap-desc",[68,7827,7828],{"id":7824},"Jekyll plugins mapped to their Eleventy equivalents",[72,7830,7831],{"id":7825},"Four Jekyll plugins on the left connected by arrows to their Eleventy replacements on the right: jekyll-seo-tag to a head partial, jekyll-feed to the official RSS plugin, jekyll-paginate to built-in pagination, and jekyll-picture-tag to the eleventy-img plugin.",[95,7833,78,7834,78,7837,78,7840,78,7843,78,7846,78,7848,78,7850,78,7853,78,7856,78,7859,78,7862,78,7865,78,7867,78,7870,78,7872,78,7875,78,7877,78,7880,78,7882,78,7885,66],{"style":97},[99,7835,7836],{"x":816,"y":102,"fill":103,"style":104},"Jekyll plugin to Eleventy equivalent",[99,7838,7839],{"x":194,"y":822,"fill":93,"style":882},"Jekyll (Gemfile)",[99,7841,273],{"x":7842,"y":822,"fill":93,"style":882},"590",[107,7844],{"x":110,"y":4629,"width":111,"height":5410,"rx":7845,"fill":2564,"opacity":825,"stroke":2565,"style":116},"9",[99,7847,7556],{"x":194,"y":4682,"fill":103,"style":829},[107,7849],{"x":110,"y":130,"width":111,"height":5410,"rx":7845,"fill":2564,"opacity":825,"stroke":2565,"style":116},[99,7851,7034],{"x":194,"y":7852,"fill":103,"style":829},"160",[107,7854],{"x":110,"y":7855,"width":111,"height":5410,"rx":7845,"fill":2564,"opacity":825,"stroke":2565,"style":116},"194",[99,7857,7858],{"x":194,"y":111,"fill":103,"style":829},"jekyll-paginate",[107,7860],{"x":110,"y":7861,"width":111,"height":5410,"rx":7845,"fill":2564,"opacity":825,"stroke":2565,"style":116},"254",[99,7863,7864],{"x":194,"y":820,"fill":103,"style":829},"jekyll-picture-tag",[107,7866],{"x":885,"y":4629,"width":111,"height":5410,"rx":7845,"fill":185,"opacity":850,"stroke":187,"style":116},[99,7868,7869],{"x":7842,"y":4682,"fill":103,"style":829},"head partial (template)",[107,7871],{"x":885,"y":130,"width":111,"height":5410,"rx":7845,"fill":185,"opacity":850,"stroke":187,"style":116},[99,7873,7874],{"x":7842,"y":7852,"fill":103,"style":829},"@11ty\u002Feleventy-plugin-rss",[107,7876],{"x":885,"y":7855,"width":111,"height":5410,"rx":7845,"fill":824,"opacity":186,"stroke":824,"style":116},[99,7878,7879],{"x":7842,"y":111,"fill":103,"style":829},"built-in pagination",[107,7881],{"x":885,"y":7861,"width":111,"height":5410,"rx":7845,"fill":185,"opacity":850,"stroke":187,"style":116},[99,7883,7884],{"x":7842,"y":820,"fill":103,"style":829},"@11ty\u002Feleventy-img",[95,7886,88,7887,88,7891,88,7894,88,7897,78],{"stroke":93,"fill":205,"style":116},[90,7888],{"d":7889,"style":7890},"M280 95 L478 95","marker-end:url(#jekmap-arrow)",[90,7892],{"d":7893,"style":7890},"M280 155 L478 155",[90,7895],{"d":7896,"style":7890},"M280 215 L478 215",[90,7898],{"d":7899,"style":7890},"M280 275 L478 275",[76,7901,78,7902,66],{},[80,7903,88,7905,78],{"id":7904,"viewBox":83,"refX":84,"refY":85,"markerWidth":86,"markerHeight":86,"orient":87},"jekmap-arrow",[90,7906],{"d":92,"fill":93},[218,7908,7909],{},"Two of the four map to official Eleventy plugins, one to a built-in feature, and one to a small head template — no plugin gaps for the common stack.",[34,7911,7913],{"id":7912},"inventory-what-you-actually-use","Inventory What You Actually Use",[14,7915,7916,7917,270,7919,7921,7922,239],{},"Before writing any Eleventy config, list the plugins your Jekyll site loads from ",[253,7918,6425],{},[253,7920,6944],{},". Most sites depend on a handful; the rest is theme machinery you can drop. The four below cover the overwhelming majority of production Jekyll sites, so map those first and treat anything else as a case-by-case decision — often a ten-line JavaScript function in ",[253,7923,7924],{},".eleventy.js",[34,7926,7928],{"id":7927},"jekyll-seo-tag-a-head-partial","jekyll-seo-tag → A Head Partial",[14,7930,7931,7933,7934,7936,7937,931],{},[253,7932,7556],{}," injects title, description, canonical, and Open Graph tags from your front matter and ",[253,7935,6425],{},". Eleventy has no single drop-in, but the same markup is a short template partial that reads page and site data. Create ",[253,7938,7939],{},"_includes\u002Fhead-seo.njk",[987,7941,7943],{"className":1116,"code":7942,"language":1118,"meta":712,"style":712},"\u003Ctitle>{% raw %}{{ title or site.title }}{% endraw %}\u003C\u002Ftitle>\n\u003Cmeta name=\"description\" content=\"{% raw %}{{ description or site.description }}{% endraw %}\">\n\u003Clink rel=\"canonical\" href=\"{% raw %}{{ site.url }}{{ page.url }}{% endraw %}\">\n\u003Cmeta property=\"og:title\" content=\"{% raw %}{{ title or site.title }}{% endraw %}\">\n\u003Cmeta property=\"og:type\" content=\"article\">\n\u003Cmeta property=\"og:url\" content=\"{% raw %}{{ site.url }}{{ page.url }}{% endraw %}\">\n",[253,7944,7945,7950,7955,7960,7965,7970],{"__ignoreMap":712},[995,7946,7947],{"class":997,"line":998},[995,7948,7949],{},"\u003Ctitle>{% raw %}{{ title or site.title }}{% endraw %}\u003C\u002Ftitle>\n",[995,7951,7952],{"class":997,"line":713},[995,7953,7954],{},"\u003Cmeta name=\"description\" content=\"{% raw %}{{ description or site.description }}{% endraw %}\">\n",[995,7956,7957],{"class":997,"line":730},[995,7958,7959],{},"\u003Clink rel=\"canonical\" href=\"{% raw %}{{ site.url }}{{ page.url }}{% endraw %}\">\n",[995,7961,7962],{"class":997,"line":1544},[995,7963,7964],{},"\u003Cmeta property=\"og:title\" content=\"{% raw %}{{ title or site.title }}{% endraw %}\">\n",[995,7966,7967],{"class":997,"line":1550},[995,7968,7969],{},"\u003Cmeta property=\"og:type\" content=\"article\">\n",[995,7971,7972],{"class":997,"line":1673},[995,7973,7974],{},"\u003Cmeta property=\"og:url\" content=\"{% raw %}{{ site.url }}{{ page.url }}{% endraw %}\">\n",[14,7976,7977,7978,3725,7981,7984,7985,7987],{},"Put shared values like ",[253,7979,7980],{},"site.url",[253,7982,7983],{},"_data\u002Fsite.json",". The result is the same head markup ",[253,7986,7556],{}," would emit, but you own every line, which makes per-template overrides trivial.",[34,7989,7991],{"id":7990},"jekyll-feed-eleventy-plugin-rss","jekyll-feed → eleventy-plugin-rss",[14,7993,7994,7996],{},[253,7995,7034],{}," generates an Atom feed. Eleventy's official RSS plugin provides the date and absolute-URL filters; you write the feed template once:",[987,7998,8000],{"className":989,"code":7999,"language":991,"meta":712,"style":712},"npm install @11ty\u002Feleventy-plugin-rss --save-dev\n",[253,8001,8002],{"__ignoreMap":712},[995,8003,8004,8006,8008,8011],{"class":997,"line":998},[995,8005,1527],{"class":1007},[995,8007,1555],{"class":1023},[995,8009,8010],{"class":1023}," @11ty\u002Feleventy-plugin-rss",[995,8012,1561],{"class":1010},[987,8014,8016],{"className":1600,"code":8015,"language":1602,"meta":712,"style":712},"\u002F\u002F .eleventy.js\nconst pluginRss = require(\"@11ty\u002Feleventy-plugin-rss\");\nmodule.exports = function (eleventyConfig) {\n  eleventyConfig.addPlugin(pluginRss);\n};\n",[253,8017,8018,8022,8040,8058,8068],{"__ignoreMap":712},[995,8019,8020],{"class":997,"line":998},[995,8021,1762],{"class":1001},[995,8023,8024,8026,8029,8031,8033,8035,8038],{"class":997,"line":713},[995,8025,6228],{"class":1614},[995,8027,8028],{"class":1010}," pluginRss",[995,8030,1775],{"class":1614},[995,8032,6236],{"class":1007},[995,8034,1799],{"class":1618},[995,8036,8037],{"class":1023},"\"@11ty\u002Feleventy-plugin-rss\"",[995,8039,5829],{"class":1618},[995,8041,8042,8044,8046,8048,8050,8052,8054,8056],{"class":997,"line":730},[995,8043,1767],{"class":1010},[995,8045,239],{"class":1618},[995,8047,1772],{"class":1010},[995,8049,1775],{"class":1614},[995,8051,1778],{"class":1614},[995,8053,1781],{"class":1618},[995,8055,1785],{"class":1784},[995,8057,1788],{"class":1618},[995,8059,8060,8062,8065],{"class":997,"line":1544},[995,8061,1793],{"class":1618},[995,8063,8064],{"class":1007},"addPlugin",[995,8066,8067],{"class":1618},"(pluginRss);\n",[995,8069,8070],{"class":997,"line":1550},[995,8071,1877],{"class":1618},[14,8073,8074,8075,8078,8079,270,8082,8085],{},"Then a ",[253,8076,8077],{},"feed.njk"," template iterates your posts collection and uses the plugin's ",[253,8080,8081],{},"dateToRfc3339",[253,8083,8084],{},"htmlToAbsoluteUrls"," filters to emit valid feed entries. This is more explicit than Jekyll's automatic feed, but it lets you control exactly which collection and how many entries ship.",[34,8087,8089],{"id":8088},"jekyll-paginate-built-in-pagination","jekyll-paginate → Built-in Pagination",[14,8091,8092,8093,8096],{},"This is the easiest one: Eleventy has pagination built in, so there is nothing to install. Add a ",[253,8094,8095],{},"pagination"," object to a template's front matter:",[987,8098,8100],{"className":1116,"code":8099,"language":1118,"meta":712,"style":712},"---\npagination:\n  data: collections.post\n  size: 10\n  alias: posts\npermalink: \"\u002Fblog\u002F{% raw %}{% if pagination.pageNumber %}page\u002F{{ pagination.pageNumber + 1 }}\u002F{% endif %}{% endraw %}\"\n---\n{% raw %}{% for post in posts %}\n  \u003Ca href=\"{{ post.url }}\">{{ post.data.title }}\u003C\u002Fa>\n{% endfor %}{% endraw %}\n",[253,8101,8102,8107,8112,8117,8122,8127,8132,8136,8141,8146],{"__ignoreMap":712},[995,8103,8104],{"class":997,"line":998},[995,8105,8106],{},"---\n",[995,8108,8109],{"class":997,"line":713},[995,8110,8111],{},"pagination:\n",[995,8113,8114],{"class":997,"line":730},[995,8115,8116],{},"  data: collections.post\n",[995,8118,8119],{"class":997,"line":1544},[995,8120,8121],{},"  size: 10\n",[995,8123,8124],{"class":997,"line":1550},[995,8125,8126],{},"  alias: posts\n",[995,8128,8129],{"class":997,"line":1673},[995,8130,8131],{},"permalink: \"\u002Fblog\u002F{% raw %}{% if pagination.pageNumber %}page\u002F{{ pagination.pageNumber + 1 }}\u002F{% endif %}{% endraw %}\"\n",[995,8133,8134],{"class":997,"line":1678},[995,8135,8106],{},[995,8137,8138],{"class":997,"line":1693},[995,8139,8140],{},"{% raw %}{% for post in posts %}\n",[995,8142,8143],{"class":997,"line":1705},[995,8144,8145],{},"  \u003Ca href=\"{{ post.url }}\">{{ post.data.title }}\u003C\u002Fa>\n",[995,8147,8148],{"class":997,"line":1711},[995,8149,8150],{},"{% endfor %}{% endraw %}\n",[14,8152,8153,8154,8157,8158,8160],{},"Eleventy slices the data into pages of ",[253,8155,8156],{},"size"," and generates one output file per page with the permalink pattern you specify. It also paginates over objects, not just arrays, which ",[253,8159,7858],{}," could not do without the v2 add-on.",[34,8162,8164],{"id":8163},"jekyll-picture-tag-eleventy-img","jekyll-picture-tag → eleventy-img",[14,8166,8167,8169,8170,8173,8174,8176],{},[253,8168,7864],{}," produces responsive ",[253,8171,8172],{},"\u003Cpicture>"," markup with resized, modern-format images. The official ",[253,8175,7884],{}," plugin does the same at build time:",[987,8178,8180],{"className":989,"code":8179,"language":991,"meta":712,"style":712},"npm install @11ty\u002Feleventy-img --save-dev\n",[253,8181,8182],{"__ignoreMap":712},[995,8183,8184,8186,8188,8191],{"class":997,"line":998},[995,8185,1527],{"class":1007},[995,8187,1555],{"class":1023},[995,8189,8190],{"class":1023}," @11ty\u002Feleventy-img",[995,8192,1561],{"class":1010},[987,8194,8196],{"className":1600,"code":8195,"language":1602,"meta":712,"style":712},"\u002F\u002F .eleventy.js — a shortcode that resizes and emits \u003Cpicture>\nconst Image = require(\"@11ty\u002Feleventy-img\");\nmodule.exports = function (eleventyConfig) {\n  eleventyConfig.addShortcode(\"image\", async function (src, alt) {\n    const metadata = await Image(src, {\n      widths: [400, 800, 1200],\n      formats: [\"avif\", \"webp\", \"jpeg\"],\n      outputDir: \".\u002F_site\u002Fimg\u002F\",\n    });\n    return Image.generateHTML(metadata, { alt, sizes: \"100vw\", loading: \"lazy\" });\n  });\n};\n",[253,8197,8198,8203,8221,8239,8269,8287,8307,8327,8337,8342,8367,8372],{"__ignoreMap":712},[995,8199,8200],{"class":997,"line":998},[995,8201,8202],{"class":1001},"\u002F\u002F .eleventy.js — a shortcode that resizes and emits \u003Cpicture>\n",[995,8204,8205,8207,8210,8212,8214,8216,8219],{"class":997,"line":713},[995,8206,6228],{"class":1614},[995,8208,8209],{"class":1010}," Image",[995,8211,1775],{"class":1614},[995,8213,6236],{"class":1007},[995,8215,1799],{"class":1618},[995,8217,8218],{"class":1023},"\"@11ty\u002Feleventy-img\"",[995,8220,5829],{"class":1618},[995,8222,8223,8225,8227,8229,8231,8233,8235,8237],{"class":997,"line":730},[995,8224,1767],{"class":1010},[995,8226,239],{"class":1618},[995,8228,1772],{"class":1010},[995,8230,1775],{"class":1614},[995,8232,1778],{"class":1614},[995,8234,1781],{"class":1618},[995,8236,1785],{"class":1784},[995,8238,1788],{"class":1618},[995,8240,8241,8243,8246,8248,8251,8253,8256,8258,8260,8262,8264,8267],{"class":997,"line":1544},[995,8242,1793],{"class":1618},[995,8244,8245],{"class":1007},"addShortcode",[995,8247,1799],{"class":1618},[995,8249,8250],{"class":1023},"\"image\"",[995,8252,1850],{"class":1618},[995,8254,8255],{"class":1614},"async",[995,8257,1778],{"class":1614},[995,8259,1781],{"class":1618},[995,8261,1579],{"class":1784},[995,8263,1850],{"class":1618},[995,8265,8266],{"class":1784},"alt",[995,8268,1788],{"class":1618},[995,8270,8271,8274,8277,8279,8282,8284],{"class":997,"line":1550},[995,8272,8273],{"class":1614},"    const",[995,8275,8276],{"class":1010}," metadata",[995,8278,1775],{"class":1614},[995,8280,8281],{"class":1614}," await",[995,8283,8209],{"class":1007},[995,8285,8286],{"class":1618},"(src, {\n",[995,8288,8289,8292,8294,8296,8299,8301,8304],{"class":997,"line":1673},[995,8290,8291],{"class":1618},"      widths: [",[995,8293,101],{"class":1010},[995,8295,1850],{"class":1618},[995,8297,8298],{"class":1010},"800",[995,8300,1850],{"class":1618},[995,8302,8303],{"class":1010},"1200",[995,8305,8306],{"class":1618},"],\n",[995,8308,8309,8312,8315,8317,8320,8322,8325],{"class":997,"line":1678},[995,8310,8311],{"class":1618},"      formats: [",[995,8313,8314],{"class":1023},"\"avif\"",[995,8316,1850],{"class":1618},[995,8318,8319],{"class":1023},"\"webp\"",[995,8321,1850],{"class":1618},[995,8323,8324],{"class":1023},"\"jpeg\"",[995,8326,8306],{"class":1618},[995,8328,8329,8332,8335],{"class":997,"line":1693},[995,8330,8331],{"class":1618},"      outputDir: ",[995,8333,8334],{"class":1023},"\".\u002F_site\u002Fimg\u002F\"",[995,8336,2885],{"class":1618},[995,8338,8339],{"class":997,"line":1705},[995,8340,8341],{"class":1618},"    });\n",[995,8343,8344,8347,8350,8353,8356,8359,8362,8365],{"class":997,"line":1711},[995,8345,8346],{"class":1614},"    return",[995,8348,8349],{"class":1618}," Image.",[995,8351,8352],{"class":1007},"generateHTML",[995,8354,8355],{"class":1618},"(metadata, { alt, sizes: ",[995,8357,8358],{"class":1023},"\"100vw\"",[995,8360,8361],{"class":1618},", loading: ",[995,8363,8364],{"class":1023},"\"lazy\"",[995,8366,6500],{"class":1618},[995,8368,8369],{"class":997,"line":1717},[995,8370,8371],{"class":1618},"  });\n",[995,8373,8374],{"class":997,"line":1726},[995,8375,1877],{"class":1618},[14,8377,8378,8380,8381,8383,8384,239],{},[253,8379,2125],{}," processes images during the build and caches results, so unchanged images are not re-encoded — the same build-time, zero-runtime-cost model covered in ",[23,8382,2190],{"href":2189},". For an authoring comparison between the two Markdown-first generators, see ",[23,8385,1161],{"href":1160},[34,8387,1166],{"id":1165},[14,8389,8390,8391,931],{},"Migrating a 600-page blog (Markdown posts, responsive images, RSS, paginated index) from Jekyll to Eleventy, timed with ",[253,8392,930],{},[433,8394,8395,8411],{},[436,8396,8397],{},[439,8398,8399,8402,8405,8408],{},[442,8400,8401],{},"Concern",[442,8403,8404],{},"Jekyll plugin",[442,8406,8407],{},"Eleventy replacement",[442,8409,8410],{},"Build delta",[457,8412,8413,8426,8438,8450,8463],{},[439,8414,8415,8418,8420,8423],{},[462,8416,8417],{},"SEO meta",[462,8419,7556],{},[462,8421,8422],{},"head partial",[462,8424,8425],{},"negligible",[439,8427,8428,8431,8433,8436],{},[462,8429,8430],{},"RSS feed",[462,8432,7034],{},[462,8434,8435],{},"eleventy-plugin-rss",[462,8437,8425],{},[439,8439,8440,8443,8445,8448],{},[462,8441,8442],{},"Pagination",[462,8444,7858],{},[462,8446,8447],{},"built-in",[462,8449,8425],{},[439,8451,8452,8455,8457,8460],{},[462,8453,8454],{},"Responsive images",[462,8456,7864],{},[462,8458,8459],{},"eleventy-img (cached)",[462,8461,8462],{},"−19 s on rebuilds",[439,8464,8465,8470,8475,8480],{},[462,8466,8467],{},[229,8468,8469],{},"Full build",[462,8471,8472],{},[229,8473,8474],{},"~38 s",[462,8476,8477],{},[229,8478,8479],{},"~11 s",[462,8481,8482],{},[229,8483,8484],{},"−27 s",[14,8486,8487,8488,8490,8491,8493],{},"The image cache is the largest contributor: ",[253,8489,2125],{}," skips re-encoding unchanged images, so warm rebuilds avoid the work ",[253,8492,7864],{}," repeated. The rest of the speedup is Eleventy avoiding Ruby's per-page rendering overhead.",[34,8495,600],{"id":599},[39,8497,8498,8510,8519,8527,8533],{},[42,8499,8500,8503,8504,8506,8507,8509],{},[229,8501,8502],{},"Recreating SEO markup by hand inconsistently:"," centralize values in ",[253,8505,7983],{}," so the head partial has one source of truth, the way ",[253,8508,6425],{}," served Jekyll.",[42,8511,8512,8515,8516,8518],{},[229,8513,8514],{},"Forgetting the feed's absolute URLs:"," feed readers need absolute links. Use the RSS plugin's ",[253,8517,8084],{}," filter or readers will fetch broken relative paths.",[42,8520,8521,8524,8525,239],{},[229,8522,8523],{},"Not caching eleventy-img output:"," if CI discards the cache directory, every build re-encodes every image and you lose the biggest speedup. Persist the cache as in ",[23,8526,1049],{"href":1048},[42,8528,8529,8532],{},[229,8530,8531],{},"Liquid vs Nunjucks surprises:"," Eleventy supports Liquid, but some Jekyll-specific filters differ. Test each migrated template's output against the Jekyll original before deleting the old site.",[42,8534,8535,8537],{},[229,8536,637],{}," keep the Jekyll site building in parallel until the Eleventy output diffs clean. Because both consume the same Markdown source, you can run both and compare generated HTML page-by-page before cutting over.",[34,8539,642],{"id":641},[14,8541,8542,8543,8545,8546,239],{},"The plugin question that blocks most Jekyll-to-Eleventy migrations turns out to have a tidy answer: the four near-universal plugins map to two official Eleventy plugins, one built-in feature, and one small head template. Inventory what you actually use, port those four first, and handle stray plugins as short JavaScript functions. In our 600-page migration the result was the same output with a build that dropped from ~38 s to ~11 s, driven mostly by ",[253,8544,2125],{},"'s cached pipeline. For the wider Jekyll-in-production picture, return to the parent ",[23,8547,2225],{"href":2224},[34,8549,651],{"id":650},[653,8551,8553],{"id":8552},"is-there-a-one-to-one-eleventy-plugin-for-every-jekyll-plugin","Is there a one-to-one Eleventy plugin for every Jekyll plugin?",[14,8555,8556],{},"No, and you usually do not need one. jekyll-feed and jekyll-seo-tag map to official Eleventy plugins or small templates, jekyll-paginate maps to Eleventy's built-in pagination, and jekyll-picture-tag maps to the official eleventy-img plugin. A few one-off Jekyll plugins become a short JavaScript function instead.",[653,8558,8560],{"id":8559},"what-replaces-jekyll-paginate-in-eleventy","What replaces jekyll-paginate in Eleventy?",[14,8562,8563],{},"Nothing to install — pagination is built into Eleventy. You add a pagination object to a template's frontmatter specifying the data to page over and the page size, and Eleventy generates the paged output files automatically.",[653,8565,8567],{"id":8566},"how-do-i-replace-jekyll-seo-tag","How do I replace jekyll-seo-tag?",[14,8569,8570],{},"Eleventy has no single drop-in tag, but the same output is a small head partial that reads site and page data and emits the title, description, canonical, and Open Graph tags. It is about twenty lines of template and gives you full control over the markup.",[653,8572,8574],{"id":8573},"does-eleventy-img-work-at-build-time-like-jekyll-picture-tag","Does eleventy-img work at build time like jekyll-picture-tag?",[14,8576,8577],{},"Yes. The official eleventy-img plugin resizes and re-encodes images during the build, writing optimized files to the output directory with no runtime cost, and it can emit a full picture element with multiple formats and widths.",[653,8579,8581],{"id":8580},"will-the-eleventy-build-be-faster-than-jekyll-after-migrating","Will the Eleventy build be faster than Jekyll after migrating?",[14,8583,8584],{},"Usually, for Markdown-heavy sites. In our 600-page migration the full build dropped from about 38 seconds in Jekyll to about 11 seconds in Eleventy, largely because Eleventy avoids Ruby's per-page overhead and runs image processing through a cached Node pipeline.",[34,8586,684],{"id":683},[39,8588,8589,8596,8601,8606],{},[42,8590,8591,692,8593,8595],{},[229,8592,691],{},[23,8594,2225],{"href":2224}," — the Jekyll-in-production setup these plugins come from.",[42,8597,8598,8600],{},[23,8599,1161],{"href":1160}," — the authoring comparison behind the migration choice.",[42,8602,8603,8605],{},[23,8604,2190],{"href":2189}," — the same build-time image model that eleventy-img follows.",[42,8607,8608,8610],{},[23,8609,31],{"href":30}," — where the migration decision fits the full picture.\n\n",[1346,8612,8613],{},"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 .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}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 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 .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":712,"searchDepth":713,"depth":713,"links":8615},[8616,8617,8618,8619,8620,8621,8622,8623,8624,8625,8632],{"id":36,"depth":713,"text":37},{"id":7912,"depth":713,"text":7913},{"id":7927,"depth":713,"text":7928},{"id":7990,"depth":713,"text":7991},{"id":8088,"depth":713,"text":8089},{"id":8163,"depth":713,"text":8164},{"id":1165,"depth":713,"text":1166},{"id":599,"depth":713,"text":600},{"id":641,"depth":713,"text":642},{"id":650,"depth":713,"text":651,"children":8626},[8627,8628,8629,8630,8631],{"id":8552,"depth":730,"text":8553},{"id":8559,"depth":730,"text":8560},{"id":8566,"depth":730,"text":8567},{"id":8573,"depth":730,"text":8574},{"id":8580,"depth":730,"text":8581},{"id":683,"depth":713,"text":684},[8634,8635,8636,8637],{"name":737,"item":738},{"name":31,"item":30},{"name":2225,"item":2224},{"name":6623,"item":6622},"Map common Jekyll plugins — jekyll-seo-tag, jekyll-feed, jekyll-paginate, jekyll-picture-tag — to their Eleventy equivalents, with config and measured build impact.",[8640,8641,8642,8643,8644],{"q":8553,"a":8556},{"q":8560,"a":8563},{"q":8567,"a":8570},{"q":8574,"a":8577},{"q":8581,"a":8584},{},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fjekyll-plugin-ecosystem\u002Freplacing-jekyll-plugins-when-migrating-to-eleventy",{"title":6623,"description":8638},"choosing-the-right-static-site-generator-for-production\u002Fjekyll-plugin-ecosystem\u002Freplacing-jekyll-plugins-when-migrating-to-eleventy\u002Findex","a3jW2cKesj6i76Su7fok78r1CJ6epa0DEN6NPN_X8pc",{"id":8651,"title":5494,"body":8652,"breadcrumb":9640,"dateModified":743,"datePublished":743,"description":9644,"extension":745,"faq":9645,"meta":9654,"navigation":752,"path":9655,"seo":9656,"slug":8656,"stem":9657,"type":2460,"__hash__":9658},"content\u002Fchoosing-the-right-static-site-generator-for-production\u002Fnextjs-static-export-for-content-sites\u002Findex.md",{"type":7,"value":8653,"toc":9623},[8654,8657,8674,8756,8760,8767,8866,8894,8920,8924,8937,9075,9084,9090,9094,9097,9100,9165,9173,9177,9185,9195,9198,9257,9376,9384,9388,9394,9439,9449,9451,9510,9512,9540,9542,9546,9552,9556,9562,9566,9573,9577,9580,9584,9587,9589,9620],[10,8655,5494],{"id":8656},"nextjs-static-export-for-content-sites",[14,8658,8659,8660,270,8663,8666,8667,8670,8671,8673],{},"Next.js can emit a fully static site. Set ",[253,8661,8662],{},"output: 'export'",[253,8664,8665],{},"next build"," writes an ",[253,8668,8669],{},"out\u002F"," directory of plain HTML and assets that any static host serves with no Node process behind it. That makes Next a candidate for the same content and marketing work you would otherwise hand to Astro or Hugo — but it arrives with a React runtime, a different build cost, and a set of image and routing caveats that the framework-native generators do not have. This guide covers when a static export fits, what it costs at runtime, where the sharp edges are, and exactly what the build emits, so the choice is measured rather than assumed. It sits within ",[23,8672,31],{"href":30},", where the trade-offs between generators are the whole point.",[55,8675,8676,8753],{},[58,8677,66,8682,66,8685,66,8688,66,8746],{"viewBox":8678,"role":61,"ariaLabelledBy":8679,"xmlns":65},"0 0 780 360",[8680,8681],"nse-fit-title","nse-fit-desc",[68,8683,8684],{"id":8680},"When Next.js static export fits a content site, and when it does not",[72,8686,8687],{"id":8681},"A decision split: a shared React app or team pushes toward static export, while a JavaScript budget near zero or a fast-build requirement pushes toward Hugo or Astro. The export build emits an out directory of HTML, JSON data, and a React runtime.",[95,8689,78,8690,78,8693,78,8695,78,8697,78,8699,78,8701,78,8705,78,8708,78,8712,78,8716,78,8719,78,8721,78,8725,78,8728,78,8731,78,8734,78,8737,66],{"style":813},[99,8691,8692],{"x":167,"y":2521,"fill":103,"style":1416},"output: 'export' — does it fit this content site?",[107,8694],{"x":158,"y":3559,"width":160,"height":3559,"rx":823,"fill":114,"opacity":186,"stroke":114,"style":116},[99,8696,8665],{"x":167,"y":120,"fill":114,"style":121},[99,8698,8662],{"x":167,"y":1426,"fill":93,"style":126},[107,8700],{"x":3578,"y":7852,"width":1463,"height":7852,"rx":113,"fill":185,"opacity":825,"stroke":187,"style":116},[99,8702,8704],{"x":142,"y":8703,"fill":187,"style":121},"188","Fits when",[99,8706,8707],{"x":142,"y":179,"fill":103,"style":859},"React app shares the stack",[99,8709,8711],{"x":142,"y":8710,"fill":103,"style":859},"238","team already knows Next",[99,8713,8715],{"x":142,"y":8714,"fill":103,"style":859},"262","MDX with React components",[99,8717,8718],{"x":142,"y":1462,"fill":93,"style":126},"JS runtime is acceptable",[107,8720],{"x":5338,"y":7852,"width":1463,"height":7852,"rx":113,"fill":2564,"opacity":115,"stroke":2565,"style":116},[99,8722,8724],{"x":8723,"y":8703,"fill":2565,"style":121},"580","Reach for Hugo \u002F Astro when",[99,8726,8727],{"x":8723,"y":179,"fill":103,"style":859},"JS budget near zero",[99,8729,8730],{"x":8723,"y":8710,"fill":103,"style":859},"build speed is critical",[99,8732,8733],{"x":8723,"y":8714,"fill":103,"style":859},"no React app alongside",[99,8735,8736],{"x":8723,"y":1462,"fill":93,"style":126},"pure content \u002F marketing",[95,8738,88,8739,88,8743,78],{"stroke":93,"fill":205,"style":116},[90,8740],{"d":8741,"style":8742},"M330 116 L210 158","marker-end:url(#nse-arrow)",[90,8744],{"d":8745,"style":8742},"M450 116 L570 158",[76,8747,78,8748,66],{},[80,8749,88,8751,78],{"id":8750,"viewBox":83,"refX":84,"refY":85,"markerWidth":86,"markerHeight":86,"orient":87},"nse-arrow",[90,8752],{"d":92,"fill":93},[218,8754,8755],{},"A shared React stack and team familiarity pull toward a static export; a near-zero JavaScript budget or a strict build-time target pull toward Hugo or Astro.",[34,8757,8759],{"id":8758},"turning-on-the-static-export","Turning On the Static Export",[14,8761,8762,8763,8766],{},"A static export is a one-line config change plus a couple of supporting flags. In ",[253,8764,8765],{},"next.config.js",", set the output mode and decide how images are handled, since the default image optimizer needs a server that the export does not have:",[987,8768,8770],{"className":1600,"code":8769,"language":1602,"meta":712,"style":712},"\u002F\u002F next.config.js\n\u002F** @type {import('next').NextConfig} *\u002F\nconst nextConfig = {\n  output: 'export',\n  images: {\n    unoptimized: true, \u002F\u002F no optimization server in a static export\n  },\n  trailingSlash: true, \u002F\u002F emit \u002Fabout\u002F as \u002Fabout\u002Findex.html\n};\n\nmodule.exports = nextConfig;\n",[253,8771,8772,8777,8791,8803,8812,8817,8829,8833,8845,8849,8853],{"__ignoreMap":712},[995,8773,8774],{"class":997,"line":998},[995,8775,8776],{"class":1001},"\u002F\u002F next.config.js\n",[995,8778,8779,8782,8785,8788],{"class":997,"line":713},[995,8780,8781],{"class":1001},"\u002F** ",[995,8783,8784],{"class":1614},"@type",[995,8786,8787],{"class":1007}," {import('next').NextConfig}",[995,8789,8790],{"class":1001}," *\u002F\n",[995,8792,8793,8795,8798,8800],{"class":997,"line":730},[995,8794,6228],{"class":1614},[995,8796,8797],{"class":1010}," nextConfig",[995,8799,1775],{"class":1614},[995,8801,8802],{"class":1618}," {\n",[995,8804,8805,8807,8810],{"class":997,"line":1544},[995,8806,2890],{"class":1618},[995,8808,8809],{"class":1023},"'export'",[995,8811,2885],{"class":1618},[995,8813,8814],{"class":997,"line":1550},[995,8815,8816],{"class":1618},"  images: {\n",[995,8818,8819,8822,8824,8826],{"class":997,"line":1673},[995,8820,8821],{"class":1618},"    unoptimized: ",[995,8823,6283],{"class":1010},[995,8825,1850],{"class":1618},[995,8827,8828],{"class":1001},"\u002F\u002F no optimization server in a static export\n",[995,8830,8831],{"class":997,"line":1678},[995,8832,1729],{"class":1618},[995,8834,8835,8838,8840,8842],{"class":997,"line":1693},[995,8836,8837],{"class":1618},"  trailingSlash: ",[995,8839,6283],{"class":1010},[995,8841,1850],{"class":1618},[995,8843,8844],{"class":1001},"\u002F\u002F emit \u002Fabout\u002F as \u002Fabout\u002Findex.html\n",[995,8846,8847],{"class":997,"line":1705},[995,8848,1877],{"class":1618},[995,8850,8851],{"class":997,"line":1711},[995,8852,1541],{"emptyLinePlaceholder":752},[995,8854,8855,8857,8859,8861,8863],{"class":997,"line":1717},[995,8856,1767],{"class":1010},[995,8858,239],{"class":1618},[995,8860,1772],{"class":1010},[995,8862,1775],{"class":1614},[995,8864,8865],{"class":1618}," nextConfig;\n",[14,8867,8868,8869,8871,8872,8874,8875,8878,8879,8882,8883,8886,8887,8890,8891,8893],{},"With this in place, ",[253,8870,8665],{}," writes everything to ",[253,8873,8669],{},". There is no ",[253,8876,8877],{},"next start"," step and no Node runtime in production — you deploy the folder the same way you would deploy a Hugo ",[253,8880,8881],{},"public\u002F"," or an Astro ",[253,8884,8885],{},"dist\u002F",". The catch is that anything depending on a server is now off the table: API routes, middleware, on-demand Incremental Static Regeneration, and ",[253,8888,8889],{},"next\u002Fimage","'s default loader all assume a running Next server and will either error at build time or silently not work. The framework documents these as unsupported features for ",[253,8892,8662],{},", and the build log names the offending route if you leave one in.",[14,8895,8896,8897,8900,8901,8904,8905,8908,8909,8912,8913,8916,8917,8919],{},"The ",[253,8898,8899],{},"trailingSlash: true"," flag matters for static hosts. Without it, Next emits ",[253,8902,8903],{},"about.html"," rather than ",[253,8906,8907],{},"about\u002Findex.html",", and some hosts will not serve ",[253,8910,8911],{},"\u002Fabout\u002F"," to ",[253,8914,8915],{},"\u002Fabout\u002Findex.html"," automatically. Setting it makes the output match how Astro and Hugo lay out directory-style URLs, which keeps your link structure portable across hosts. Aligning slug conventions across generators is part of the broader ",[23,8918,26],{"href":25}," evaluation.",[34,8921,8923],{"id":8922},"reading-data-at-build-time","Reading Data at Build Time",[14,8925,8926,8927,8930,8931,270,8934,8936],{},"On a content site, your pages come from Markdown, MDX, or a headless CMS. In a static export every page must be fully resolvable at build time, so you read data inside ",[253,8928,8929],{},"generateStaticParams"," and the server component body (App Router) or ",[253,8932,8933],{},"getStaticProps",[253,8935,2258],{}," (Pages Router). There is no request-time data fetching, because there is no request.",[987,8938,8942],{"className":8939,"code":8940,"language":8941,"meta":712,"style":712},"language-jsx shiki shiki-themes github-light github-dark","\u002F\u002F app\u002Fblog\u002F[slug]\u002Fpage.jsx (App Router)\nimport { getAllSlugs, getPostBySlug } from '@\u002Flib\u002Fposts';\n\nexport function generateStaticParams() {\n  return getAllSlugs().map((slug) => ({ slug }));\n}\n\nexport default async function Post({ params }) {\n  const post = await getPostBySlug(params.slug);\n  return \u003Carticle dangerouslySetInnerHTML={{ __html: post.html }} \u002F>;\n}\n","jsx",[253,8943,8944,8949,8963,8967,8979,9004,9009,9013,9036,9053,9071],{"__ignoreMap":712},[995,8945,8946],{"class":997,"line":998},[995,8947,8948],{"class":1001},"\u002F\u002F app\u002Fblog\u002F[slug]\u002Fpage.jsx (App Router)\n",[995,8950,8951,8953,8956,8958,8961],{"class":997,"line":713},[995,8952,1615],{"class":1614},[995,8954,8955],{"class":1618}," { getAllSlugs, getPostBySlug } ",[995,8957,1622],{"class":1614},[995,8959,8960],{"class":1023}," '@\u002Flib\u002Fposts'",[995,8962,1628],{"class":1618},[995,8964,8965],{"class":997,"line":730},[995,8966,1541],{"emptyLinePlaceholder":752},[995,8968,8969,8971,8973,8976],{"class":997,"line":1544},[995,8970,1681],{"class":1614},[995,8972,1778],{"class":1614},[995,8974,8975],{"class":1007}," generateStaticParams",[995,8977,8978],{"class":1618},"() {\n",[995,8980,8981,8983,8986,8989,8992,8994,8997,8999,9001],{"class":997,"line":1550},[995,8982,5855],{"class":1614},[995,8984,8985],{"class":1007}," getAllSlugs",[995,8987,8988],{"class":1618},"().",[995,8990,8991],{"class":1007},"map",[995,8993,1845],{"class":1618},[995,8995,8996],{"class":1784},"slug",[995,8998,1811],{"class":1618},[995,9000,1858],{"class":1614},[995,9002,9003],{"class":1618}," ({ slug }));\n",[995,9005,9006],{"class":997,"line":1673},[995,9007,9008],{"class":1618},"}\n",[995,9010,9011],{"class":997,"line":1678},[995,9012,1541],{"emptyLinePlaceholder":752},[995,9014,9015,9017,9019,9022,9024,9027,9030,9033],{"class":997,"line":1693},[995,9016,1681],{"class":1614},[995,9018,1684],{"class":1614},[995,9020,9021],{"class":1614}," async",[995,9023,1778],{"class":1614},[995,9025,9026],{"class":1007}," Post",[995,9028,9029],{"class":1618},"({ ",[995,9031,9032],{"class":1784},"params",[995,9034,9035],{"class":1618}," }) {\n",[995,9037,9038,9040,9043,9045,9047,9050],{"class":997,"line":1705},[995,9039,6270],{"class":1614},[995,9041,9042],{"class":1010}," post",[995,9044,1775],{"class":1614},[995,9046,8281],{"class":1614},[995,9048,9049],{"class":1007}," getPostBySlug",[995,9051,9052],{"class":1618},"(params.slug);\n",[995,9054,9055,9057,9060,9063,9066,9068],{"class":997,"line":1711},[995,9056,5855],{"class":1614},[995,9058,9059],{"class":1618}," \u003C",[995,9061,9062],{"class":1921},"article",[995,9064,9065],{"class":1007}," dangerouslySetInnerHTML",[995,9067,7317],{"class":1614},[995,9069,9070],{"class":1618},"{{ __html: post.html }} \u002F>;\n",[995,9072,9073],{"class":997,"line":1717},[995,9074,9008],{"class":1618},[14,9076,9077,270,9080,9083],{},[253,9078,9079],{},"getAllSlugs",[253,9081,9082],{},"getPostBySlug"," read the filesystem or call your CMS during the build and never run again. This is the same mental model Hugo and Astro use — resolve everything up front — but in Next you are writing it as React data functions. A page with no client interactivity still pulls in the framework runtime to hydrate, which is the runtime cost the next section measures.",[14,9085,9086,9087,9089],{},"The one ergonomic win Next gives you here is that MDX content can embed real React components, so an authored article can drop in a live pricing table or an interactive chart without leaving Markdown. That capability is genuinely hard to replicate in Hugo and is the main reason a documentation team on React reaches for a Next export rather than a templating generator. If your content never needs embedded interactive components, that advantage evaporates and the JavaScript cost is pure overhead — which is the trade the ",[23,9088,26],{"href":25}," is built to score explicitly.",[34,9091,9093],{"id":9092},"what-it-costs-at-runtime","What It Costs at Runtime",[14,9095,9096],{},"This is the decision that actually matters for a content site. Hugo and Eleventy ship zero JavaScript by default; Astro ships zero unless an island opts in. An exported Next.js page always ships the React runtime plus its hydration and routing client, even on a page that has no interactive elements at all.",[14,9098,9099],{},"We built the same simple article page — a heading, body copy, and a nav — in each generator and measured the compressed JavaScript transferred for a first load with Chrome DevTools' Network panel (throttled to Fast 3G, cache disabled):",[433,9101,9102,9114],{},[436,9103,9104],{},[439,9105,9106,9108,9111],{},[442,9107,3136],{},[442,9109,9110],{},"First-load JS (gzip)",[442,9112,9113],{},"Notes",[457,9115,9116,9125,9134,9144,9155],{},[439,9117,9118,9120,9122],{},[462,9119,265],{},[462,9121,1214],{},[462,9123,9124],{},"no client runtime unless you add one",[439,9126,9127,9129,9131],{},[462,9128,273],{},[462,9130,1214],{},[462,9132,9133],{},"plain HTML output",[439,9135,9136,9139,9141],{},[462,9137,9138],{},"Astro (no island)",[462,9140,1214],{},[462,9142,9143],{},"hydration only on opted-in islands",[439,9145,9146,9149,9152],{},[462,9147,9148],{},"Astro (one island)",[462,9150,9151],{},"~14 KB",[462,9153,9154],{},"the island's framework runtime",[439,9156,9157,9159,9162],{},[462,9158,5553],{},[462,9160,9161],{},"~82 KB",[462,9163,9164],{},"React + hydration + router client",[14,9166,9167,9168,9172],{},"That 82 KB is not a bug — it is the cost of keeping React's component model on the client so links prefetch and route transitions feel like an app. For a marketing page measured against Core Web Vitals, that JavaScript has to parse and execute before the page is interactive, which shows up in Interaction to Next Paint. The head-to-head in ",[23,9169,9171],{"href":9170},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fnextjs-static-export-for-content-sites\u002Fnextjs-static-export-vs-astro-for-marketing-sites\u002F","Next.js Static Export vs Astro for Marketing Sites"," measures the LCP and INP consequences directly.",[34,9174,9176],{"id":9175},"image-handling-without-the-optimizer","Image Handling Without the Optimizer",[14,9178,9179,9181,9182,9184],{},[253,9180,8889],{}," is one of Next's best features and the one a static export breaks. The default loader resizes and reformats images through an optimization server that simply is not present in ",[253,9183,8669],{},". You have two workable paths.",[14,9186,9187,9188,9191,9192,9194],{},"The first is ",[253,9189,9190],{},"images.unoptimized: true",", shown earlier. ",[253,9193,8889],{}," still renders, still reserves space to avoid layout shift, and still lazy-loads — but it serves the original file untouched, so you must pre-size and pre-compress images yourself. For a content site with a fixed set of authored images, exporting WebP at the display width before the build is a perfectly good answer.",[14,9196,9197],{},"The second is a custom loader that hands resizing to an external image CDN:",[987,9199,9201],{"className":1600,"code":9200,"language":1602,"meta":712,"style":712},"\u002F\u002F next.config.js\nconst nextConfig = {\n  output: 'export',\n  images: {\n    loader: 'custom',\n    loaderFile: '.\u002Fimage-loader.js',\n  },\n};\n",[253,9202,9203,9207,9217,9225,9229,9239,9249,9253],{"__ignoreMap":712},[995,9204,9205],{"class":997,"line":998},[995,9206,8776],{"class":1001},[995,9208,9209,9211,9213,9215],{"class":997,"line":713},[995,9210,6228],{"class":1614},[995,9212,8797],{"class":1010},[995,9214,1775],{"class":1614},[995,9216,8802],{"class":1618},[995,9218,9219,9221,9223],{"class":997,"line":730},[995,9220,2890],{"class":1618},[995,9222,8809],{"class":1023},[995,9224,2885],{"class":1618},[995,9226,9227],{"class":997,"line":1544},[995,9228,8816],{"class":1618},[995,9230,9231,9234,9237],{"class":997,"line":1550},[995,9232,9233],{"class":1618},"    loader: ",[995,9235,9236],{"class":1023},"'custom'",[995,9238,2885],{"class":1618},[995,9240,9241,9244,9247],{"class":997,"line":1673},[995,9242,9243],{"class":1618},"    loaderFile: ",[995,9245,9246],{"class":1023},"'.\u002Fimage-loader.js'",[995,9248,2885],{"class":1618},[995,9250,9251],{"class":997,"line":1678},[995,9252,1729],{"class":1618},[995,9254,9255],{"class":997,"line":1693},[995,9256,1877],{"class":1618},[987,9258,9260],{"className":1600,"code":9259,"language":1602,"meta":712,"style":712},"\u002F\u002F image-loader.js\nexport default function cloudinaryLoader({ src, width, quality }) {\n  const params = ['f_auto', 'c_limit', `w_${width}`, `q_${quality || 75}`];\n  return `https:\u002F\u002Fres.cloudinary.com\u002Fdemo\u002Fimage\u002Fupload\u002F${params.join(',')}\u002F${src}`;\n}\n",[253,9261,9262,9267,9294,9342,9372],{"__ignoreMap":712},[995,9263,9264],{"class":997,"line":998},[995,9265,9266],{"class":1001},"\u002F\u002F image-loader.js\n",[995,9268,9269,9271,9273,9275,9278,9280,9282,9284,9287,9289,9292],{"class":997,"line":713},[995,9270,1681],{"class":1614},[995,9272,1684],{"class":1614},[995,9274,1778],{"class":1614},[995,9276,9277],{"class":1007}," cloudinaryLoader",[995,9279,9029],{"class":1618},[995,9281,1579],{"class":1784},[995,9283,1850],{"class":1618},[995,9285,9286],{"class":1784},"width",[995,9288,1850],{"class":1618},[995,9290,9291],{"class":1784},"quality",[995,9293,9035],{"class":1618},[995,9295,9296,9298,9301,9303,9306,9309,9311,9314,9316,9319,9321,9324,9326,9329,9331,9334,9337,9339],{"class":997,"line":730},[995,9297,6270],{"class":1614},[995,9299,9300],{"class":1010}," params",[995,9302,1775],{"class":1614},[995,9304,9305],{"class":1618}," [",[995,9307,9308],{"class":1023},"'f_auto'",[995,9310,1850],{"class":1618},[995,9312,9313],{"class":1023},"'c_limit'",[995,9315,1850],{"class":1618},[995,9317,9318],{"class":1023},"`w_${",[995,9320,9286],{"class":1618},[995,9322,9323],{"class":1023},"}`",[995,9325,1850],{"class":1618},[995,9327,9328],{"class":1023},"`q_${",[995,9330,9291],{"class":1618},[995,9332,9333],{"class":1614}," ||",[995,9335,9336],{"class":1010}," 75",[995,9338,9323],{"class":1023},[995,9340,9341],{"class":1618},"];\n",[995,9343,9344,9346,9349,9351,9353,9356,9358,9361,9363,9366,9368,9370],{"class":997,"line":1544},[995,9345,5855],{"class":1614},[995,9347,9348],{"class":1023}," `https:\u002F\u002Fres.cloudinary.com\u002Fdemo\u002Fimage\u002Fupload\u002F${",[995,9350,9032],{"class":1618},[995,9352,239],{"class":1023},[995,9354,9355],{"class":1007},"join",[995,9357,1799],{"class":1023},[995,9359,9360],{"class":1023},"','",[995,9362,982],{"class":1023},[995,9364,9365],{"class":1023},"}\u002F${",[995,9367,1579],{"class":1618},[995,9369,9323],{"class":1023},[995,9371,1628],{"class":1618},[995,9373,9374],{"class":997,"line":1550},[995,9375,9008],{"class":1618},[14,9377,9378,9379,9381,9382,239],{},"This keeps responsive ",[253,9380,3720],{}," generation but moves the actual transform to request time at the CDN, which is the same trade you would make for any generator that lacks build-time image processing. The principle of doing image work once and serving the result is covered framework-agnostically in ",[23,9383,2190],{"href":2189},[34,9385,9387],{"id":9386},"build-output-and-build-speed","Build Output and Build Speed",[14,9389,9390,9391,9393],{},"The export build is heavier than a framework-native generator's because Next compiles a React application, not just templates. We benchmarked a clean build of the same 500-page content set with ",[253,9392,930],{}," on an 8-core runner:",[433,9395,9396,9408],{},[436,9397,9398],{},[439,9399,9400,9402,9405],{},[442,9401,3136],{},[442,9403,9404],{},"Median cold build",[442,9406,9407],{},"Output size (HTML + JS)",[457,9409,9410,9420,9429],{},[439,9411,9412,9414,9417],{},[462,9413,265],{},[462,9415,9416],{},"3.1s",[462,9418,9419],{},"18 MB",[439,9421,9422,9424,9426],{},[462,9423,269],{},[462,9425,2086],{},[462,9427,9428],{},"26 MB",[439,9430,9431,9433,9436],{},[462,9432,5553],{},[462,9434,9435],{},"41s",[462,9437,9438],{},"61 MB",[14,9440,9441,9442,9445,9446,9448],{},"The Next output is larger partly because each route ships a JSON data payload alongside its HTML so client navigation can hydrate without a full reload, and partly because of the shared chunks the React runtime needs. On CI, the build cost compounds, so caching matters: persist ",[253,9443,9444],{},".next\u002Fcache"," between runs and the warm rebuild in our test dropped from 41s to 19s. The same caching discipline that helps every generator is covered in ",[23,9447,288],{"href":287},", which lays out a fair benchmarking method you can extend to Next.",[34,9450,2266],{"id":2265},[39,9452,9453,9465,9478,9489,9504],{},[42,9454,9455,9458,9459,9462,9463,239],{},[229,9456,9457],{},"Leaving a server-only feature in the tree:"," an API route, ",[253,9460,9461],{},"middleware.ts",", or a route using on-demand revalidation will fail the export build. Remove them or move them to an external service before switching to ",[253,9464,8662],{},[42,9466,9467,9474,9475,9477],{},[229,9468,9469,9470,9473],{},"Forgetting ",[253,9471,9472],{},"images.unoptimized"," or a custom loader:"," the default ",[253,9476,8889],{}," loader errors at build time in an export. Decide your image strategy first.",[42,9479,9480,9483,9484,256,9486,9488],{},[229,9481,9482],{},"Assuming dynamic routes work without params:"," every dynamic segment needs ",[253,9485,8929],{},[253,9487,2258],{},") to enumerate paths, or the page is simply not generated.",[42,9490,9491,9494,9495,9497,9498,8912,9500,9503],{},[229,9492,9493],{},"Host trailing-slash mismatch:"," without ",[253,9496,8899],{},", directory-style URLs may 404 on hosts that do not rewrite ",[253,9499,8911],{},[253,9501,9502],{},"\u002Fabout.html",". Match the flag to your host's behavior.",[42,9505,9506,9509],{},[229,9507,9508],{},"Shipping the JS cost unexamined:"," the ~82 KB runtime is fine for an app-adjacent site and wasteful for a brochure page. Measure it against your performance budget rather than ignoring it.",[34,9511,2321],{"id":2320},[39,9513,9514,9522,9528,9531,9537],{},[42,9515,9516,9518,9519,9521],{},[253,9517,8662],{}," produces a fully static ",[253,9520,8669],{}," directory with no Node server — deploy it like any other static site.",[42,9523,9524,9525,9527],{},"Server-dependent features (API routes, middleware, on-demand ISR, the default ",[253,9526,8889],{}," loader) are unavailable; plan around them up front.",[42,9529,9530],{},"Every exported page ships the React runtime: roughly 82 KB compressed in our test, versus 0 KB for an equivalent Hugo or no-island Astro page.",[42,9532,9533,9534,9536],{},"Builds are slower and outputs larger than Hugo or Astro; cache ",[253,9535,9444],{}," to make CI rebuilds reasonable.",[42,9538,9539],{},"Choose a static export when a React app and team already live on the stack; choose Hugo or Astro for a pure content site with a tight JavaScript budget.",[34,9541,651],{"id":650},[653,9543,9545],{"id":9544},"what-does-output-export-actually-produce","What does output export actually produce?",[14,9547,9548,9549,9551],{},"A fully static ",[253,9550,8669],{}," directory of HTML, JSON, JS, and assets that any static host can serve. There is no Node server in the output, so server components run only at build time and API routes, middleware, and on-demand revalidation are unavailable.",[653,9553,9555],{"id":9554},"does-nextimage-work-with-a-static-export","Does next\u002Fimage work with a static export?",[14,9557,9558,9559,9561],{},"Not with the default loader, which needs the optimization server. You either set ",[253,9560,9472],{}," to true and pre-size your own images, or wire a custom loader that points at an external image CDN that resizes at request time.",[653,9563,9565],{"id":9564},"is-a-static-export-slower-to-build-than-hugo-or-astro","Is a static export slower to build than Hugo or Astro?",[14,9567,9568,9569,9572],{},"Usually yes. In our measurement a 500-page content site built in 3.1s with Hugo, 14s with Astro, and 41s with ",[253,9570,9571],{},"next export",", because Next bundles a React runtime and per-route data even for pages that ship no interactivity.",[653,9574,9576],{"id":9575},"how-much-javascript-does-an-exported-nextjs-page-ship","How much JavaScript does an exported Next.js page ship?",[14,9578,9579],{},"More than a comparable Astro or Hugo page. A minimal exported route loaded about 82 KB of compressed JS in our test for the framework and hydration runtime alone, versus 0 KB for an equivalent Hugo page and roughly 0-14 KB for an Astro page using islands.",[653,9581,9583],{"id":9582},"when-is-a-static-export-the-right-call-anyway","When is a static export the right call anyway?",[14,9585,9586],{},"When the team already knows React, shares components with an app on the same stack, or needs Next conventions like file-based routing and MDX with React components. For a pure content or marketing site with no app alongside it, Hugo or Astro usually delivers less JavaScript for less build time.",[34,9588,684],{"id":683},[39,9590,9591,9598,9605,9610,9615],{},[42,9592,9593,692,9595,9597],{},[229,9594,691],{},[23,9596,31],{"href":30}," — where this trade-off lives.",[42,9599,9600,9604],{},[23,9601,9603],{"href":9602},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fnextjs-static-export-for-content-sites\u002Fmigrating-from-gatsby-to-nextjs-static-export\u002F","Migrating from Gatsby to Next.js Static Export"," — the concrete migration recipe.",[42,9606,9607,9609],{},[23,9608,9171],{"href":9170}," — the measured head-to-head.",[42,9611,9612,9614],{},[23,9613,26],{"href":25}," — scoring Next against the other generators.",[42,9616,9617,9619],{},[23,9618,774],{"href":773}," — the same comparison framing for docs.",[1346,9621,9622],{},"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 .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .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 .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}",{"title":712,"searchDepth":713,"depth":713,"links":9624},[9625,9626,9627,9628,9629,9630,9631,9632,9639],{"id":8758,"depth":713,"text":8759},{"id":8922,"depth":713,"text":8923},{"id":9092,"depth":713,"text":9093},{"id":9175,"depth":713,"text":9176},{"id":9386,"depth":713,"text":9387},{"id":2265,"depth":713,"text":2266},{"id":2320,"depth":713,"text":2321},{"id":650,"depth":713,"text":651,"children":9633},[9634,9635,9636,9637,9638],{"id":9544,"depth":730,"text":9545},{"id":9554,"depth":730,"text":9555},{"id":9564,"depth":730,"text":9565},{"id":9575,"depth":730,"text":9576},{"id":9582,"depth":730,"text":9583},{"id":683,"depth":713,"text":684},[9641,9642,9643],{"name":737,"item":738},{"name":31,"item":30},{"name":5494,"item":5493},"When output export fits content and marketing sites — the runtime cost versus Astro and Hugo, routing and image caveats, and exactly what the static build emits.",[9646,9648,9650,9652,9653],{"q":9545,"a":9647},"A fully static out directory of HTML, JSON, JS, and assets that any static host can serve. There is no Node server in the output, so server components run only at build time and API routes, middleware, and on-demand revalidation are unavailable.",{"q":9555,"a":9649},"Not with the default loader, which needs the optimization server. You either set images.unoptimized to true and pre-size your own images, or wire a custom loader that points at an external image CDN that resizes at request time.",{"q":9565,"a":9651},"Usually yes. In our measurement a 500-page content site built in 3.1s with Hugo, 14s with Astro, and 41s with next export, because Next bundles a React runtime and per-route data even for pages that ship no interactivity.",{"q":9576,"a":9579},{"q":9583,"a":9586},{},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fnextjs-static-export-for-content-sites",{"title":5494,"description":9644},"choosing-the-right-static-site-generator-for-production\u002Fnextjs-static-export-for-content-sites\u002Findex","12759jTMTSFCiXbgYfRO4ZrJTkWziENYTiBXVoKb-1s",{"id":9660,"title":9603,"body":9661,"breadcrumb":10729,"dateModified":743,"datePublished":743,"description":10734,"extension":745,"faq":10735,"meta":10742,"navigation":752,"path":10743,"seo":10744,"slug":9665,"stem":10745,"type":756,"__hash__":10746},"content\u002Fchoosing-the-right-static-site-generator-for-production\u002Fnextjs-static-export-for-content-sites\u002Fmigrating-from-gatsby-to-nextjs-static-export\u002Findex.md",{"type":7,"value":9662,"toc":10713},[9663,9666,9678,9680,9694,9804,9808,9811,9861,9889,9893,9906,10160,10295,10310,10314,10317,10400,10403,10498,10500,10506,10563,10568,10570,10632,10634,10640,10642,10646,10652,10656,10670,10674,10677,10681,10684,10686,10710],[10,9664,9603],{"id":9665},"migrating-from-gatsby-to-nextjs-static-export",[14,9667,9668,9669,9671,9672,9674,9675,9677],{},"Gatsby and a Next.js static export solve the same problem — a React-based static content site — but they get there very differently. Gatsby routes everything through a build-time GraphQL data layer fed by source and transformer plugins. A Next.js static export skips that layer entirely: pages read data with plain functions and ",[253,9670,8665],{}," emits an ",[253,9673,8669],{}," directory. This guide is the concrete recipe for moving across — mapping the GraphQL queries to file reads, swapping the plugin ecosystem for ordinary libraries, fixing image handling, and the measured build-time and bundle deltas we saw doing it on a real content site. It belongs under ",[23,9676,5494],{"href":5493},", which covers when an export is the right target in the first place.",[34,9679,37],{"id":36},[39,9681,9682,9685,9688],{},[42,9683,9684],{},"A working Gatsby content site sourcing Markdown or MDX (a CMS source maps the same way — swap the file read for an API call).",[42,9686,9687],{},"Node 18+ and familiarity with React; both frameworks share the component model, so JSX components mostly port unchanged.",[42,9689,9690,9691,9693],{},"A decision already made that React is the right runtime for this content site. If it is not, ",[23,9692,9171],{"href":9170}," shows what a non-React generator saves.",[55,9695,9696,9801],{},[58,9697,66,9702,66,9705,66,9708,66,9794],{"viewBox":9698,"role":61,"ariaLabelledBy":9699,"xmlns":65},"0 0 780 330",[9700,9701],"g2n-map-title","g2n-map-desc",[68,9703,9704],{"id":9700},"Mapping Gatsby concepts to Next.js static export equivalents",[72,9706,9707],{"id":9701},"Four Gatsby concepts on the left — GraphQL data layer, source and transformer plugins, gatsby-image, and gatsby-plugin-react-helmet — map by arrows to their Next.js static export equivalents on the right: file reads in generateStaticParams, gray-matter plus a Markdown processor, next\u002Fimage, and the App Router metadata API.",[95,9709,78,9710,78,9713,78,9715,78,9718,78,9721,78,9724,78,9727,78,9730,78,9732,78,9735,78,9738,78,9740,78,9743,78,9746,78,9748,78,9752,78,9755,78,9757,78,9760,78,9763,78,9765,78,9768,78,9771,78,9773,78,9776,78,9779,66],{"style":97},[99,9711,9712],{"x":167,"y":109,"fill":103,"style":1416},"Gatsby concept → Next.js export equivalent",[107,9714],{"x":109,"y":110,"width":158,"height":5380,"rx":3579,"fill":114,"opacity":186,"stroke":114,"style":116},[99,9716,9717],{"x":160,"y":1430,"fill":114,"style":121},"GraphQL data layer",[99,9719,9720],{"x":160,"y":6849,"fill":93,"style":126},"page queries",[107,9722],{"x":109,"y":9723,"width":158,"height":5380,"rx":3579,"fill":114,"opacity":186,"stroke":114,"style":116},"122",[99,9725,9726],{"x":160,"y":5379,"fill":114,"style":121},"source + transformer plugins",[99,9728,9729],{"x":160,"y":7852,"fill":93,"style":126},"gatsby-source-filesystem, remark",[107,9731],{"x":109,"y":845,"width":158,"height":5380,"rx":3579,"fill":114,"opacity":186,"stroke":114,"style":116},[99,9733,9734],{"x":160,"y":5402,"fill":114,"style":121},"gatsby-image",[99,9736,9737],{"x":160,"y":146,"fill":93,"style":126},"GraphQL image nodes",[107,9739],{"x":109,"y":4674,"width":158,"height":5380,"rx":3579,"fill":114,"opacity":186,"stroke":114,"style":116},[99,9741,9742],{"x":160,"y":854,"fill":114,"style":121},"react-helmet plugin",[99,9744,9745],{"x":160,"y":154,"fill":93,"style":126},"head tags per page",[107,9747],{"x":863,"y":110,"width":158,"height":5380,"rx":3579,"fill":185,"opacity":186,"stroke":187,"style":116},[99,9749,9751],{"x":9750,"y":1430,"fill":187,"style":121},"600","file reads in generateStaticParams",[99,9753,9754],{"x":9750,"y":6849,"fill":93,"style":126},"plain async functions",[107,9756],{"x":863,"y":9723,"width":158,"height":5380,"rx":3579,"fill":185,"opacity":186,"stroke":187,"style":116},[99,9758,9759],{"x":9750,"y":5379,"fill":187,"style":121},"gray-matter + markdown processor",[99,9761,9762],{"x":9750,"y":7852,"fill":93,"style":126},"remark \u002F next-mdx-remote",[107,9764],{"x":863,"y":845,"width":158,"height":5380,"rx":3579,"fill":185,"opacity":186,"stroke":187,"style":116},[99,9766,9767],{"x":9750,"y":5402,"fill":187,"style":121},"next\u002Fimage (unoptimized \u002F loader)",[99,9769,9770],{"x":9750,"y":146,"fill":93,"style":126},"pre-sized or CDN-resized",[107,9772],{"x":863,"y":4674,"width":158,"height":5380,"rx":3579,"fill":185,"opacity":186,"stroke":187,"style":116},[99,9774,9775],{"x":9750,"y":854,"fill":187,"style":121},"metadata API \u002F next\u002Fhead",[99,9777,9778],{"x":9750,"y":154,"fill":93,"style":126},"export const metadata",[95,9780,88,9781,88,9785,88,9788,88,9791,78],{"stroke":93,"fill":205,"style":116},[90,9782],{"d":9783,"style":9784},"M330 84 L448 84","marker-end:url(#g2n-arrow)",[90,9786],{"d":9787,"style":9784},"M330 146 L448 146",[90,9789],{"d":9790,"style":9784},"M330 208 L448 208",[90,9792],{"d":9793,"style":9784},"M330 270 L448 270",[76,9795,78,9796,66],{},[80,9797,88,9799,78],{"id":9798,"viewBox":83,"refX":84,"refY":85,"markerWidth":86,"markerHeight":86,"orient":87},"g2n-arrow",[90,9800],{"d":92,"fill":93},[218,9802,9803],{},"Each Gatsby abstraction collapses into a small, explicit equivalent — the GraphQL layer becomes ordinary file reads, and most plugins become one library each.",[34,9805,9807],{"id":9806},"step-1-scaffold-and-turn-on-export","Step 1 — Scaffold and Turn On Export",[14,9809,9810],{},"Create a Next project and set it to static export immediately, so every change you port is validated against the real output target rather than a dev server that hides export-only failures:",[987,9812,9814],{"className":1600,"code":9813,"language":1602,"meta":712,"style":712},"\u002F\u002F next.config.js\nmodule.exports = {\n  output: 'export',\n  trailingSlash: true,\n  images: { unoptimized: true },\n};\n",[253,9815,9816,9820,9832,9840,9848,9857],{"__ignoreMap":712},[995,9817,9818],{"class":997,"line":998},[995,9819,8776],{"class":1001},[995,9821,9822,9824,9826,9828,9830],{"class":997,"line":713},[995,9823,1767],{"class":1010},[995,9825,239],{"class":1618},[995,9827,1772],{"class":1010},[995,9829,1775],{"class":1614},[995,9831,8802],{"class":1618},[995,9833,9834,9836,9838],{"class":997,"line":730},[995,9835,2890],{"class":1618},[995,9837,8809],{"class":1023},[995,9839,2885],{"class":1618},[995,9841,9842,9844,9846],{"class":997,"line":1544},[995,9843,8837],{"class":1618},[995,9845,6283],{"class":1010},[995,9847,2885],{"class":1618},[995,9849,9850,9853,9855],{"class":997,"line":1550},[995,9851,9852],{"class":1618},"  images: { unoptimized: ",[995,9854,6283],{"class":1010},[995,9856,2911],{"class":1618},[995,9858,9859],{"class":997,"line":1673},[995,9860,1877],{"class":1618},[14,9862,9863,9864,9867,9868,1850,9871,1850,9874,2114,9877,9880,9881,2039,9883,7048,9886,239],{},"Copy your ",[253,9865,9866],{},"src\u002Fcomponents"," over more or less as-is — React components that do not call Gatsby APIs (",[253,9869,9870],{},"useStaticQuery",[253,9872,9873],{},"graphql",[253,9875,9876],{},"\u003CLink>",[253,9878,9879],{},"gatsby",") need only an import swap, replacing ",[253,9882,9879],{},[253,9884,9885],{},"Link",[253,9887,9888],{},"next\u002Flink",[34,9890,9892],{"id":9891},"step-2-replace-the-graphql-data-layer-with-file-reads","Step 2 — Replace the GraphQL Data Layer with File Reads",[14,9894,9895,9896,9898,9899,9902,9903,9905],{},"This is the heart of the migration. In Gatsby a blog post page declares a ",[253,9897,9873],{}," query and receives ",[253,9900,9901],{},"data"," as a prop. In a Next static export you read the same source files directly in a small helper and call it from ",[253,9904,8929],{}," and the page component:",[987,9907,9909],{"className":1600,"code":9908,"language":1602,"meta":712,"style":712},"\u002F\u002F lib\u002Fposts.js\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport matter from 'gray-matter';\n\nconst DIR = path.join(process.cwd(), 'content\u002Fposts');\n\nexport function getAllSlugs() {\n  return fs.readdirSync(DIR).map((f) => f.replace(\u002F\\.md$\u002F, ''));\n}\n\nexport function getPostBySlug(slug) {\n  const raw = fs.readFileSync(path.join(DIR, `${slug}.md`), 'utf8');\n  const { data, content } = matter(raw);\n  return { frontmatter: data, body: content };\n}\n",[253,9910,9911,9916,9930,9944,9958,9962,9990,9994,10004,10062,10066,10070,10084,10124,10149,10156],{"__ignoreMap":712},[995,9912,9913],{"class":997,"line":998},[995,9914,9915],{"class":1001},"\u002F\u002F lib\u002Fposts.js\n",[995,9917,9918,9920,9923,9925,9928],{"class":997,"line":713},[995,9919,1615],{"class":1614},[995,9921,9922],{"class":1618}," fs ",[995,9924,1622],{"class":1614},[995,9926,9927],{"class":1023}," 'node:fs'",[995,9929,1628],{"class":1618},[995,9931,9932,9934,9937,9939,9942],{"class":997,"line":730},[995,9933,1615],{"class":1614},[995,9935,9936],{"class":1618}," path ",[995,9938,1622],{"class":1614},[995,9940,9941],{"class":1023}," 'node:path'",[995,9943,1628],{"class":1618},[995,9945,9946,9948,9951,9953,9956],{"class":997,"line":1544},[995,9947,1615],{"class":1614},[995,9949,9950],{"class":1618}," matter ",[995,9952,1622],{"class":1614},[995,9954,9955],{"class":1023}," 'gray-matter'",[995,9957,1628],{"class":1618},[995,9959,9960],{"class":997,"line":1550},[995,9961,1541],{"emptyLinePlaceholder":752},[995,9963,9964,9966,9969,9971,9974,9976,9979,9982,9985,9988],{"class":997,"line":1673},[995,9965,6228],{"class":1614},[995,9967,9968],{"class":1010}," DIR",[995,9970,1775],{"class":1614},[995,9972,9973],{"class":1618}," path.",[995,9975,9355],{"class":1007},[995,9977,9978],{"class":1618},"(process.",[995,9980,9981],{"class":1007},"cwd",[995,9983,9984],{"class":1618},"(), ",[995,9986,9987],{"class":1023},"'content\u002Fposts'",[995,9989,5829],{"class":1618},[995,9991,9992],{"class":997,"line":1678},[995,9993,1541],{"emptyLinePlaceholder":752},[995,9995,9996,9998,10000,10002],{"class":997,"line":1693},[995,9997,1681],{"class":1614},[995,9999,1778],{"class":1614},[995,10001,8985],{"class":1007},[995,10003,8978],{"class":1618},[995,10005,10006,10008,10011,10014,10016,10019,10021,10023,10025,10028,10030,10032,10035,10038,10040,10042,10046,10049,10052,10054,10056,10059],{"class":997,"line":1705},[995,10007,5855],{"class":1614},[995,10009,10010],{"class":1618}," fs.",[995,10012,10013],{"class":1007},"readdirSync",[995,10015,1799],{"class":1618},[995,10017,10018],{"class":1010},"DIR",[995,10020,260],{"class":1618},[995,10022,8991],{"class":1007},[995,10024,1845],{"class":1618},[995,10026,10027],{"class":1784},"f",[995,10029,1811],{"class":1618},[995,10031,1858],{"class":1614},[995,10033,10034],{"class":1618}," f.",[995,10036,10037],{"class":1007},"replace",[995,10039,1799],{"class":1618},[995,10041,738],{"class":1023},[995,10043,10045],{"class":10044},"snhLl","\\.",[995,10047,745],{"class":10048},"sA_wV",[995,10050,10051],{"class":1614},"$",[995,10053,738],{"class":1023},[995,10055,1850],{"class":1618},[995,10057,10058],{"class":1023},"''",[995,10060,10061],{"class":1618},"));\n",[995,10063,10064],{"class":997,"line":1711},[995,10065,9008],{"class":1618},[995,10067,10068],{"class":997,"line":1717},[995,10069,1541],{"emptyLinePlaceholder":752},[995,10071,10072,10074,10076,10078,10080,10082],{"class":997,"line":1726},[995,10073,1681],{"class":1614},[995,10075,1778],{"class":1614},[995,10077,9049],{"class":1007},[995,10079,1799],{"class":1618},[995,10081,8996],{"class":1784},[995,10083,1788],{"class":1618},[995,10085,10086,10088,10091,10093,10095,10098,10101,10103,10105,10107,10109,10112,10114,10117,10119,10122],{"class":997,"line":1732},[995,10087,6270],{"class":1614},[995,10089,10090],{"class":1010}," raw",[995,10092,1775],{"class":1614},[995,10094,10010],{"class":1618},[995,10096,10097],{"class":1007},"readFileSync",[995,10099,10100],{"class":1618},"(path.",[995,10102,9355],{"class":1007},[995,10104,1799],{"class":1618},[995,10106,10018],{"class":1010},[995,10108,1850],{"class":1618},[995,10110,10111],{"class":1023},"`${",[995,10113,8996],{"class":1618},[995,10115,10116],{"class":1023},"}.md`",[995,10118,6317],{"class":1618},[995,10120,10121],{"class":1023},"'utf8'",[995,10123,5829],{"class":1618},[995,10125,10126,10128,10131,10133,10135,10138,10141,10143,10146],{"class":997,"line":2967},[995,10127,6270],{"class":1614},[995,10129,10130],{"class":1618}," { ",[995,10132,9901],{"class":1010},[995,10134,1850],{"class":1618},[995,10136,10137],{"class":1010},"content",[995,10139,10140],{"class":1618}," } ",[995,10142,7317],{"class":1614},[995,10144,10145],{"class":1007}," matter",[995,10147,10148],{"class":1618},"(raw);\n",[995,10150,10151,10153],{"class":997,"line":2972},[995,10152,5855],{"class":1614},[995,10154,10155],{"class":1618}," { frontmatter: data, body: content };\n",[995,10157,10158],{"class":997,"line":4147},[995,10159,9008],{"class":1618},[987,10161,10163],{"className":8939,"code":10162,"language":8941,"meta":712,"style":712},"\u002F\u002F app\u002Fblog\u002F[slug]\u002Fpage.jsx\nimport { getAllSlugs, getPostBySlug } from '@\u002Flib\u002Fposts';\n\nexport function generateStaticParams() {\n  return getAllSlugs().map((slug) => ({ slug }));\n}\n\nexport default async function Post({ params }) {\n  const { frontmatter, body } = getPostBySlug(params.slug);\n  \u002F\u002F render body with your Markdown processor (next step)\n  return \u003Carticle>{\u002F* ... *\u002F}\u003C\u002Farticle>;\n}\n",[253,10164,10165,10170,10182,10186,10196,10216,10220,10224,10242,10264,10269,10291],{"__ignoreMap":712},[995,10166,10167],{"class":997,"line":998},[995,10168,10169],{"class":1001},"\u002F\u002F app\u002Fblog\u002F[slug]\u002Fpage.jsx\n",[995,10171,10172,10174,10176,10178,10180],{"class":997,"line":713},[995,10173,1615],{"class":1614},[995,10175,8955],{"class":1618},[995,10177,1622],{"class":1614},[995,10179,8960],{"class":1023},[995,10181,1628],{"class":1618},[995,10183,10184],{"class":997,"line":730},[995,10185,1541],{"emptyLinePlaceholder":752},[995,10187,10188,10190,10192,10194],{"class":997,"line":1544},[995,10189,1681],{"class":1614},[995,10191,1778],{"class":1614},[995,10193,8975],{"class":1007},[995,10195,8978],{"class":1618},[995,10197,10198,10200,10202,10204,10206,10208,10210,10212,10214],{"class":997,"line":1550},[995,10199,5855],{"class":1614},[995,10201,8985],{"class":1007},[995,10203,8988],{"class":1618},[995,10205,8991],{"class":1007},[995,10207,1845],{"class":1618},[995,10209,8996],{"class":1784},[995,10211,1811],{"class":1618},[995,10213,1858],{"class":1614},[995,10215,9003],{"class":1618},[995,10217,10218],{"class":997,"line":1673},[995,10219,9008],{"class":1618},[995,10221,10222],{"class":997,"line":1678},[995,10223,1541],{"emptyLinePlaceholder":752},[995,10225,10226,10228,10230,10232,10234,10236,10238,10240],{"class":997,"line":1693},[995,10227,1681],{"class":1614},[995,10229,1684],{"class":1614},[995,10231,9021],{"class":1614},[995,10233,1778],{"class":1614},[995,10235,9026],{"class":1007},[995,10237,9029],{"class":1618},[995,10239,9032],{"class":1784},[995,10241,9035],{"class":1618},[995,10243,10244,10246,10248,10251,10253,10256,10258,10260,10262],{"class":997,"line":1705},[995,10245,6270],{"class":1614},[995,10247,10130],{"class":1618},[995,10249,10250],{"class":1010},"frontmatter",[995,10252,1850],{"class":1618},[995,10254,10255],{"class":1010},"body",[995,10257,10140],{"class":1618},[995,10259,7317],{"class":1614},[995,10261,9049],{"class":1007},[995,10263,9052],{"class":1618},[995,10265,10266],{"class":997,"line":1711},[995,10267,10268],{"class":1001},"  \u002F\u002F render body with your Markdown processor (next step)\n",[995,10270,10271,10273,10275,10277,10280,10283,10286,10288],{"class":997,"line":1717},[995,10272,5855],{"class":1614},[995,10274,9059],{"class":1618},[995,10276,9062],{"class":1921},[995,10278,10279],{"class":1618},">{",[995,10281,10282],{"class":1001},"\u002F* ... *\u002F",[995,10284,10285],{"class":1618},"}\u003C\u002F",[995,10287,9062],{"class":1921},[995,10289,10290],{"class":1618},">;\n",[995,10292,10293],{"class":997,"line":1726},[995,10294,9008],{"class":1618},[14,10296,10297,10298,10301,10302,10305,10306,10309],{},"The Gatsby ",[253,10299,10300],{},"allMarkdownRemark"," list query that built your index page becomes a ",[253,10303,10304],{},"getAllSlugs().map(getPostBySlug)"," call sorted by date. There is no schema to infer and no GraphQL to learn — it is plain Node reading the same files. The mental shift is the same one covered for ",[23,10307,10308],{"href":25},"SSG selection generally",": explicit code over an inferred data layer.",[34,10311,10313],{"id":10312},"step-3-swap-plugins-for-libraries","Step 3 — Swap Plugins for Libraries",[14,10315,10316],{},"Gatsby's plugins map to ordinary npm packages you call yourself:",[39,10318,10319,10340,10352,10367,10383],{},[42,10320,10321,10327,10328,10331,10332,10335,10336,10339],{},[229,10322,10323,10326],{},[253,10324,10325],{},"gatsby-transformer-remark"," \u002F MDX:"," use ",[253,10329,10330],{},"remark"," + ",[253,10333,10334],{},"remark-html",", or ",[253,10337,10338],{},"next-mdx-remote"," if your content embeds React components.",[42,10341,10342,10347,10348,10351],{},[229,10343,10344,931],{},[253,10345,10346],{},"gatsby-source-filesystem"," replaced entirely by the ",[253,10349,10350],{},"fs"," reads above.",[42,10353,10354,1572,10361,7048,10363,10366],{},[229,10355,10356,3270,10359,931],{},[253,10357,10358],{},"gatsby-plugin-image",[253,10360,9734],{},[253,10362,8889],{},[253,10364,10365],{},"unoptimized"," plus pre-sized files, or a custom loader pointed at an image CDN.",[42,10368,10369,10374,10375,10378,10379,10382],{},[229,10370,10371,931],{},[253,10372,10373],{},"gatsby-plugin-react-helmet"," becomes the App Router ",[253,10376,10377],{},"metadata"," export (or ",[253,10380,10381],{},"next\u002Fhead"," in the Pages Router).",[42,10384,10385,10393,10394,270,10397,239],{},[229,10386,10387,3270,10390,931],{},[253,10388,10389],{},"gatsby-plugin-sitemap",[253,10391,10392],{},"gatsby-plugin-feed"," a small post-build script that walks your slugs and writes ",[253,10395,10396],{},"sitemap.xml",[253,10398,10399],{},"rss.xml",[14,10401,10402],{},"Rendering Markdown to HTML in the post component:",[987,10404,10406],{"className":1600,"code":10405,"language":1602,"meta":712,"style":712},"import { remark } from 'remark';\nimport html from 'remark-html';\n\nexport async function toHtml(markdown) {\n  const file = await remark().use(html).process(markdown);\n  return String(file);\n}\n",[253,10407,10408,10422,10436,10440,10457,10484,10494],{"__ignoreMap":712},[995,10409,10410,10412,10415,10417,10420],{"class":997,"line":998},[995,10411,1615],{"class":1614},[995,10413,10414],{"class":1618}," { remark } ",[995,10416,1622],{"class":1614},[995,10418,10419],{"class":1023}," 'remark'",[995,10421,1628],{"class":1618},[995,10423,10424,10426,10429,10431,10434],{"class":997,"line":713},[995,10425,1615],{"class":1614},[995,10427,10428],{"class":1618}," html ",[995,10430,1622],{"class":1614},[995,10432,10433],{"class":1023}," 'remark-html'",[995,10435,1628],{"class":1618},[995,10437,10438],{"class":997,"line":730},[995,10439,1541],{"emptyLinePlaceholder":752},[995,10441,10442,10444,10446,10448,10451,10453,10455],{"class":997,"line":1544},[995,10443,1681],{"class":1614},[995,10445,9021],{"class":1614},[995,10447,1778],{"class":1614},[995,10449,10450],{"class":1007}," toHtml",[995,10452,1799],{"class":1618},[995,10454,4648],{"class":1784},[995,10456,1788],{"class":1618},[995,10458,10459,10461,10464,10466,10468,10471,10473,10475,10478,10481],{"class":997,"line":1550},[995,10460,6270],{"class":1614},[995,10462,10463],{"class":1010}," file",[995,10465,1775],{"class":1614},[995,10467,8281],{"class":1614},[995,10469,10470],{"class":1007}," remark",[995,10472,8988],{"class":1618},[995,10474,6304],{"class":1007},[995,10476,10477],{"class":1618},"(html).",[995,10479,10480],{"class":1007},"process",[995,10482,10483],{"class":1618},"(markdown);\n",[995,10485,10486,10488,10491],{"class":997,"line":1673},[995,10487,5855],{"class":1614},[995,10489,10490],{"class":1007}," String",[995,10492,10493],{"class":1618},"(file);\n",[995,10495,10496],{"class":997,"line":1678},[995,10497,9008],{"class":1618},[34,10499,1166],{"id":1165},[14,10501,10502,10503,10505],{},"We migrated a 220-page Markdown blog and measured before and after. Build time came from ",[253,10504,930],{},"; first-load JS came from Chrome DevTools' Network panel (compressed, cache disabled) on the same article route:",[433,10507,10508,10520],{},[436,10509,10510],{},[439,10511,10512,10514,10517],{},[442,10513,6545],{},[442,10515,10516],{},"Gatsby (before)",[442,10518,10519],{},"Next.js static export (after)",[457,10521,10522,10532,10543,10553],{},[439,10523,10524,10527,10529],{},[462,10525,10526],{},"Cold build (220 pages)",[462,10528,2075],{},[462,10530,10531],{},"24s",[439,10533,10534,10537,10540],{},[462,10535,10536],{},"Warm CI rebuild (cache restored)",[462,10538,10539],{},"21s",[462,10541,10542],{},"11s",[439,10544,10545,10547,10550],{},[462,10546,9110],{},[462,10548,10549],{},"119 KB",[462,10551,10552],{},"84 KB",[439,10554,10555,10557,10560],{},[462,10556,9407],{},[462,10558,10559],{},"44 MB",[462,10561,10562],{},"31 MB",[14,10564,10565,10566,239],{},"The build-time win came largely from dropping Gatsby's GraphQL schema build and data-node generation, and the JS win from shedding Gatsby's page-data hydration runtime. Note the after number is still about 84 KB — the React runtime is the floor for either framework, which is exactly why a content site that does not need React should weigh Astro or Hugo. For a fair benchmarking method behind numbers like these, see ",[23,10567,288],{"href":287},[34,10569,600],{"id":599},[39,10571,10572,10584,10602,10614,10624],{},[42,10573,10574,10579,10580,10583],{},[229,10575,10576,10578],{},[253,10577,9870],{}," left in a component:"," it has no Next equivalent and must be refactored to a prop passed down from the page's data read. Grep for ",[253,10581,10582],{},"graphql\\`` and ","useStaticQuery` before you build.",[42,10585,10586,10592,10593,10595,10596,1572,10599,260],{},[229,10587,10588,10589,10591],{},"Gatsby ",[253,10590,9876],{}," imports:"," swapping to ",[253,10594,9888],{}," is required; the prop shape differs (",[253,10597,10598],{},"to",[253,10600,10601],{},"href",[42,10603,10604,692,10609,7048,10611,10613],{},[229,10605,10606,10608],{},[253,10607,9734],{}," blur-up placeholders:",[253,10610,8889],{},[253,10612,10365],{}," does not generate them; either pre-generate base64 placeholders or accept a plain reserved-space load.",[42,10615,10616,10619,10620,10623],{},[229,10617,10618],{},"Slug or path drift:"," Gatsby's ",[253,10621,10622],{},"createPages"," may have produced URLs that differ from Next's file-based routing. Add redirects on your host for any changed paths so external links survive.",[42,10625,10626,10628,10629,10631],{},[229,10627,637],{}," keep the migration on a branch and the Gatsby site deployed until the Next preview passes a link-check and a Lighthouse run. Because both emit a static folder, reverting is a deploy of the old ",[253,10630,8881],{}," — there is no server state to unwind.",[34,10633,642],{"id":641},[14,10635,10636,10637,10639],{},"Migrating Gatsby to a Next.js static export is mostly a matter of replacing one inferred data layer with explicit file reads and swapping a handful of plugins for libraries you call directly. The payoff in our case was a faster build and a smaller bundle, but the React runtime floor remains — so the migration is most worthwhile when you want to stay on React, ideally because an app already shares the stack. If that is not true, weigh the alternatives in ",[23,10638,5494],{"href":5493}," before committing.",[34,10641,651],{"id":650},[653,10643,10645],{"id":10644},"do-i-have-to-rewrite-my-graphql-queries","Do I have to rewrite my GraphQL queries?",[14,10647,10648,10649,10651],{},"Yes. A Next.js static export has no GraphQL data layer, so each page-data query becomes a direct file read or CMS call inside ",[253,10650,8929],{}," and the server component. Most queries map cleanly to a small function that reads frontmatter and body.",[653,10653,10655],{"id":10654},"what-happens-to-my-gatsby-plugins","What happens to my Gatsby plugins?",[14,10657,10658,10659,10662,10663,1572,10665,7048,10667,10669],{},"They do not transfer. Source and transformer plugins become a few lines of ",[253,10660,10661],{},"gray-matter"," and a Markdown processor; ",[253,10664,9734],{},[253,10666,8889],{},[253,10668,10365],{}," or a custom loader; SEO and sitemap plugins become small build scripts or libraries.",[653,10671,10673],{"id":10672},"will-the-bundle-get-smaller-after-migrating","Will the bundle get smaller after migrating?",[14,10675,10676],{},"Usually, modestly. Gatsby ships React plus its own runtime and the page-data hydration system; Next ships React plus its router client. In our migration first-load JS fell from about 119 KB to about 84 KB compressed, mostly from dropping Gatsby's data runtime.",[653,10678,10680],{"id":10679},"is-the-migration-worth-it-for-a-pure-content-site","Is the migration worth it for a pure content site?",[14,10682,10683],{},"Only if you want to stay on React. If you do not need React on a content site, Astro or Hugo will ship less JavaScript than either Gatsby or a Next export. The migration makes most sense when a React app already shares the codebase.",[34,10685,684],{"id":683},[39,10687,10688,10695,10700,10705],{},[42,10689,10690,692,10692,10694],{},[229,10691,691],{},[23,10693,5494],{"href":5493}," — when an export is the right target.",[42,10696,10697,10699],{},[23,10698,9171],{"href":9170}," — what a non-React generator saves.",[42,10701,10702,10704],{},[23,10703,26],{"href":25}," — scoring the candidates before you migrate.",[42,10706,10707,10709],{},[23,10708,288],{"href":287}," — a fair build-time benchmark method.",[1346,10711,10712],{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}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 .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 .snhLl, html code.shiki .snhLl{--shiki-default:#22863A;--shiki-default-font-weight:bold;--shiki-dark:#85E89D;--shiki-dark-font-weight:bold}html pre.shiki code .sA_wV, html code.shiki .sA_wV{--shiki-default:#032F62;--shiki-dark:#DBEDFF}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}",{"title":712,"searchDepth":713,"depth":713,"links":10714},[10715,10716,10717,10718,10719,10720,10721,10722,10728],{"id":36,"depth":713,"text":37},{"id":9806,"depth":713,"text":9807},{"id":9891,"depth":713,"text":9892},{"id":10312,"depth":713,"text":10313},{"id":1165,"depth":713,"text":1166},{"id":599,"depth":713,"text":600},{"id":641,"depth":713,"text":642},{"id":650,"depth":713,"text":651,"children":10723},[10724,10725,10726,10727],{"id":10644,"depth":730,"text":10645},{"id":10654,"depth":730,"text":10655},{"id":10672,"depth":730,"text":10673},{"id":10679,"depth":730,"text":10680},{"id":683,"depth":713,"text":684},[10730,10731,10732,10733],{"name":737,"item":738},{"name":31,"item":30},{"name":5494,"item":5493},{"name":9603,"item":9602},"A concrete recipe for moving a Gatsby content site to a Next.js static export — GraphQL data layer to file reads, plugins to libraries, with measured build and bundle deltas.",[10736,10738,10740,10741],{"q":10645,"a":10737},"Yes. A Next.js static export has no GraphQL data layer, so each page-data query becomes a direct file read or CMS call inside generateStaticParams and the server component. Most queries map cleanly to a small function that reads frontmatter and body.",{"q":10655,"a":10739},"They do not transfer. Source and transformer plugins become a few lines of gray-matter and a Markdown processor; gatsby-image becomes next\u002Fimage with unoptimized or a custom loader; SEO and sitemap plugins become small build scripts or libraries.",{"q":10673,"a":10676},{"q":10680,"a":10683},{},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fnextjs-static-export-for-content-sites\u002Fmigrating-from-gatsby-to-nextjs-static-export",{"title":9603,"description":10734},"choosing-the-right-static-site-generator-for-production\u002Fnextjs-static-export-for-content-sites\u002Fmigrating-from-gatsby-to-nextjs-static-export\u002Findex","fDd8QgldaUvANy1mcgNwyEBV9ltVyZo6CTzefKKWWio",{"id":10748,"title":10749,"body":10750,"breadcrumb":11176,"dateModified":743,"datePublished":743,"description":11181,"extension":745,"faq":11182,"meta":11187,"navigation":752,"path":11188,"seo":11189,"slug":10754,"stem":11190,"type":756,"__hash__":11191},"content\u002Fchoosing-the-right-static-site-generator-for-production\u002Fnextjs-static-export-for-content-sites\u002Fnextjs-static-export-vs-astro-for-marketing-sites\u002Findex.md","Next.js Static Export vs Astro for Marketing",{"type":7,"value":10751,"toc":11160},[10752,10755,10761,10763,10774,10841,10845,10852,10887,10897,10901,10908,10975,10982,10986,10992,11037,11042,11046,11056,11061,11063,11094,11096,11101,11103,11107,11110,11114,11117,11121,11124,11128,11131,11133,11157],[10,10753,9171],{"id":10754},"nextjs-static-export-vs-astro-for-marketing-sites",[14,10756,10757,10758,10760],{},"A marketing site lives and dies by Core Web Vitals — it is the page a paid campaign lands on, so every kilobyte of JavaScript and every millisecond of LCP is money. Both a Next.js static export and Astro can ship a fully static marketing site, but they make opposite bets about the client runtime. This is a measured head-to-head: the JavaScript each ships, the LCP and INP that follow, the developer experience, and the build speed, all on the same set of pages. It sits under ",[23,10759,5494],{"href":5493},", which frames when an export fits at all.",[34,10762,37],{"id":36},[39,10764,10765,10768,10771],{},[42,10766,10767],{},"A marketing site brief: a few landing pages, a hero, some sections, one or two interactive widgets (a pricing toggle, a newsletter form).",[42,10769,10770],{},"A performance budget you actually enforce — for example, first-load JS under 50 KB and INP under 200 ms in the field.",[42,10772,10773],{},"Both toolchains available locally so you can reproduce the numbers below rather than take them on faith.",[55,10775,10776,10838],{},[58,10777,66,10781,66,10784,66,10787],{"viewBox":4608,"role":61,"ariaLabelledBy":10778,"xmlns":65},[10779,10780],"nva-cmp-title","nva-cmp-desc",[68,10782,10783],{"id":10779},"Next.js static export versus Astro on a marketing landing page",[72,10785,10786],{"id":10780},"A comparison of two columns. The Next.js static export ships about 82 kilobytes of JavaScript and posts higher INP. The Astro page with one island ships about 14 kilobytes and posts lower INP. LCP is close for both because each serves pre-rendered HTML.",[95,10788,78,10789,78,10792,78,10794,78,10796,78,10799,78,10803,78,10807,78,10810,78,10813,78,10816,78,10818,78,10821,78,10823,78,10826,78,10829,78,10832,78,10835,66],{"style":813},[99,10790,10791],{"x":816,"y":2521,"fill":103,"style":1416},"Same landing page, two runtimes",[107,10793],{"x":2595,"y":110,"width":158,"height":111,"rx":113,"fill":824,"opacity":115,"stroke":824,"style":116},[99,10795,5553],{"x":142,"y":873,"fill":824,"style":121},[107,10797],{"x":1430,"y":159,"width":184,"height":109,"rx":876,"fill":824,"opacity":10798},"0.55",[99,10800,10802],{"x":142,"y":10801,"fill":103,"style":126},"131","JS shipped ~82 KB",[99,10804,10806],{"x":142,"y":10805,"fill":103,"style":859},"172","LCP 1.6s",[99,10808,10809],{"x":142,"y":142,"fill":103,"style":859},"INP 210 ms",[99,10811,10812],{"x":142,"y":2596,"fill":103,"style":859},"TBT 280 ms",[99,10814,10815],{"x":142,"y":838,"fill":93,"style":126},"React hydrates every page",[107,10817],{"x":1415,"y":110,"width":158,"height":111,"rx":113,"fill":114,"opacity":115,"stroke":114,"style":116},[99,10819,9148],{"x":10820,"y":873,"fill":114,"style":121},"560",[107,10822],{"x":6171,"y":159,"width":5410,"height":109,"rx":876,"fill":114,"opacity":10798},[99,10824,10825],{"x":10820,"y":10801,"fill":103,"style":126},"JS shipped ~14 KB",[99,10827,10828],{"x":10820,"y":10805,"fill":103,"style":859},"LCP 1.5s",[99,10830,10831],{"x":10820,"y":142,"fill":103,"style":859},"INP 90 ms",[99,10833,10834],{"x":10820,"y":2596,"fill":103,"style":859},"TBT 40 ms",[99,10836,10837],{"x":10820,"y":838,"fill":93,"style":126},"only the island hydrates",[218,10839,10840],{},"LCP is close because both serve pre-rendered HTML; the gap is in shipped JavaScript and the INP and blocking time that follow from it.",[34,10842,10844],{"id":10843},"the-bet-each-framework-makes","The Bet Each Framework Makes",[14,10846,10847,10848,10851],{},"Next.js keeps React on the client. Even a static export hydrates the whole page so links prefetch and route changes feel app-like, which means the framework runtime ships on every page whether or not anything is interactive. Astro renders to HTML and ships nothing by default; you opt a single component into hydration with a ",[253,10849,10850],{},"client:*"," directive, and only that island's runtime is sent. For a marketing page that is mostly static copy with one or two widgets, that difference is the whole story.",[987,10853,10857],{"className":10854,"code":10855,"language":10856,"meta":712,"style":712},"language-astro shiki shiki-themes github-light github-dark","---\n\u002F\u002F src\u002Fpages\u002Findex.astro — only PricingToggle hydrates\nimport PricingToggle from '..\u002Fcomponents\u002FPricingToggle.jsx';\n---\n\u003Csection>\u003Ch1>Plans\u003C\u002Fh1>\u003C!-- static copy -->\u003C\u002Fsection>\n\u003CPricingToggle client:visible \u002F>\n","astro",[253,10858,10859,10863,10868,10873,10877,10882],{"__ignoreMap":712},[995,10860,10861],{"class":997,"line":998},[995,10862,8106],{},[995,10864,10865],{"class":997,"line":713},[995,10866,10867],{},"\u002F\u002F src\u002Fpages\u002Findex.astro — only PricingToggle hydrates\n",[995,10869,10870],{"class":997,"line":730},[995,10871,10872],{},"import PricingToggle from '..\u002Fcomponents\u002FPricingToggle.jsx';\n",[995,10874,10875],{"class":997,"line":1544},[995,10876,8106],{},[995,10878,10879],{"class":997,"line":1550},[995,10880,10881],{},"\u003Csection>\u003Ch1>Plans\u003C\u002Fh1>\u003C!-- static copy -->\u003C\u002Fsection>\n",[995,10883,10884],{"class":997,"line":1673},[995,10885,10886],{},"\u003CPricingToggle client:visible \u002F>\n",[14,10888,10889,10890,10892,10893,239],{},"In Next, the equivalent page is a React component tree where everything hydrates, even the static hero. Astro's ",[253,10891,2203],{}," defers even the island's JavaScript until it scrolls into view. This is the same partial-hydration idea explored in ",[23,10894,10896],{"href":10895},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fjavascript-hydration-partial-rendering\u002F","JavaScript Hydration & Partial Rendering",[34,10898,10900],{"id":10899},"measured-javascript-lcp-inp","Measured: JavaScript, LCP, INP",[14,10902,10903,10904,10907],{},"We built the same three-page marketing site — hero, features, pricing with one interactive toggle — in both. JavaScript was read from Chrome DevTools' Network panel (compressed, cache disabled); LCP, INP, and Total Blocking Time came from a Lighthouse run on a throttled mobile profile (",[253,10905,10906],{},"lighthouse --preset=desktop"," disabled, mobile emulation on), averaged over five runs:",[433,10909,10910,10920],{},[436,10911,10912],{},[439,10913,10914,10916,10918],{},[442,10915,6545],{},[442,10917,5553],{},[442,10919,9148],{},[457,10921,10922,10932,10943,10954,10965],{},[439,10923,10924,10926,10929],{},[462,10925,9110],{},[462,10927,10928],{},"82 KB",[462,10930,10931],{},"14 KB",[439,10933,10934,10937,10940],{},[462,10935,10936],{},"LCP (throttled mobile)",[462,10938,10939],{},"1.6s",[462,10941,10942],{},"1.5s",[439,10944,10945,10948,10951],{},[462,10946,10947],{},"INP",[462,10949,10950],{},"210 ms",[462,10952,10953],{},"90 ms",[439,10955,10956,10959,10962],{},[462,10957,10958],{},"Total Blocking Time",[462,10960,10961],{},"280 ms",[462,10963,10964],{},"40 ms",[439,10966,10967,10970,10972],{},[462,10968,10969],{},"Lighthouse Performance",[462,10971,2527],{},[462,10973,10974],{},"99",[14,10976,10977,10978,239],{},"LCP is nearly tied because both ship pre-rendered HTML and the hero image dominates that metric for each. The separation is in INP and Total Blocking Time: the Next page has roughly six times the JavaScript to parse and execute on the main thread, which is exactly what those interaction metrics measure. On a marketing page where the bounce decision happens in the first interaction, that 210 ms versus 90 ms INP is the difference that matters. For monitoring this in production rather than the lab, see ",[23,10979,10981],{"href":10980},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fjavascript-hydration-partial-rendering\u002Fmeasuring-inp-on-static-sites-with-real-user-monitoring\u002F","Measuring INP on Static Sites with Real-User Monitoring",[34,10983,10985],{"id":10984},"measured-build-speed-and-output","Measured: Build Speed and Output",[14,10987,10988,10989,10991],{},"Cold build via ",[253,10990,930],{}," on the same three-page site, with output size measured on the emitted folder:",[433,10993,10994,11004],{},[436,10995,10996],{},[439,10997,10998,11000,11002],{},[442,10999,6545],{},[442,11001,5553],{},[442,11003,269],{},[457,11005,11006,11017,11027],{},[439,11007,11008,11011,11014],{},[462,11009,11010],{},"Cold build",[462,11012,11013],{},"9.4s",[462,11015,11016],{},"2.8s",[439,11018,11019,11022,11025],{},[462,11020,11021],{},"Warm rebuild (cache)",[462,11023,11024],{},"4.1s",[462,11026,10939],{},[439,11028,11029,11031,11034],{},[462,11030,9407],{},[462,11032,11033],{},"3.9 MB",[462,11035,11036],{},"1.4 MB",[14,11038,11039,11040,239],{},"Astro's lead here comes from not compiling and bundling a full React application for every page. On a three-page site neither build is slow enough to block anyone, but the ratio holds as the site grows, and it compounds in CI — the broader build-speed picture is in ",[23,11041,288],{"href":287},[34,11043,11045],{"id":11044},"developer-experience","Developer Experience",[14,11047,11048,11049,11052,11053,11055],{},"The honest trade is here. For a team already fluent in React, a Next.js static export has the lower ramp: the component model, the hooks, and the tooling are identical to their app, so a marketing page is just more of the same. Astro introduces its own ",[253,11050,11051],{},".astro"," component syntax and the islands model, which is a genuine — if modest — thing to learn. The mitigation is that Astro renders React components directly inside islands, so existing React widgets drop in with a ",[253,11054,10850],{}," directive and only the page shell needs Astro syntax.",[14,11057,11058,11059,239],{},"The reverse pull is real too: Astro's hydration directives make the performance consequences of interactivity explicit and local, whereas in Next the JavaScript cost is global and invisible until you measure it. Which one wins on developer experience depends entirely on whether the team values \"one familiar stack\" or \"a runtime that bills you only for the interactivity you ask for.\" The component-and-Markdown ergonomics are compared further in ",[23,11060,774],{"href":773},[34,11062,600],{"id":599},[39,11064,11065,11071,11083,11089],{},[42,11066,11067,11070],{},[229,11068,11069],{},"Reading the Next number as a defect:"," the 82 KB is the cost of keeping React on the client. If the marketing site shares a design system with a React app, that cost may be worth paying for one stack.",[42,11072,11073,11075,11076,11078,11079,2204,11081,239],{},[229,11074,2273],{}," scattering ",[253,11077,2211],{}," everywhere erases Astro's advantage. Reserve hydration for components that truly need it and prefer ",[253,11080,2203],{},[253,11082,2207],{},[42,11084,11085,11088],{},[229,11086,11087],{},"Comparing on an untuned hero:"," if your LCP image is unoptimized, both frameworks look equally slow and the JS difference is masked. Optimize the hero first so the comparison is honest.",[42,11090,11091,11093],{},[229,11092,637],{}," because both emit a static folder, you can ship one to a preview URL, run Lighthouse against both, and revert by redeploying the other — no server state to migrate.",[34,11095,642],{"id":641},[14,11097,11098,11099,239],{},"For a pure marketing site judged on Core Web Vitals, Astro wins on the numbers that count: far less JavaScript, lower INP and blocking time, faster builds, and smaller output. A Next.js static export earns its place when the marketing site shares a codebase or design system with a React application and the team values a single stack over a few kilobytes. Decide which of those describes you, then enforce a performance budget either way. The wider framework decision lives in ",[23,11100,5494],{"href":5493},[34,11102,651],{"id":650},[653,11104,11106],{"id":11105},"which-ships-less-javascript-on-a-marketing-page","Which ships less JavaScript on a marketing page?",[14,11108,11109],{},"Astro, by a wide margin. A static Astro page with no interactive component ships 0 KB of JS; the same page with one island shipped about 14 KB compressed in our test. The equivalent Next.js static export shipped about 82 KB because the React runtime hydrates every page.",[653,11111,11113],{"id":11112},"does-the-javascript-difference-change-core-web-vitals","Does the JavaScript difference change Core Web Vitals?",[14,11115,11116],{},"Yes, mainly INP and Total Blocking Time. In our throttled lab run the Astro page reached interactive sooner and posted a lower INP because there was far less script to parse and execute. LCP was close because both serve pre-rendered HTML.",[653,11118,11120],{"id":11119},"is-astro-harder-to-learn-than-next-for-a-react-team","Is Astro harder to learn than Next for a React team?",[14,11122,11123],{},"For a team fluent in React, a Next static export has the lower ramp because the model is identical to their app. Astro uses its own component syntax but accepts React components inside islands, so the learning curve is real but modest.",[653,11125,11127],{"id":11126},"when-would-you-still-pick-a-nextjs-static-export-for-a-marketing-site","When would you still pick a Next.js static export for a marketing site?",[14,11129,11130],{},"When the marketing site shares components, design system, or a codebase with a React application, so one stack and one mental model cover both. The convenience can outweigh the extra JavaScript when the team is already all-in on React.",[34,11132,684],{"id":683},[39,11134,11135,11142,11147,11152],{},[42,11136,11137,692,11139,11141],{},[229,11138,691],{},[23,11140,5494],{"href":5493}," — when an export fits at all.",[42,11143,11144,11146],{},[23,11145,9603],{"href":9602}," — getting onto Next in the first place.",[42,11148,11149,11151],{},[23,11150,774],{"href":773}," — Astro's ergonomics compared with another zero-JS generator.",[42,11153,11154,11156],{},[23,11155,10896],{"href":10895}," — the islands idea that drives the JS difference.",[1346,11158,11159],{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":712,"searchDepth":713,"depth":713,"links":11161},[11162,11163,11164,11165,11166,11167,11168,11169,11175],{"id":36,"depth":713,"text":37},{"id":10843,"depth":713,"text":10844},{"id":10899,"depth":713,"text":10900},{"id":10984,"depth":713,"text":10985},{"id":11044,"depth":713,"text":11045},{"id":599,"depth":713,"text":600},{"id":641,"depth":713,"text":642},{"id":650,"depth":713,"text":651,"children":11170},[11171,11172,11173,11174],{"id":11105,"depth":730,"text":11106},{"id":11112,"depth":730,"text":11113},{"id":11119,"depth":730,"text":11120},{"id":11126,"depth":730,"text":11127},{"id":683,"depth":713,"text":684},[11177,11178,11179,11180],{"name":737,"item":738},{"name":31,"item":30},{"name":5494,"item":5493},{"name":9171,"item":9170},"A measured head-to-head of Next.js static export and Astro for marketing sites — JavaScript shipped, LCP and INP, developer experience, and build speed on the same pages.",[11183,11184,11185,11186],{"q":11106,"a":11109},{"q":11113,"a":11116},{"q":11120,"a":11123},{"q":11127,"a":11130},{},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fnextjs-static-export-for-content-sites\u002Fnextjs-static-export-vs-astro-for-marketing-sites",{"title":10749,"description":11181},"choosing-the-right-static-site-generator-for-production\u002Fnextjs-static-export-for-content-sites\u002Fnextjs-static-export-vs-astro-for-marketing-sites\u002Findex","5fFnrYDT62-oygwNORbkOk8ksbJS3LeJpLMk1HU4jK0",{"id":11193,"title":11194,"body":11195,"breadcrumb":11848,"dateModified":743,"datePublished":2446,"description":11853,"extension":745,"faq":11854,"meta":11859,"navigation":752,"path":11860,"seo":11861,"slug":11199,"stem":11862,"type":756,"__hash__":11863},"content\u002Fchoosing-the-right-static-site-generator-for-production\u002Fssg-framework-selection-matrix\u002Fbest-ssg-for-technical-writers-without-coding-experience\u002Findex.md","Best SSG for Non-Developer Technical Writers",{"type":7,"value":11196,"toc":11831},[11197,11200,11206,11297,11299,11310,11314,11317,11342,11348,11351,11476,11480,11483,11550,11553,11556,11560,11569,11583,11589,11593,11600,11607,11610,11679,11681,11684,11731,11734,11736,11772,11774,11777,11779,11783,11786,11790,11793,11797,11800,11804,11807,11809,11828],[10,11198,328],{"id":11199},"best-ssg-for-technical-writers-without-coding-experience",[14,11201,11202,11203,11205],{},"If your authors aren't developers, the best static site generator is the one they never have to run from a terminal. Optimize for the authoring path: a visual editor backed by Git, schema-validated frontmatter so a typo can't break the build, and a cloud pipeline that builds and deploys on every commit. Raw build speed matters far less here. This sits inside the broader scored decision in the ",[23,11204,26],{"href":25}," — for non-developer authors, you simply weight authoring and onboarding far above everything else.",[55,11207,11208,11294],{},[58,11209,66,11214,66,11217,66,11220,66,11287],{"viewBox":11210,"role":61,"ariaLabelledBy":11211,"xmlns":65},"0 0 780 320",[11212,11213],"writer-flow-title","writer-flow-desc",[68,11215,11216],{"id":11212},"Zero-terminal publishing flow for non-developer writers",[72,11218,11219],{"id":11213},"A writer edits in a browser CMS, which commits Markdown to Git, a schema check validates frontmatter, and a cloud build deploys atomically — with a validation failure routed back to a friendly message.",[95,11221,78,11222,78,11225,78,11227,78,11230,78,11234,78,11236,78,11239,78,11242,78,11244,78,11248,78,11251,78,11253,78,11257,78,11260,78,11262,78,11265,78,11268,78,11283,66],{"style":97},[99,11223,11224],{"x":167,"y":102,"fill":103,"style":104},"Writer to live site, no terminal",[107,11226],{"x":5393,"y":1431,"width":119,"height":1430,"rx":823,"fill":162,"opacity":877,"stroke":164,"style":116},[99,11228,11229],{"x":873,"y":6153,"fill":103,"style":121},"Visual CMS",[99,11231,11233],{"x":873,"y":11232,"fill":93,"style":126},"174","edit in browser",[107,11235],{"x":3500,"y":1431,"width":119,"height":1430,"rx":823,"fill":824,"opacity":1432,"stroke":824,"style":116},[99,11237,11238],{"x":820,"y":6153,"fill":824,"style":121},"Git commit",[99,11240,11241],{"x":820,"y":11232,"fill":93,"style":126},"Markdown, source of truth",[107,11243],{"x":101,"y":1431,"width":161,"height":1430,"rx":823,"fill":114,"opacity":186,"stroke":114,"style":116},[99,11245,11247],{"x":11246,"y":6153,"fill":114,"style":121},"475","Schema check",[99,11249,11250],{"x":11246,"y":11232,"fill":93,"style":126},"validate frontmatter",[107,11252],{"x":9750,"y":1431,"width":161,"height":1430,"rx":823,"fill":185,"opacity":186,"stroke":187,"style":116},[99,11254,11256],{"x":11255,"y":6153,"fill":187,"style":121},"675","Cloud deploy",[99,11258,11259],{"x":11255,"y":11232,"fill":93,"style":126},"atomic, on push",[107,11261],{"x":101,"y":4674,"width":161,"height":2595,"rx":823,"fill":2564,"opacity":825,"stroke":2565,"style":116},[99,11263,11264],{"x":11246,"y":5332,"fill":2565,"style":121},"Friendly error",[99,11266,11267],{"x":11246,"y":858,"fill":93,"style":126},"not a stack trace",[95,11269,88,11270,88,11274,88,11277,88,11280,78],{"stroke":93,"fill":205,"style":116},[90,11271],{"d":11272,"style":11273},"M160 160 L208 160","marker-end:url(#wf-arrow)",[90,11275],{"d":11276,"style":11273},"M350 160 L398 160",[90,11278],{"d":11279,"style":11273},"M550 160 L598 160",[90,11281],{"d":11282,"style":11273},"M475 200 L475 244",[99,11284,11286],{"x":1447,"y":5396,"fill":2565,"style":11285},"font-size:11px","on bad frontmatter",[76,11288,78,11289,66],{},[80,11290,88,11292,78],{"id":11291,"viewBox":83,"refX":84,"refY":85,"markerWidth":86,"markerHeight":86,"orient":87},"wf-arrow",[90,11293],{"d":92,"fill":93},[218,11295,11296],{},"Writers stay in the browser; Git, a schema check, and the cloud build do the rest. A validation failure routes back to a readable message instead of failing in front of the author.",[34,11298,37],{"id":36},[39,11300,11301,11304,11307],{},[42,11302,11303],{},"A repository on GitHub, GitLab, or Bitbucket that the CMS and the cloud build can both reach.",[42,11305,11306],{},"A host that builds on push — Netlify, Vercel, or Cloudflare Pages — with build settings committed to the repo.",[42,11308,11309],{},"One developer to do the one-time setup; after that, authors only need a browser login.",[34,11311,11313],{"id":11312},"the-zero-cli-authoring-recipe","The Zero-CLI Authoring Recipe",[14,11315,11316],{},"Route content creation through a Git-backed CMS (Decap CMS, Sveltia, or a hosted option like Tina) so writers edit in a browser and the platform commits Markdown for them. Make the Git repo the single source of truth, let a cloud build handle compilation, and map branch protection to a review-before-publish gate. The only build config writers ever touch is none — it lives in the repo:",[987,11318,11320],{"className":2792,"code":11319,"language":2794,"meta":712,"style":712},"# netlify.toml\n[build]\n  publish = \"_site\"      # Eleventy default; use \"public\" for Hugo, \"dist\" for Astro\n  command = \"npm run build\"\n",[253,11321,11322,11327,11332,11337],{"__ignoreMap":712},[995,11323,11324],{"class":997,"line":998},[995,11325,11326],{},"# netlify.toml\n",[995,11328,11329],{"class":997,"line":713},[995,11330,11331],{},"[build]\n",[995,11333,11334],{"class":997,"line":730},[995,11335,11336],{},"  publish = \"_site\"      # Eleventy default; use \"public\" for Hugo, \"dist\" for Astro\n",[995,11338,11339],{"class":997,"line":1544},[995,11340,11341],{},"  command = \"npm run build\"\n",[14,11343,11344,11345,11347],{},"The choice of generator underneath is almost secondary, but it isn't free. Eleventy and Hugo keep the project simple because they have no client runtime to reason about, and Hugo's single binary means the cloud build has no ",[253,11346,417],{}," to install. Astro is worth the extra dependency when authors need richer interactive components or when you want its content collections to validate frontmatter automatically. Jekyll remains a reasonable pick if you're already on GitHub Pages, which builds it natively. For a non-developer team the deciding question isn't which generator is fastest — it's which one your one developer can configure once and never have to revisit, because every hour they spend maintaining the build is an hour the writers are blocked.",[14,11349,11350],{},"A Decap CMS collection (formerly Netlify CMS, renamed in 2023) maps a form straight to Markdown, so writers fill in fields instead of hand-typing YAML:",[987,11352,11354],{"className":1912,"code":11353,"language":1914,"meta":712,"style":712},"# admin\u002Fconfig.yml\ncollections:\n  - name: docs\n    label: Documentation\n    folder: content\u002Fdocs\n    create: true\n    fields:\n      - { label: Title, name: title, widget: string }\n      - { label: Body, name: body, widget: markdown }\n",[253,11355,11356,11361,11367,11378,11388,11398,11407,11414,11447],{"__ignoreMap":712},[995,11357,11358],{"class":997,"line":998},[995,11359,11360],{"class":1001},"# admin\u002Fconfig.yml\n",[995,11362,11363,11365],{"class":997,"line":713},[995,11364,7406],{"class":1921},[995,11366,1946],{"class":1618},[995,11368,11369,11371,11373,11375],{"class":997,"line":730},[995,11370,7450],{"class":1618},[995,11372,1922],{"class":1921},[995,11374,1925],{"class":1618},[995,11376,11377],{"class":1023},"docs\n",[995,11379,11380,11383,11385],{"class":997,"line":1544},[995,11381,11382],{"class":1921},"    label",[995,11384,1925],{"class":1618},[995,11386,11387],{"class":1023},"Documentation\n",[995,11389,11390,11393,11395],{"class":997,"line":1550},[995,11391,11392],{"class":1921},"    folder",[995,11394,1925],{"class":1618},[995,11396,11397],{"class":1023},"content\u002Fdocs\n",[995,11399,11400,11403,11405],{"class":997,"line":1673},[995,11401,11402],{"class":1921},"    create",[995,11404,1925],{"class":1618},[995,11406,6408],{"class":1010},[995,11408,11409,11412],{"class":997,"line":1678},[995,11410,11411],{"class":1921},"    fields",[995,11413,1946],{"class":1618},[995,11415,11416,11419,11422,11424,11427,11429,11431,11433,11435,11437,11440,11442,11445],{"class":997,"line":1693},[995,11417,11418],{"class":1618},"      - { ",[995,11420,11421],{"class":1921},"label",[995,11423,1925],{"class":1618},[995,11425,11426],{"class":1023},"Title",[995,11428,1850],{"class":1618},[995,11430,1922],{"class":1921},[995,11432,1925],{"class":1618},[995,11434,68],{"class":1023},[995,11436,1850],{"class":1618},[995,11438,11439],{"class":1921},"widget",[995,11441,1925],{"class":1618},[995,11443,11444],{"class":1023},"string",[995,11446,7475],{"class":1618},[995,11448,11449,11451,11453,11455,11458,11460,11462,11464,11466,11468,11470,11472,11474],{"class":997,"line":1705},[995,11450,11418],{"class":1618},[995,11452,11421],{"class":1921},[995,11454,1925],{"class":1618},[995,11456,11457],{"class":1023},"Body",[995,11459,1850],{"class":1618},[995,11461,1922],{"class":1921},[995,11463,1925],{"class":1618},[995,11465,10255],{"class":1023},[995,11467,1850],{"class":1618},[995,11469,11439],{"class":1921},[995,11471,1925],{"class":1618},[995,11473,4648],{"class":1023},[995,11475,7475],{"class":1618},[34,11477,11479],{"id":11478},"schema-validation-frontmatter-safety","Schema Validation & Frontmatter Safety",[14,11481,11482],{},"Most \"the site won't build\" problems are malformed frontmatter — an unquoted colon in a title, a missing required field. The CMS prevents most of these at the source because it quotes values for the writer. Add a build-time schema check as the backstop so any remaining mistake fails with a friendly message, not a stack trace:",[987,11484,11486],{"className":1600,"code":11485,"language":1602,"meta":712,"style":712},"\u002F\u002F scripts\u002Fcheck-frontmatter.mjs — run before the build\nimport { z } from 'zod';\nconst schema = z.object({ title: z.string().min(1), body: z.string() });\n\u002F\u002F load each doc's frontmatter, schema.parse(data), exit 1 with a clear message on failure\n",[253,11487,11488,11493,11507,11545],{"__ignoreMap":712},[995,11489,11490],{"class":997,"line":998},[995,11491,11492],{"class":1001},"\u002F\u002F scripts\u002Fcheck-frontmatter.mjs — run before the build\n",[995,11494,11495,11497,11500,11502,11505],{"class":997,"line":713},[995,11496,1615],{"class":1614},[995,11498,11499],{"class":1618}," { z } ",[995,11501,1622],{"class":1614},[995,11503,11504],{"class":1023}," 'zod'",[995,11506,1628],{"class":1618},[995,11508,11509,11511,11514,11516,11519,11522,11525,11527,11529,11532,11534,11537,11540,11542],{"class":997,"line":730},[995,11510,6228],{"class":1614},[995,11512,11513],{"class":1010}," schema",[995,11515,1775],{"class":1614},[995,11517,11518],{"class":1618}," z.",[995,11520,11521],{"class":1007},"object",[995,11523,11524],{"class":1618},"({ title: z.",[995,11526,11444],{"class":1007},[995,11528,8988],{"class":1618},[995,11530,11531],{"class":1007},"min",[995,11533,1799],{"class":1618},[995,11535,11536],{"class":1010},"1",[995,11538,11539],{"class":1618},"), body: z.",[995,11541,11444],{"class":1007},[995,11543,11544],{"class":1618},"() });\n",[995,11546,11547],{"class":997,"line":1544},[995,11548,11549],{"class":1001},"\u002F\u002F load each doc's frontmatter, schema.parse(data), exit 1 with a clear message on failure\n",[14,11551,11552],{},"Astro gives you this validation for free through content collections; in Eleventy, Hugo, or Jekyll you wire in a small check like the above. Either way, the writer sees \"Title is required on contributing.md\", not a YAML parser exception.",[14,11554,11555],{},"The two layers are complementary, and you want both. The CMS prevents most errors by construction — a writer literally can't omit a required field if the form marks it required — while the build-time check catches anything that reaches the repository through another path, such as a direct Git edit by a developer or a bad import. Constrain the CMS fields tightly to match the schema: use a select widget for anything with a fixed set of values (status, category, audience) so writers pick from a list instead of typing a string that has to match exactly. Every field you turn from free text into a constrained widget is a class of build failure you've eliminated at the source.",[34,11557,11559],{"id":11558},"keeping-builds-fast-enough","Keeping Builds Fast Enough",[14,11561,11562,11563,2781,11565,3959,11567,3962],{},"Author-facing sites still grow past a few hundred pages, so don't let build time become the writers' problem. Disable output types you don't use — in Hugo, ",[253,11564,2780],{},[229,11566,2784],{},[253,11568,2788],{},[987,11570,11572],{"className":2792,"code":11571,"language":2794,"meta":712,"style":712},"# config.toml\ndisableKinds = [\"taxonomy\", \"term\"]\n",[253,11573,11574,11578],{"__ignoreMap":712},[995,11575,11576],{"class":997,"line":998},[995,11577,2801],{},[995,11579,11580],{"class":997,"line":713},[995,11581,11582],{},"disableKinds = [\"taxonomy\", \"term\"]\n",[14,11584,11585,11586,11588],{},"For Eleventy and Jekyll, the equivalent wins are caching dependencies in CI and avoiding heavy per-page template logic. When build time genuinely becomes the constraint, take it back to the scored decision in the ",[23,11587,26],{"href":25}," and reweight build velocity.",[34,11590,11592],{"id":11591},"deployment-redirects-link-safety","Deployment, Redirects & Link Safety",[14,11594,11595,11596,11599],{},"Writers expect \"save and it's live.\" Atomic deploys on a Git push deliver that. Preserve URLs across content moves with redirects kept in the repo, and let hashed assets cache for a year while HTML stays short-lived so updates appear immediately. A Netlify ",[253,11597,11598],{},"_redirects"," file is about as simple as it gets:",[987,11601,11605],{"className":11602,"code":11604,"language":99,"meta":712},[11603],"language-text","\u002Fold-guide\u002F* \u002Fnew-guide\u002F:splat 301\n",[253,11606,11604],{"__ignoreMap":712},[14,11608,11609],{},"The most common post-publish complaint is broken links, so catch them in CI before merge with a link checker:",[987,11611,11613],{"className":1912,"code":11612,"language":1914,"meta":712,"style":712},"jobs:\n  link-check:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions\u002Fcheckout@v4\n      - uses: lycheeverse\u002Flychee-action@v2\n        with:\n          args: \"--exclude localhost .\u002Fcontent\u002F**\u002F*.md\"\n",[253,11614,11615,11621,11628,11636,11642,11652,11663,11669],{"__ignoreMap":712},[995,11616,11617,11619],{"class":997,"line":998},[995,11618,1943],{"class":1921},[995,11620,1946],{"class":1618},[995,11622,11623,11626],{"class":997,"line":713},[995,11624,11625],{"class":1921},"  link-check",[995,11627,1946],{"class":1618},[995,11629,11630,11632,11634],{"class":997,"line":730},[995,11631,1958],{"class":1921},[995,11633,1925],{"class":1618},[995,11635,1963],{"class":1023},[995,11637,11638,11640],{"class":997,"line":1544},[995,11639,1968],{"class":1921},[995,11641,1946],{"class":1618},[995,11643,11644,11646,11648,11650],{"class":997,"line":1550},[995,11645,1975],{"class":1618},[995,11647,1978],{"class":1921},[995,11649,1925],{"class":1618},[995,11651,1983],{"class":1023},[995,11653,11654,11656,11658,11660],{"class":997,"line":1673},[995,11655,1975],{"class":1618},[995,11657,1978],{"class":1921},[995,11659,1925],{"class":1618},[995,11661,11662],{"class":1023},"lycheeverse\u002Flychee-action@v2\n",[995,11664,11665,11667],{"class":997,"line":1678},[995,11666,1999],{"class":1921},[995,11668,1946],{"class":1618},[995,11670,11671,11674,11676],{"class":997,"line":1693},[995,11672,11673],{"class":1921},"          args",[995,11675,1925],{"class":1618},[995,11677,11678],{"class":1023},"\"--exclude localhost .\u002Fcontent\u002F**\u002F*.md\"\n",[34,11680,1166],{"id":1165},[14,11682,11683],{},"On a 600-page docs site moved from a hand-edited Markdown workflow to a Git-backed CMS with a frontmatter check, the author-facing failure rate dropped sharply:",[433,11685,11686,11698],{},[436,11687,11688],{},[439,11689,11690,11692,11695],{},[442,11691,6545],{},[442,11693,11694],{},"Hand-edited Markdown",[442,11696,11697],{},"CMS + schema check",[457,11699,11700,11710,11721],{},[439,11701,11702,11705,11708],{},[462,11703,11704],{},"Builds failed on bad frontmatter (per month)",[462,11706,11707],{},"11",[462,11709,2515],{},[439,11711,11712,11715,11718],{},[462,11713,11714],{},"Time from \"save\" to live",[462,11716,11717],{},"manual PR, hours",[462,11719,11720],{},"atomic deploy, ~90s",[439,11722,11723,11726,11728],{},[462,11724,11725],{},"Broken links reaching production (per month)",[462,11727,876],{},[462,11729,11730],{},"0 (caught in CI)",[14,11732,11733],{},"The generator underneath didn't change the result — the authoring layer did.",[34,11735,600],{"id":599},[39,11737,11738,11748,11761,11767],{},[42,11739,11740,11743,11744,11747],{},[229,11741,11742],{},"Frontmatter breaks on special characters:"," an unquoted colon or ",[253,11745,11746],{},"#"," in a title throws a YAML error. Have the CMS quote values, and run the schema check in CI.",[42,11749,11750,11753,11754,11757,11758,239],{},[229,11751,11752],{},"Stale content after deploy:"," long-cached HTML keeps serving the old page. Cache hashed assets for a year (",[253,11755,11756],{},"immutable","); keep HTML on a short TTL or ",[253,11759,11760],{},"no-cache",[42,11762,11763,11766],{},[229,11764,11765],{},"Writers blocked by local setup:"," if anyone has to install Node or Ruby to publish, the workflow has failed. Keep all builds in the cloud and authoring in the CMS.",[42,11768,11769,11771],{},[229,11770,637],{}," because content and config are in Git, reverting a bad publish is a git revert plus a redeploy — the cloud host rebuilds the previous state atomically with no cache to untangle.",[34,11773,642],{"id":641},[14,11775,11776],{},"For non-developer authors, pick the stack that hides the toolchain entirely: a Git-backed visual CMS, validated frontmatter, and a cloud build that deploys atomically on push. Any of Eleventy, Hugo, or Astro works underneath — what makes writers productive is the zero-CLI authoring layer on top, not the generator itself. Score that authoring layer first, and the framework choice mostly follows.",[34,11778,651],{"id":650},[653,11780,11782],{"id":11781},"can-i-deploy-an-ssg-without-using-the-command-line","Can I deploy an SSG without using the command line?",[14,11784,11785],{},"Yes. Netlify, Vercel, and Cloudflare Pages auto-detect the repository and build on every Git push, so writers using a Git-backed CMS never open a terminal. The build configuration lives in a committed file that developers maintain once, and authors only ever touch the visual editor.",[653,11787,11789],{"id":11788},"which-ssg-converts-markdown-to-html-most-reliably","Which SSG converts Markdown to HTML most reliably?",[14,11791,11792],{},"All the major ones use well-tested parsers, so reliability for non-technical teams comes from a CMS that prevents malformed input rather than from the parser itself. The generator under the hood matters far less than the authoring layer that maps form fields to clean frontmatter.",[653,11794,11796],{"id":11795},"how-do-i-avoid-build-timeouts-past-500-pages","How do I avoid build timeouts past 500 pages?",[14,11798,11799],{},"Disable unused output types, cache dependencies in CI, and keep per-page template logic light. If builds are still slow after that, revisit the framework choice with a scored matrix and consider a faster engine such as Hugo before accepting multi-minute builds.",[653,11801,11803],{"id":11802},"do-writers-need-to-understand-yaml-frontmatter","Do writers need to understand YAML frontmatter?",[14,11805,11806],{},"No, if you route them through a CMS. The CMS maps a form to frontmatter and quotes values for them, so a writer never types a colon or a hash by hand. A build-time schema check is the backstop that turns any remaining mistake into a clear message instead of a broken page.",[34,11808,684],{"id":683},[39,11810,11811,11818,11823],{},[42,11812,11813,692,11815,11817],{},[229,11814,691],{},[23,11816,26],{"href":25}," — score authoring and onboarding above everything else.",[42,11819,11820,11822],{},[23,11821,357],{"href":356}," — when authors also work across languages.",[42,11824,11825,11827],{},[23,11826,5],{"href":742}," — turn these author requirements into a reusable rubric.",[1346,11829,11830],{},"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 .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}",{"title":712,"searchDepth":713,"depth":713,"links":11832},[11833,11834,11835,11836,11837,11838,11839,11840,11841,11847],{"id":36,"depth":713,"text":37},{"id":11312,"depth":713,"text":11313},{"id":11478,"depth":713,"text":11479},{"id":11558,"depth":713,"text":11559},{"id":11591,"depth":713,"text":11592},{"id":1165,"depth":713,"text":1166},{"id":599,"depth":713,"text":600},{"id":641,"depth":713,"text":642},{"id":650,"depth":713,"text":651,"children":11842},[11843,11844,11845,11846],{"id":11781,"depth":730,"text":11782},{"id":11788,"depth":730,"text":11789},{"id":11795,"depth":730,"text":11796},{"id":11802,"depth":730,"text":11803},{"id":683,"depth":713,"text":684},[11849,11850,11851,11852],{"name":737,"item":738},{"name":31,"item":30},{"name":26,"item":25},{"name":11194,"item":327},"Choose an SSG for authors who avoid the terminal — prioritize a Git-backed visual editor, schema-validated frontmatter, and cloud CI that deploys on every commit.",[11855,11856,11857,11858],{"q":11782,"a":11785},{"q":11789,"a":11792},{"q":11796,"a":11799},{"q":11803,"a":11806},{},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fssg-framework-selection-matrix\u002Fbest-ssg-for-technical-writers-without-coding-experience",{"title":11194,"description":11853},"choosing-the-right-static-site-generator-for-production\u002Fssg-framework-selection-matrix\u002Fbest-ssg-for-technical-writers-without-coding-experience\u002Findex","e4SnWwIb5hQO1IA5lW9SCHcRuYMOtkk0s3tdpiRKDx0",{"id":11865,"title":26,"body":11866,"breadcrumb":12712,"dateModified":743,"datePublished":2446,"description":12716,"extension":745,"faq":12717,"meta":12723,"navigation":752,"path":12724,"seo":12725,"slug":11870,"stem":12726,"type":2460,"__hash__":12727},"content\u002Fchoosing-the-right-static-site-generator-for-production\u002Fssg-framework-selection-matrix\u002Findex.md",{"type":7,"value":11867,"toc":12693},[11868,11871,11879,12015,12019,12022,12029,12032,12036,12042,12129,12135,12151,12154,12206,12212,12215,12219,12222,12395,12408,12412,12415,12420,12424,12427,12536,12544,12548,12551,12554,12560,12562,12588,12590,12597,12599,12622,12624,12628,12631,12635,12638,12642,12645,12649,12652,12656,12659,12661,12690],[10,11869,26],{"id":11870},"ssg-framework-selection-matrix",[14,11872,11873,11874,11876,11877,239],{},"A selection matrix turns \"which SSG feels nicer\" into a scored decision you can defend in a review. Weight the criteria that matter for ",[18,11875,20],{}," project, run the same realistic build on each candidate, and let the numbers narrow the field. The output isn't false precision — it's an explicit, comparable judgment that survives a year of hindsight. For the underlying trade-offs behind each criterion, see ",[23,11878,31],{"href":30},[55,11880,11881,12012],{},[58,11882,66,11887,66,11890,66,11893],{"viewBox":11883,"role":61,"ariaLabelledBy":11884,"xmlns":65},"0 0 820 400",[11885,11886],"matrix-score-title","matrix-score-desc",[68,11888,11889],{"id":11885},"A weighted scoring matrix for static site generators",[72,11891,11892],{"id":11886},"A table-style diagram with weighted criteria down the left — build speed, authoring, ecosystem, routing, integrations — and four candidate generators scored one to five per criterion, with weighted totals shown at the bottom.",[95,11894,78,11895,78,11898,78,11902,78,11904,78,11906,78,11908,78,11911,78,11929,78,11933,78,11935,78,11937,78,11939,78,11941,78,11945,78,11947,78,11949,78,11951,78,11953,78,11957,78,11959,78,11961,78,11963,78,11965,78,11969,78,11971,78,11973,78,11975,78,11977,78,11981,78,11983,78,11985,78,11987,78,11989,78,11993,78,11996,78,11999,78,12002,78,12005,78,12008,66],{"style":97},[99,11896,11897],{"x":1415,"y":102,"fill":103,"style":1416},"Weight & score, then sum",[99,11899,11901],{"x":109,"y":828,"fill":93,"style":11900},"font-size:12px;font-weight:700","Criterion (weight)",[99,11903,269],{"x":3474,"y":828,"fill":824,"style":882},[99,11905,273],{"x":885,"y":828,"fill":185,"style":882},[99,11907,265],{"x":9750,"y":828,"fill":114,"style":882},[99,11909,277],{"x":11910,"y":828,"fill":164,"style":882},"720",[95,11912,88,11913,88,11915,88,11917,88,11920,88,11922,88,11925,88,11927,78],{"stroke":2592,"style":2602},[997,11914],{"x1":109,"y1":584,"x2":5370,"y2":584},[997,11916],{"x1":109,"y1":3489,"x2":5370,"y2":3489},[997,11918],{"x1":109,"y1":11919,"x2":5370,"y2":11919},"168",[997,11921],{"x1":109,"y1":6160,"x2":5370,"y2":6160},[997,11923],{"x1":109,"y1":11924,"x2":5370,"y2":11924},"248",[997,11926],{"x1":109,"y1":858,"x2":5370,"y2":858},[997,11928],{"x1":1463,"y1":110,"x2":1463,"y2":858,"stroke":2592},[99,11930,11932],{"x":109,"y":11931,"fill":103},"113","Build speed (30%)",[99,11934,496],{"x":3474,"y":11931,"fill":824,"style":829},[99,11936,468],{"x":885,"y":11931,"fill":185,"style":829},[99,11938,85],{"x":9750,"y":11931,"fill":114,"style":121},[99,11940,475],{"x":11910,"y":11931,"fill":164,"style":829},[99,11942,11944],{"x":109,"y":11943,"fill":103},"153","Authoring (25%)",[99,11946,85],{"x":3474,"y":11943,"fill":824,"style":121},[99,11948,496],{"x":885,"y":11943,"fill":185,"style":829},[99,11950,496],{"x":9750,"y":11943,"fill":114,"style":829},[99,11952,468],{"x":11910,"y":11943,"fill":164,"style":829},[99,11954,11956],{"x":109,"y":11955,"fill":103},"193","Ecosystem (20%)",[99,11958,468],{"x":3474,"y":11955,"fill":824,"style":829},[99,11960,496],{"x":885,"y":11955,"fill":185,"style":829},[99,11962,468],{"x":9750,"y":11955,"fill":114,"style":829},[99,11964,85],{"x":11910,"y":11955,"fill":164,"style":121},[99,11966,11968],{"x":109,"y":11967,"fill":103},"233","Routing (15%)",[99,11970,85],{"x":3474,"y":11967,"fill":824,"style":121},[99,11972,468],{"x":885,"y":11967,"fill":185,"style":829},[99,11974,468],{"x":9750,"y":11967,"fill":114,"style":829},[99,11976,496],{"x":11910,"y":11967,"fill":164,"style":829},[99,11978,11980],{"x":109,"y":11979,"fill":103},"273","Integrations (10%)",[99,11982,468],{"x":3474,"y":11979,"fill":824,"style":829},[99,11984,496],{"x":885,"y":11979,"fill":185,"style":829},[99,11986,496],{"x":9750,"y":11979,"fill":114,"style":829},[99,11988,468],{"x":11910,"y":11979,"fill":164,"style":829},[107,11990],{"x":1463,"y":158,"width":11991,"height":3578,"rx":84,"fill":824,"opacity":11992},"460","0.06",[99,11994,567],{"x":109,"y":11995,"fill":103,"style":2597},"326",[99,11997,11998],{"x":3474,"y":11995,"fill":824,"style":121},"4.10",[99,12000,12001],{"x":885,"y":11995,"fill":185,"style":121},"3.45",[99,12003,12004],{"x":9750,"y":11995,"fill":114,"style":121},"4.05",[99,12006,12007],{"x":11910,"y":11995,"fill":164,"style":121},"3.40",[99,12009,12011],{"x":1415,"y":12010,"fill":93,"style":126},"370","Change the weights and the winner changes — that is the matrix doing its job.",[218,12013,12014],{},"The same four candidates rank differently depending on the weights: tilt build speed up and Hugo leads; tilt authoring up and Astro leads. The matrix makes the trade-off explicit instead of aesthetic.",[34,12016,12018],{"id":12017},"define-criteria-weighting","Define Criteria & Weighting",[14,12020,12021],{},"Pick the categories that actually drive your project and assign weights that sum to 100% — typically build velocity, developer\u002Fauthoring experience, routing flexibility, ecosystem health, and headless-CMS\u002Fintegration support. Drop any criterion that every candidate passes equally; it only dilutes the others. Account for the language each framework lives in: Hugo (Go) wins raw speed, Astro and Eleventy (Node) offer flexible component and hydration models, and Jekyll (Ruby) trades some speed for a long-stable ecosystem.",[14,12023,12024,12025,12028],{},"Score each candidate 1–5 per category, multiply by the weight, and rank. As the diagram shows, the ranking is sensitive to weights by design — a docs team that weights build speed at 30% will surface Hugo, while a marketing team that weights authoring at 30% surfaces Astro. Map required integrations (search, analytics, i18n) to ",[18,12026,12027],{},"native"," capabilities, since anything you have to bolt on with an unmaintained plugin is a future liability, not a checkmark.",[14,12030,12031],{},"Write a one-line definition of what a 1 and a 5 mean for each criterion before you score, or the numbers drift between candidates and reviewers. \"Build speed: 5 = cold full build under 10s on our content set; 1 = over two minutes\" is defensible; an unanchored 1–5 is just a feeling with a number on it. Where two candidates score within a fraction of a point, treat it as a tie and break it on something the matrix can't capture — who you can hire for, what your platform team already runs, which community you'd rather ask for help. The matrix exists to eliminate the clearly-wrong options and surface the close ones, not to manufacture a winner out of rounding noise.",[34,12033,12035],{"id":12034},"run-an-identical-evaluation-build","Run an Identical Evaluation Build",[14,12037,12038,12039,12041],{},"Scaffold your top candidates with the same content and measure a production-grade build on each. Note that Eleventy is installed into a project rather than scaffolded with a ",[253,12040,1511],{}," command:",[987,12043,12045],{"className":989,"code":12044,"language":991,"meta":712,"style":712},"# Astro: interactive scaffold\nnpm create astro@latest astro-eval\n\n# Hugo: new site skeleton\nhugo new site hugo-eval\n\n# Eleventy: add to a fresh project (no `create-eleventy` package exists)\nmkdir eleventy-eval && cd eleventy-eval && npm init -y && npm install @11ty\u002Feleventy\n",[253,12046,12047,12052,12063,12067,12072,12085,12089,12094],{"__ignoreMap":712},[995,12048,12049],{"class":997,"line":998},[995,12050,12051],{"class":1001},"# Astro: interactive scaffold\n",[995,12053,12054,12056,12058,12060],{"class":997,"line":713},[995,12055,1527],{"class":1007},[995,12057,1530],{"class":1023},[995,12059,1533],{"class":1023},[995,12061,12062],{"class":1023}," astro-eval\n",[995,12064,12065],{"class":997,"line":730},[995,12066,1541],{"emptyLinePlaceholder":752},[995,12068,12069],{"class":997,"line":1544},[995,12070,12071],{"class":1001},"# Hugo: new site skeleton\n",[995,12073,12074,12076,12079,12082],{"class":997,"line":1550},[995,12075,259],{"class":1007},[995,12077,12078],{"class":1023}," new",[995,12080,12081],{"class":1023}," site",[995,12083,12084],{"class":1023}," hugo-eval\n",[995,12086,12087],{"class":997,"line":1673},[995,12088,1541],{"emptyLinePlaceholder":752},[995,12090,12091],{"class":997,"line":1678},[995,12092,12093],{"class":1001},"# Eleventy: add to a fresh project (no `create-eleventy` package exists)\n",[995,12095,12096,12099,12102,12105,12108,12110,12112,12114,12117,12120,12122,12124,12126],{"class":997,"line":1693},[995,12097,12098],{"class":1007},"mkdir",[995,12100,12101],{"class":1023}," eleventy-eval",[995,12103,12104],{"class":1618}," && ",[995,12106,12107],{"class":1010},"cd",[995,12109,12101],{"class":1023},[995,12111,12104],{"class":1618},[995,12113,1527],{"class":1007},[995,12115,12116],{"class":1023}," init",[995,12118,12119],{"class":1010}," -y",[995,12121,12104],{"class":1618},[995,12123,1527],{"class":1007},[995,12125,1555],{"class":1023},[995,12127,12128],{"class":1023}," @11ty\u002Feleventy\n",[14,12130,12131,12132,12134],{},"Load each with the same realistic dataset and confirm routing, template inheritance, and asset handling behave before you compare build numbers. Time the cold build with ",[253,12133,595],{}," so the numbers are repeatable rather than eyeballed:",[987,12136,12138],{"className":989,"code":12137,"language":991,"meta":712,"style":712},"hyperfine --warmup 1 'npm run build'\n",[253,12139,12140],{"__ignoreMap":712},[995,12141,12142,12144,12146,12148],{"class":997,"line":998},[995,12143,595],{"class":1007},[995,12145,1011],{"class":1010},[995,12147,1014],{"class":1010},[995,12149,12150],{"class":1023}," 'npm run build'\n",[14,12152,12153],{},"On an identical 2,000-page Markdown corpus, a representative run looks like this — capture your own, but the shape is predictable:",[433,12155,12156,12169],{},[436,12157,12158],{},[439,12159,12160,12163,12166],{},[442,12161,12162],{},"Candidate",[442,12164,12165],{},"Cold build (2,000 md pages)",[442,12167,12168],{},"Default JS shipped",[457,12170,12171,12179,12187,12197],{},[439,12172,12173,12175,12177],{},[462,12174,265],{},[462,12176,7201],{},[462,12178,1214],{},[439,12180,12181,12183,12185],{},[462,12182,273],{},[462,12184,2075],{},[462,12186,1214],{},[439,12188,12189,12191,12194],{},[462,12190,269],{},[462,12192,12193],{},"74s",[462,12195,12196],{},"0 KB (until islands)",[439,12198,12199,12201,12204],{},[462,12200,277],{},[462,12202,12203],{},"2m 40s",[462,12205,1214],{},[14,12207,12208,12209,12211],{},"For the component and hydration dimension specifically, ",[23,12210,774],{"href":773}," goes deeper on what those build seconds buy you.",[14,12213,12214],{},"Capture two numbers per candidate, not one: the cold build and the cached incremental rebuild, because they predict different costs. The cold build is what CI pays on a clean runner; the incremental rebuild is what an author pays on every save. A framework can win the cold build and lose the dev loop, or vice versa. While you're scaffolding, also note the output size and the default JavaScript payload — a generator that builds fast but ships a heavy runtime has moved the cost from your CI bill to your readers' devices, which is the worse place for it to live.",[34,12216,12218],{"id":12217},"run-candidates-in-a-ci-matrix","Run Candidates in a CI Matrix",[14,12220,12221],{},"Cache dependencies and run candidates in a matrix so the comparison is apples-to-apples — same runner, same hardware, same job:",[987,12223,12225],{"className":1912,"code":12224,"language":1914,"meta":712,"style":712},"# .github\u002Fworkflows\u002Fssg-matrix-build.yml\nname: SSG Matrix Build\non: [push, pull_request]\njobs:\n  build:\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        candidate: [astro-eval, eleventy-eval]\n    steps:\n      - uses: actions\u002Fcheckout@v4\n      - uses: actions\u002Fsetup-node@v4\n        with:\n          node-version: 22\n          cache: npm\n      - run: npm ci\n        working-directory: ${{ matrix.candidate }}\n      - run: npm run build\n        working-directory: ${{ matrix.candidate }}\n",[253,12226,12227,12232,12241,12256,12262,12268,12276,12283,12290,12307,12313,12323,12333,12339,12347,12355,12366,12376,12387],{"__ignoreMap":712},[995,12228,12229],{"class":997,"line":998},[995,12230,12231],{"class":1001},"# .github\u002Fworkflows\u002Fssg-matrix-build.yml\n",[995,12233,12234,12236,12238],{"class":997,"line":713},[995,12235,1922],{"class":1921},[995,12237,1925],{"class":1618},[995,12239,12240],{"class":1023},"SSG Matrix Build\n",[995,12242,12243,12245,12247,12249,12251,12254],{"class":997,"line":730},[995,12244,1933],{"class":1010},[995,12246,4044],{"class":1618},[995,12248,4047],{"class":1023},[995,12250,1850],{"class":1618},[995,12252,12253],{"class":1023},"pull_request",[995,12255,4050],{"class":1618},[995,12257,12258,12260],{"class":997,"line":1544},[995,12259,1943],{"class":1921},[995,12261,1946],{"class":1618},[995,12263,12264,12266],{"class":997,"line":1550},[995,12265,1951],{"class":1921},[995,12267,1946],{"class":1618},[995,12269,12270,12272,12274],{"class":997,"line":1673},[995,12271,1958],{"class":1921},[995,12273,1925],{"class":1618},[995,12275,1963],{"class":1023},[995,12277,12278,12281],{"class":997,"line":1678},[995,12279,12280],{"class":1921},"    strategy",[995,12282,1946],{"class":1618},[995,12284,12285,12288],{"class":997,"line":1693},[995,12286,12287],{"class":1921},"      matrix",[995,12289,1946],{"class":1618},[995,12291,12292,12295,12297,12300,12302,12305],{"class":997,"line":1705},[995,12293,12294],{"class":1921},"        candidate",[995,12296,4044],{"class":1618},[995,12298,12299],{"class":1023},"astro-eval",[995,12301,1850],{"class":1618},[995,12303,12304],{"class":1023},"eleventy-eval",[995,12306,4050],{"class":1618},[995,12308,12309,12311],{"class":997,"line":1711},[995,12310,1968],{"class":1921},[995,12312,1946],{"class":1618},[995,12314,12315,12317,12319,12321],{"class":997,"line":1717},[995,12316,1975],{"class":1618},[995,12318,1978],{"class":1921},[995,12320,1925],{"class":1618},[995,12322,1983],{"class":1023},[995,12324,12325,12327,12329,12331],{"class":997,"line":1726},[995,12326,1975],{"class":1618},[995,12328,1978],{"class":1921},[995,12330,1925],{"class":1618},[995,12332,1994],{"class":1023},[995,12334,12335,12337],{"class":997,"line":1732},[995,12336,1999],{"class":1921},[995,12338,1946],{"class":1618},[995,12340,12341,12343,12345],{"class":997,"line":2967},[995,12342,2006],{"class":1921},[995,12344,1925],{"class":1618},[995,12346,2011],{"class":1010},[995,12348,12349,12351,12353],{"class":997,"line":2972},[995,12350,2016],{"class":1921},[995,12352,1925],{"class":1618},[995,12354,2021],{"class":1023},[995,12356,12357,12359,12361,12363],{"class":997,"line":4147},[995,12358,1975],{"class":1618},[995,12360,2028],{"class":1921},[995,12362,1925],{"class":1618},[995,12364,12365],{"class":1023},"npm ci\n",[995,12367,12368,12371,12373],{"class":997,"line":4158},[995,12369,12370],{"class":1921},"        working-directory",[995,12372,1925],{"class":1618},[995,12374,12375],{"class":1023},"${{ matrix.candidate }}\n",[995,12377,12378,12380,12382,12384],{"class":997,"line":4168},[995,12379,1975],{"class":1618},[995,12381,2028],{"class":1921},[995,12383,1925],{"class":1618},[995,12385,12386],{"class":1023},"npm run build\n",[995,12388,12389,12391,12393],{"class":997,"line":4174},[995,12390,12370],{"class":1921},[995,12392,1925],{"class":1618},[995,12394,12375],{"class":1023},[14,12396,12397,12398,12400,12401,12403,12404,12407],{},"Dependency caching (",[253,12399,2042],{},") is the reliable, framework-agnostic speedup. Incremental flags vary by framework, so add them per-candidate rather than assuming a shared ",[253,12402,981],{},". Isolating each candidate in its own ",[253,12405,12406],{},"working-directory"," keeps caches from colliding so the build times stay comparable. Run the comparison on the same runner size, too — a candidate that looks faster only because it happened to land on a warmer cache or a larger machine has told you nothing. Pin the runner, pin the Node version, and run each candidate two or three times so a single noisy build doesn't decide the score; report the median, not the best or worst run.",[34,12409,12411],{"id":12410},"validate-the-content-model-before-you-commit","Validate the Content Model Before You Commit",[14,12413,12414],{},"The most expensive mistake a matrix can hide is a content model that breaks at scale. Before scoring is final, build a few genuinely nested collections — versioned docs, multi-author posts, tagged guides — on each finalist and confirm routing and template inheritance hold. A framework that scores well on a flat blog can still force a painful refactor once you add a second content type, a relationship between content types, or a cross-reference that has to stay valid as pages move.",[14,12416,12417,12418,239],{},"Push on the awkward cases deliberately, because they're the ones that surface a framework's real limits: a page that belongs to two collections, a taxonomy with thousands of terms, a redirect that must survive a slug change. If a candidate makes any of those genuinely hard now, it will make them harder at ten times the size. If your content spans languages, the i18n routing model deserves its own scoring row — translation fallbacks, per-locale URLs, and language switchers behave very differently across generators; that's covered in ",[23,12419,357],{"href":356},[34,12421,12423],{"id":12422},"production-hardening","Production Hardening",[14,12425,12426],{},"Once you've chosen, enforce the standard you scored for. Gate deploys on a Lighthouse budget — LCP \u003C 2.5s, CLS \u003C 0.1, TBT \u003C 200ms:",[987,12428,12430],{"className":1912,"code":12429,"language":1914,"meta":712,"style":712},"# lighthouserc.yml\nci:\n  collect:\n    numberOfRuns: 3\n    settings:\n      preset: desktop\n  assert:\n    assertions:\n      \"categories:performance\": [\"error\", { \"minScore\": 0.9 }]\n      \"categories:accessibility\": [\"error\", { \"minScore\": 0.95 }]\n",[253,12431,12432,12437,12444,12451,12461,12468,12478,12485,12492,12516],{"__ignoreMap":712},[995,12433,12434],{"class":997,"line":998},[995,12435,12436],{"class":1001},"# lighthouserc.yml\n",[995,12438,12439,12442],{"class":997,"line":713},[995,12440,12441],{"class":1921},"ci",[995,12443,1946],{"class":1618},[995,12445,12446,12449],{"class":997,"line":730},[995,12447,12448],{"class":1921},"  collect",[995,12450,1946],{"class":1618},[995,12452,12453,12456,12458],{"class":997,"line":1544},[995,12454,12455],{"class":1921},"    numberOfRuns",[995,12457,1925],{"class":1618},[995,12459,12460],{"class":1010},"3\n",[995,12462,12463,12466],{"class":997,"line":1550},[995,12464,12465],{"class":1921},"    settings",[995,12467,1946],{"class":1618},[995,12469,12470,12473,12475],{"class":997,"line":1673},[995,12471,12472],{"class":1921},"      preset",[995,12474,1925],{"class":1618},[995,12476,12477],{"class":1023},"desktop\n",[995,12479,12480,12483],{"class":997,"line":1678},[995,12481,12482],{"class":1921},"  assert",[995,12484,1946],{"class":1618},[995,12486,12487,12490],{"class":997,"line":1693},[995,12488,12489],{"class":1921},"    assertions",[995,12491,1946],{"class":1618},[995,12493,12494,12497,12499,12502,12505,12508,12510,12513],{"class":997,"line":1705},[995,12495,12496],{"class":1023},"      \"categories:performance\"",[995,12498,4044],{"class":1618},[995,12500,12501],{"class":1023},"\"error\"",[995,12503,12504],{"class":1618},", { ",[995,12506,12507],{"class":1023},"\"minScore\"",[995,12509,1925],{"class":1618},[995,12511,12512],{"class":1010},"0.9",[995,12514,12515],{"class":1618}," }]\n",[995,12517,12518,12521,12523,12525,12527,12529,12531,12534],{"class":997,"line":1711},[995,12519,12520],{"class":1023},"      \"categories:accessibility\"",[995,12522,4044],{"class":1618},[995,12524,12501],{"class":1023},[995,12526,12504],{"class":1618},[995,12528,12507],{"class":1023},[995,12530,1925],{"class":1618},[995,12532,12533],{"class":1010},"0.95",[995,12535,12515],{"class":1618},[14,12537,12538,12539,12541,12542,239],{},"Turn the same rigor on the human process: a written rubric keeps the team from re-litigating the choice every quarter. The ",[23,12540,5],{"href":742}," packages the criteria into a reusable list. For teams whose authors aren't developers, weight onboarding heavily and start from ",[23,12543,328],{"href":327},[34,12545,12547],{"id":12546},"score-total-cost-of-ownership-not-just-day-one","Score Total Cost of Ownership, Not Just Day One",[14,12549,12550],{},"A matrix that only measures launch-day fit will reward the framework that demos best and punish you eighteen months later. The criteria that predict long-term cost are dull on day one and decisive over the project's life: how often the framework ships breaking changes, how large the upgrade is each time, how active the maintenance community is, and how many people you can realistically hire who already know it.",[14,12552,12553],{},"Add an explicit row for upgrade burden and score it from the framework's own history. A generator that ships a major version every year with a migration guide is a known, plannable cost; one that breaks templates on minor releases is a recurring tax you can't schedule. Check the changelog and the issue tracker for the candidates, not the marketing site — the gap between \"we value stability\" and the actual cadence of breaking changes is where the real cost hides.",[14,12555,12556,12557,12559],{},"Hiring and knowledge transfer belong in the score too. The language the framework lives in is a proxy for both: a Node-based generator draws from the largest frontend talent pool, Go and Ruby from smaller ones. If the site will outlive its original author — and most production sites do — the relevant question isn't \"can our best engineer use this,\" it's \"can the engineer who inherits this in two years figure it out from the docs without us.\" A framework with thorough first-party documentation and a large, answer-rich community scores high on a criterion that never shows up in a build benchmark but dominates the maintenance years. Capture the durable version of these criteria in a written rubric — the ",[23,12558,5],{"href":742}," is built for exactly this — so the same standard applies the next time the question comes up.",[34,12561,2266],{"id":2265},[39,12563,12564,12570,12576,12582],{},[42,12565,12566,12569],{},[229,12567,12568],{},"Ignoring incremental\u002Fbuild-speed needs:"," a framework that only does full rebuilds becomes a CI bottleneck at scale. Weight build velocity for large repos.",[42,12571,12572,12575],{},[229,12573,12574],{},"Counting plugins as a positive:"," a long plugin list often means reliance on unmaintained community code. Prefer native capabilities and official integrations.",[42,12577,12578,12581],{},[229,12579,12580],{},"Skipping content-modeling validation:"," not testing nested collections and custom routing early forces a painful mid-project refactor.",[42,12583,12584,12587],{},[229,12585,12586],{},"Over-precise weights:"," scoring to two decimal places implies certainty you don't have. Round to whole-number weights and let clear gaps, not 0.05 differences, decide.",[34,12589,642],{"id":641},[14,12591,12592,12593,12596],{},"A selection matrix is only as good as its inputs: weight what your project actually needs, anchor each score to a written definition, run the ",[18,12594,12595],{},"same"," realistic build on each candidate, and prove the routing and content model before you commit. Score the durable costs — upgrade burden, hiring, documentation — alongside the day-one fit, because those are what you pay for over the life of the site. Decide deliberately and you avoid the far more expensive decision, the one no matrix can soften: migrating frameworks after launch, with real content and real readers already depending on the URLs.",[34,12598,2321],{"id":2320},[39,12600,12601,12604,12613,12616,12619],{},[42,12602,12603],{},"Weight what your project actually needs; the ranking is meant to move when the weights move.",[42,12605,12606,12607,12609,12610,12612],{},"Run the ",[18,12608,12595],{}," realistic build on each candidate and time it with ",[253,12611,595],{}," so the numbers are repeatable.",[42,12614,12615],{},"Use a CI matrix with isolated working directories so build times are produced on identical hardware.",[42,12617,12618],{},"Validate nested collections and routing before committing — a flat-blog winner can fail at scale.",[42,12620,12621],{},"A written rubric keeps the decision from being re-argued every quarter.",[34,12623,651],{"id":650},[653,12625,12627],{"id":12626},"how-do-i-weight-criteria-for-documentation-versus-marketing-sites","How do I weight criteria for documentation versus marketing sites?",[14,12629,12630],{},"Documentation favors build speed, native Markdown and MDX support, and search indexing, so weight those highest. Marketing favors component flexibility, headless-CMS integration, and Core Web Vitals. The same matrix works for both — only the weights change, which is exactly the point of scoring instead of guessing.",[653,12632,12634],{"id":12633},"what-build-time-threshold-should-i-target-for-10000-or-more-pages","What build-time threshold should I target for 10,000 or more pages?",[14,12636,12637],{},"Aim for full builds in the tens of seconds. If you are into minutes, look at resource caching, scoped templates, or a faster engine such as Hugo before accepting it. A multi-minute cold build will eventually collide with CI timeouts and slow every release as the repository grows.",[653,12639,12641],{"id":12640},"can-i-evaluate-multiple-ssgs-in-one-ci-pipeline","Can I evaluate multiple SSGs in one CI pipeline?",[14,12643,12644],{},"Yes. Use a build matrix and isolate each candidate in its own working directory or container so caches do not collide. Run the identical content set through each, capture the build time and output size, and compare them in the same job so the numbers are produced on identical hardware.",[653,12646,12648],{"id":12647},"how-many-criteria-should-a-selection-matrix-have","How many criteria should a selection matrix have?",[14,12650,12651],{},"Usually five to seven. Fewer than four hides important trade-offs; more than eight makes the weights so small that the ranking gets noisy. Pick the criteria that genuinely drive your project, drop the ones every candidate passes, and make the weights sum to one hundred percent.",[653,12653,12655],{"id":12654},"should-plugin-count-be-a-scoring-criterion","Should plugin count be a scoring criterion?",[14,12657,12658],{},"Not as a positive on its own. A long plugin list often signals reliance on unmaintained community code rather than capability. Score native, first-party support for the integrations you actually need, and treat heavy plugin dependence as a maintenance risk rather than a feature.",[34,12660,684],{"id":683},[39,12662,12663,12670,12675,12680,12685],{},[42,12664,12665,692,12667,12669],{},[229,12666,691],{},[23,12668,31],{"href":30}," — the trade-offs behind each scoring criterion.",[42,12671,12672,12674],{},[23,12673,328],{"href":327}," — how to weight the matrix when authors avoid the terminal.",[42,12676,12677,12679],{},[23,12678,357],{"href":356}," — adding an i18n routing row to the matrix.",[42,12681,12682,12684],{},[23,12683,5],{"href":742}," — the criteria as a reusable rubric.",[42,12686,12687,12689],{},[23,12688,774],{"href":773}," — what the build seconds actually buy.",[1346,12691,12692],{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}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 .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}",{"title":712,"searchDepth":713,"depth":713,"links":12694},[12695,12696,12697,12698,12699,12700,12701,12702,12703,12704,12711],{"id":12017,"depth":713,"text":12018},{"id":12034,"depth":713,"text":12035},{"id":12217,"depth":713,"text":12218},{"id":12410,"depth":713,"text":12411},{"id":12422,"depth":713,"text":12423},{"id":12546,"depth":713,"text":12547},{"id":2265,"depth":713,"text":2266},{"id":641,"depth":713,"text":642},{"id":2320,"depth":713,"text":2321},{"id":650,"depth":713,"text":651,"children":12705},[12706,12707,12708,12709,12710],{"id":12626,"depth":730,"text":12627},{"id":12633,"depth":730,"text":12634},{"id":12640,"depth":730,"text":12641},{"id":12647,"depth":730,"text":12648},{"id":12654,"depth":730,"text":12655},{"id":683,"depth":713,"text":684},[12713,12714,12715],{"name":737,"item":738},{"name":31,"item":30},{"name":26,"item":25},"Turn SSG selection into a scored decision matrix — weight criteria for your project, run identical benchmark builds on each candidate, and let the numbers guide the choice.",[12718,12719,12720,12721,12722],{"q":12627,"a":12630},{"q":12634,"a":12637},{"q":12641,"a":12644},{"q":12648,"a":12651},{"q":12655,"a":12658},{},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fssg-framework-selection-matrix",{"title":26,"description":12716},"choosing-the-right-static-site-generator-for-production\u002Fssg-framework-selection-matrix\u002Findex","awDt_LH4d9JaLJnco5Hs91SeQW_S70LbDs6xhn4aWp0",{"id":12729,"title":12730,"body":12731,"breadcrumb":13355,"dateModified":743,"datePublished":743,"description":13360,"extension":745,"faq":13361,"meta":13369,"navigation":752,"path":13370,"seo":13371,"slug":12735,"stem":13372,"type":756,"__hash__":13373},"content\u002Fchoosing-the-right-static-site-generator-for-production\u002Fssg-framework-selection-matrix\u002Fpicking-an-ssg-for-a-multi-language-documentation-site\u002Findex.md","Picking an SSG for a Multi-Language Docs Site",{"type":7,"value":12732,"toc":13337},[12733,12736,12743,12745,12758,12889,12893,12912,12957,12972,13042,13058,13062,13065,13071,13080,13084,13094,13113,13117,13123,13193,13202,13206,13224,13226,13264,13266,13271,13273,13277,13280,13284,13287,13291,13294,13298,13301,13305,13308,13310,13334],[10,12734,357],{"id":12735},"picking-an-ssg-for-a-multi-language-documentation-site",[14,12737,12738,12739,4582,12741,239],{},"A multi-language documentation site multiplies every decision by the number of locales. Routing, content organization, fallback behavior, and build time all change character once you have four languages instead of one. The four major static generators — Astro, Hugo, Eleventy, and Jekyll — handle internationalization very differently, from Hugo's deep built-in support to Eleventy's convention-driven approach. This guide compares them on the dimensions that matter for translated docs, with measured numbers from a four-language, 1,000-source-page corpus. It sits under the ",[23,12740,26],{"href":25},[23,12742,31],{"href":30},[34,12744,37],{"id":36},[39,12746,12747,12750,12753],{},[42,12748,12749],{},"A clear list of target locales and a fallback policy decision (what a reader sees when a page is not yet translated).",[42,12751,12752],{},"A sense of your translation workflow — whether translators edit Markdown directly or content comes from a TMS export — since that shapes the content layout.",[42,12754,12755,12757],{},[253,12756,595],{}," for build timing if you intend to compare candidates on your own corpus, plus each generator installed for a representative trial.",[55,12759,12760,12886],{},[58,12761,66,12765,66,12768,66,12771],{"viewBox":4608,"role":61,"ariaLabelledBy":12762,"xmlns":65},[12763,12764],"i18nmatrix-title","i18nmatrix-desc",[68,12766,12767],{"id":12763},"Internationalization capability matrix across four SSGs",[72,12769,12770],{"id":12764},"A grid rating Astro, Hugo, Eleventy, and Jekyll on built-in routing, content organization, fallback handling, and hreflang generation, with Hugo strongest overall and Jekyll weakest.",[95,12772,78,12773,78,12776,78,12778,78,12780,78,12782,78,12785,78,12789,78,12793,78,12797,78,78,12800,12802,78,12804,12806,78,12809,12811,78,12815,12817,78,78,12819,12821,78,12823,12825,78,12827,12829,78,12832,12834,78,78,12836,12838,78,12841,12843,78,12845,12847,78,12849,12851,78,78,12853,12855,78,12857,12859,78,12861,12863,78,12865,12867,78,12869,66],{"style":97},[99,12774,12775],{"x":816,"y":102,"fill":103,"style":104},"i18n capability by generator",[99,12777,269],{"x":112,"y":589,"fill":93,"style":882},[99,12779,265],{"x":816,"y":589,"fill":93,"style":882},[99,12781,273],{"x":906,"y":589,"fill":93,"style":882},[99,12783,277],{"x":12784,"y":589,"fill":93,"style":882},"640",[99,12786,12788],{"x":160,"y":4682,"fill":93,"style":12787},"font-size:12px;text-anchor:end","routing",[99,12790,12792],{"x":160,"y":12791,"fill":93,"style":12787},"148","content org",[99,12794,12796],{"x":160,"y":12795,"fill":93,"style":12787},"196","fallback",[99,12798,12799],{"x":160,"y":150,"fill":93,"style":12787},"hreflang",[107,12801],{"x":3500,"y":120,"width":1430,"height":5418,"rx":876,"fill":185,"opacity":4631,"stroke":187},[99,12803,12027],{"x":112,"y":3485,"style":4658},[107,12805],{"x":3500,"y":2535,"width":1430,"height":5418,"rx":876,"fill":185,"opacity":4631,"stroke":187},[99,12807,12808],{"x":112,"y":161,"style":4658},"folders",[107,12810],{"x":3500,"y":160,"width":1430,"height":5418,"rx":876,"fill":162,"opacity":4638,"stroke":164},[99,12812,12814],{"x":112,"y":12813,"style":4658},"198","manual",[107,12816],{"x":3500,"y":2596,"width":1430,"height":5418,"rx":876,"fill":162,"opacity":4638,"stroke":164},[99,12818,9901],{"x":112,"y":4674,"style":4658},[107,12820],{"x":6144,"y":120,"width":1430,"height":5418,"rx":876,"fill":185,"opacity":4638,"stroke":187},[99,12822,12027],{"x":816,"y":3485,"style":4658},[107,12824],{"x":6144,"y":2535,"width":1430,"height":5418,"rx":876,"fill":185,"opacity":4638,"stroke":187},[99,12826,12808],{"x":816,"y":161,"style":4658},[107,12828],{"x":6144,"y":160,"width":1430,"height":5418,"rx":876,"fill":185,"opacity":4638,"stroke":187},[99,12830,12831],{"x":816,"y":12813,"style":4658},"auto",[107,12833],{"x":6144,"y":2596,"width":1430,"height":5418,"rx":876,"fill":185,"opacity":4638,"stroke":187},[99,12835,12027],{"x":816,"y":4674,"style":4658},[107,12837],{"x":4696,"y":120,"width":1430,"height":5418,"rx":876,"fill":162,"opacity":4638,"stroke":164},[99,12839,12840],{"x":906,"y":3485,"style":4658},"plugin",[107,12842],{"x":4696,"y":2535,"width":1430,"height":5418,"rx":876,"fill":185,"opacity":4631,"stroke":187},[99,12844,12808],{"x":906,"y":161,"style":4658},[107,12846],{"x":4696,"y":160,"width":1430,"height":5418,"rx":876,"fill":162,"opacity":4638,"stroke":164},[99,12848,12814],{"x":906,"y":12813,"style":4658},[107,12850],{"x":4696,"y":2596,"width":1430,"height":5418,"rx":876,"fill":162,"opacity":4638,"stroke":164},[99,12852,9901],{"x":906,"y":4674,"style":4658},[107,12854],{"x":9750,"y":120,"width":1430,"height":5418,"rx":876,"fill":2564,"opacity":4644,"stroke":2565},[99,12856,12840],{"x":12784,"y":3485,"style":4658},[107,12858],{"x":9750,"y":2535,"width":1430,"height":5418,"rx":876,"fill":162,"opacity":4638,"stroke":164},[99,12860,7406],{"x":12784,"y":161,"style":4658},[107,12862],{"x":9750,"y":160,"width":1430,"height":5418,"rx":876,"fill":2564,"opacity":4644,"stroke":2565},[99,12864,12814],{"x":12784,"y":12813,"style":4658},[107,12866],{"x":9750,"y":2596,"width":1430,"height":5418,"rx":876,"fill":2564,"opacity":4644,"stroke":2565},[99,12868,12814],{"x":12784,"y":4674,"style":4658},[95,12870,88,12871,12873,88,12876,12878,88,12881,12883,78],{"style":11285},[107,12872],{"x":3500,"y":4683,"width":113,"height":113,"fill":185,"opacity":4638,"stroke":187},[99,12874,12875],{"x":4634,"y":1462,"fill":93},"built-in \u002F strong",[107,12877],{"x":3474,"y":4683,"width":113,"height":113,"fill":162,"opacity":4638,"stroke":164},[99,12879,12880],{"x":816,"y":1462,"fill":93},"convention \u002F add-on",[107,12882],{"x":2552,"y":4683,"width":113,"height":113,"fill":2564,"opacity":4644,"stroke":2565},[99,12884,12885],{"x":10820,"y":1462,"fill":93},"manual \u002F weak",[218,12887,12888],{},"Hugo is strongest across all four i18n dimensions; Astro is close behind with native routing; Eleventy and Jekyll require more manual wiring as locales grow.",[34,12890,12892],{"id":12891},"routing-across-locales","Routing Across Locales",[14,12894,12895,12896,12898,12899,12902,12903,1850,12905,1850,12908,12911],{},"Routing is where the gap is widest. ",[229,12897,265],{}," treats languages as a first-class concept: declare them in ",[253,12900,12901],{},"hugo.toml"," and it generates ",[253,12904,738],{},[253,12906,12907],{},"\u002Ffr\u002F",[253,12909,12910],{},"\u002Fde\u002F"," routes automatically with no plugin.",[987,12913,12915],{"className":2792,"code":12914,"language":2794,"meta":712,"style":712},"# hugo.toml\ndefaultContentLanguage = \"en\"\n[languages.en]\n  weight = 1\n[languages.fr]\n  weight = 2\n[languages.de]\n  weight = 3\n",[253,12916,12917,12922,12927,12932,12937,12942,12947,12952],{"__ignoreMap":712},[995,12918,12919],{"class":997,"line":998},[995,12920,12921],{},"# hugo.toml\n",[995,12923,12924],{"class":997,"line":713},[995,12925,12926],{},"defaultContentLanguage = \"en\"\n",[995,12928,12929],{"class":997,"line":730},[995,12930,12931],{},"[languages.en]\n",[995,12933,12934],{"class":997,"line":1544},[995,12935,12936],{},"  weight = 1\n",[995,12938,12939],{"class":997,"line":1550},[995,12940,12941],{},"[languages.fr]\n",[995,12943,12944],{"class":997,"line":1673},[995,12945,12946],{},"  weight = 2\n",[995,12948,12949],{"class":997,"line":1678},[995,12950,12951],{},"[languages.de]\n",[995,12953,12954],{"class":997,"line":1693},[995,12955,12956],{},"  weight = 3\n",[14,12958,12959,12961,12962,3725,12964,12967,12968,12971],{},[229,12960,269],{}," added native i18n routing in v4 — configure ",[253,12963,139],{},[253,12965,12966],{},"astro.config.mjs"," and it handles locale prefixes and ",[253,12969,12970],{},"getRelativeLocaleUrl"," helpers:",[987,12973,12975],{"className":1600,"code":12974,"language":1602,"meta":712,"style":712},"\u002F\u002F astro.config.mjs\nexport default defineConfig({\n  i18n: {\n    defaultLocale: \"en\",\n    locales: [\"en\", \"fr\", \"de\"],\n    routing: { prefixDefaultLocale: false },\n  },\n});\n",[253,12976,12977,12981,12991,12996,13006,13025,13034,13038],{"__ignoreMap":712},[995,12978,12979],{"class":997,"line":998},[995,12980,1609],{"class":1001},[995,12982,12983,12985,12987,12989],{"class":997,"line":713},[995,12984,1681],{"class":1614},[995,12986,1684],{"class":1614},[995,12988,1687],{"class":1007},[995,12990,1690],{"class":1618},[995,12992,12993],{"class":997,"line":730},[995,12994,12995],{"class":1618},"  i18n: {\n",[995,12997,12998,13001,13004],{"class":997,"line":1544},[995,12999,13000],{"class":1618},"    defaultLocale: ",[995,13002,13003],{"class":1023},"\"en\"",[995,13005,2885],{"class":1618},[995,13007,13008,13011,13013,13015,13018,13020,13023],{"class":997,"line":1550},[995,13009,13010],{"class":1618},"    locales: [",[995,13012,13003],{"class":1023},[995,13014,1850],{"class":1618},[995,13016,13017],{"class":1023},"\"fr\"",[995,13019,1850],{"class":1618},[995,13021,13022],{"class":1023},"\"de\"",[995,13024,8306],{"class":1618},[995,13026,13027,13030,13032],{"class":997,"line":1673},[995,13028,13029],{"class":1618},"    routing: { prefixDefaultLocale: ",[995,13031,2929],{"class":1010},[995,13033,2911],{"class":1618},[995,13035,13036],{"class":997,"line":1678},[995,13037,1729],{"class":1618},[995,13039,13040],{"class":997,"line":1693},[995,13041,1735],{"class":1618},[14,13043,13044,13046,13047,13050,13051,13053,13054,13057],{},[229,13045,273],{}," has no built-in locale router; the common approach is folder-per-language plus the community ",[253,13048,13049],{},"eleventy-plugin-i18n"," for string lookups, with permalinks driven by directory data. ",[229,13052,277],{}," relies on a plugin such as ",[253,13055,13056],{},"jekyll-multiple-languages-plugin"," or manual collections, which is the most hand-wired of the four.",[34,13059,13061],{"id":13060},"content-organization","Content Organization",[14,13063,13064],{},"For documentation at scale, folder-per-language is the layout that stays legible:",[987,13066,13069],{"className":13067,"code":13068,"language":99,"meta":712},[11603],"content\u002F\n  en\u002Fgetting-started\u002Findex.md\n  fr\u002Fgetting-started\u002Findex.md\n  de\u002Fgetting-started\u002Findex.md\n",[253,13070,13068],{"__ignoreMap":712},[14,13072,13073,13074,13077,13078,239],{},"Hugo, Astro, and Eleventy all map this cleanly to per-locale routes. Hugo also supports filename suffixes (",[253,13075,13076],{},"index.fr.md","), which is tidy for small sites but noisy across thousands of pages. Jekyll typically models languages as collections, which works but couples your locale structure to Jekyll's collection conventions. Whichever generator you pick, mirror the directory tree exactly across locales so translators always know where a page lives — the same discipline that keeps large single-language docs maintainable, as discussed in ",[23,13079,767],{"href":1372},[34,13081,13083],{"id":13082},"fallback-and-hreflang","Fallback and hreflang",[14,13085,13086,13087,13090,13091,13093],{},"Two i18n-specific concerns separate a polished multi-language site from a broken one. ",[229,13088,13089],{},"Fallback",": when a page is not yet translated, Hugo can serve the default-language version automatically; with Astro and Eleventy you decide explicitly whether to render the default page or hide the link, so readers never hit a 404. ",[229,13092,12799],{},": each translated page should declare its alternates so the right locale is matched. Hugo emits these from its translation data; in Astro and Eleventy you build them from a small data structure listing each page's available locales:",[987,13095,13097],{"className":1116,"code":13096,"language":1118,"meta":712,"style":712},"{% raw %}{% for locale in page.locales %}\n\u003Clink rel=\"alternate\" hreflang=\"{{ locale.lang }}\" href=\"{{ site.url }}{{ locale.url }}\">\n{% endfor %}{% endraw %}\n",[253,13098,13099,13104,13109],{"__ignoreMap":712},[995,13100,13101],{"class":997,"line":998},[995,13102,13103],{},"{% raw %}{% for locale in page.locales %}\n",[995,13105,13106],{"class":997,"line":713},[995,13107,13108],{},"\u003Clink rel=\"alternate\" hreflang=\"{{ locale.lang }}\" href=\"{{ site.url }}{{ locale.url }}\">\n",[995,13110,13111],{"class":997,"line":730},[995,13112,8150],{},[34,13114,13116],{"id":13115},"build-cost","Build Cost",[14,13118,13119,13120,13122],{},"Each locale is a full set of pages, so build time scales roughly linearly with total rendered pages. Timed with ",[253,13121,930],{}," on the four-language corpus (1,000 source pages × 4 locales = 4,000 rendered pages):",[433,13124,13125,13140],{},[436,13126,13127],{},[439,13128,13129,13131,13134,13137],{},[442,13130,3136],{},[442,13132,13133],{},"Single locale (1,000 pages)",[442,13135,13136],{},"Four locales (4,000 pages)",[442,13138,13139],{},"i18n setup",[457,13141,13142,13155,13168,13180],{},[439,13143,13144,13146,13149,13152],{},[462,13145,265],{},[462,13147,13148],{},"6 s",[462,13150,13151],{},"22 s",[462,13153,13154],{},"native, no add-ons",[439,13156,13157,13159,13162,13165],{},[462,13158,269],{},[462,13160,13161],{},"19 s",[462,13163,13164],{},"71 s",[462,13166,13167],{},"native routing (v4)",[439,13169,13170,13172,13174,13177],{},[462,13171,273],{},[462,13173,4886],{},[462,13175,13176],{},"36 s",[462,13178,13179],{},"folders + i18n plugin",[439,13181,13182,13184,13187,13190],{},[462,13183,277],{},[462,13185,13186],{},"26 s",[462,13188,13189],{},"104 s",[462,13191,13192],{},"multi-language plugin",[14,13194,13195,13196,13198,13199,13201],{},"The four-language numbers track the single-language ratios, confirming that i18n itself adds little overhead beyond the extra pages. Hugo's lead widens in absolute terms simply because it is the fastest per page. If build time at scale is your constraint, that ranking matches the broader picture in ",[23,13197,2478],{"href":2477},", and the caching levers in ",[23,13200,3324],{"href":3323}," apply per locale.",[34,13203,13205],{"id":13204},"putting-it-together","Putting It Together",[14,13207,13208,13209,13211,13212,13214,13215,13217,13218,13220,13221,13223],{},"For a large multi-language documentation site, the ranking is clear: ",[229,13210,265],{}," if you want the deepest built-in i18n and the fastest builds; ",[229,13213,269],{}," if you want native i18n routing with a component model and can absorb the higher per-page build cost; ",[229,13216,273],{}," if your team prefers templates-and-Markdown and you accept wiring locale routing yourself; ",[229,13219,277],{}," only if you are already committed to it, since its i18n story is the most manual. Score these against your own weights using the ",[23,13222,26],{"href":25}," rather than picking on i18n alone.",[34,13225,600],{"id":599},[39,13227,13228,13241,13247,13253,13259],{},[42,13229,13230,13233,13234,270,13237,13240],{},[229,13231,13232],{},"Inconsistent directory trees across locales:"," if ",[253,13235,13236],{},"fr\u002F",[253,13238,13239],{},"en\u002F"," diverge in structure, translators and fallback logic both break. Mirror the tree exactly.",[42,13242,13243,13246],{},[229,13244,13245],{},"Missing fallback policy:"," without an explicit decision, untranslated pages 404. Pick fallback-to-default or hide-the-link and apply it uniformly.",[42,13248,13249,13252],{},[229,13250,13251],{},"Forgetting hreflang:"," translated pages without alternate declarations leave locale matching to guesswork. Generate hreflang from a single source of locale data.",[42,13254,13255,13258],{},[229,13256,13257],{},"Underestimating build time:"," four locales is four times the pages. Benchmark the full multiplied corpus, not a single language, before committing.",[42,13260,13261,13263],{},[229,13262,637],{}," because all four generators consume a folder-per-language Markdown tree, a trial migration of one locale is cheap. Keep content generator-agnostic and you can build the same translated tree with another tool to compare real numbers.",[34,13265,642],{"id":641},[14,13267,13268,13269,239],{},"Internationalization is the dimension where these four generators differ most. Hugo leads on built-in routing, fallback, hreflang, and raw build speed; Astro's v4 routing makes it a strong second; Eleventy and Jekyll handle multiple languages but ask for more manual wiring as locales grow. Decide your fallback policy, mirror the directory tree across locales, emit hreflang from one data source, and benchmark the full multiplied page count. Then weight i18n alongside your other criteria in the parent ",[23,13270,26],{"href":25},[34,13272,651],{"id":650},[653,13274,13276],{"id":13275},"which-ssg-has-the-strongest-built-in-internationalization","Which SSG has the strongest built-in internationalization?",[14,13278,13279],{},"Hugo has the most complete built-in i18n: native language configuration, automatic per-language routing, translation tables, and per-language sitemaps without plugins. Astro added solid native i18n routing in version 4. Eleventy and Jekyll handle multiple languages but lean more on conventions and add-ons.",[653,13281,13283],{"id":13282},"how-does-build-time-scale-with-more-languages","How does build time scale with more languages?",[14,13285,13286],{},"Roughly linearly with total page count, since each locale is a full set of pages. In our four-language, 1,000-source-page test the 4,000 rendered pages built in 22 seconds with Hugo and 71 seconds with Astro, tracking the single-language ratio between the two.",[653,13288,13290],{"id":13289},"should-each-language-live-in-its-own-folder-or-use-filename-suffixes","Should each language live in its own folder or use filename suffixes?",[14,13292,13293],{},"Both work. Folder-per-language (content\u002Fen, content\u002Ffr) is the clearest for large docs and is the idiomatic layout in Hugo and Astro. Filename suffixes like page.fr.md are convenient for small sites but get noisy at scale.",[653,13295,13297],{"id":13296},"how-do-i-handle-untranslated-pages","How do I handle untranslated pages?",[14,13299,13300],{},"Decide on a fallback policy. Hugo can fall back to the default language for missing translations; with Astro or Eleventy you typically render the default-language page or hide the link. Define this explicitly so readers never hit a 404 for a page that exists in another language.",[653,13302,13304],{"id":13303},"do-i-need-hreflang-tags-on-a-static-multi-language-site","Do I need hreflang tags on a static multi-language site?",[14,13306,13307],{},"Yes, so each translated page declares its alternates. Hugo can emit these from its translation data; in Astro and Eleventy you generate them from a small data structure that lists each page's available locales.",[34,13309,684],{"id":683},[39,13311,13312,13319,13324,13329],{},[42,13313,13314,692,13316,13318],{},[229,13315,691],{},[23,13317,26],{"href":25}," — the scored decision framework to weigh i18n alongside other criteria.",[42,13320,13321,13323],{},[23,13322,328],{"href":327}," — the authoring-first view that matters when translators edit Markdown.",[42,13325,13326,13328],{},[23,13327,767],{"href":1372}," — scaling discipline that compounds across locales.",[42,13330,13331,13333],{},[23,13332,2478],{"href":2477}," — why Hugo's per-page speed leads the build-cost table.\n\n",[1346,13335,13336],{},"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 .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 .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}",{"title":712,"searchDepth":713,"depth":713,"links":13338},[13339,13340,13341,13342,13343,13344,13345,13346,13347,13354],{"id":36,"depth":713,"text":37},{"id":12891,"depth":713,"text":12892},{"id":13060,"depth":713,"text":13061},{"id":13082,"depth":713,"text":13083},{"id":13115,"depth":713,"text":13116},{"id":13204,"depth":713,"text":13205},{"id":599,"depth":713,"text":600},{"id":641,"depth":713,"text":642},{"id":650,"depth":713,"text":651,"children":13348},[13349,13350,13351,13352,13353],{"id":13275,"depth":730,"text":13276},{"id":13282,"depth":730,"text":13283},{"id":13289,"depth":730,"text":13290},{"id":13296,"depth":730,"text":13297},{"id":13303,"depth":730,"text":13304},{"id":683,"depth":713,"text":684},[13356,13357,13358,13359],{"name":737,"item":738},{"name":31,"item":30},{"name":26,"item":25},{"name":357,"item":356},"Compare i18n in Astro, Hugo, Eleventy, and Jekyll for multi-language documentation — routing, content organization, and build cost — with measured numbers across locales.",[13362,13365,13366,13367,13368],{"q":13276,"a":13363},{"Hugo has the most complete built-in i18n":13364},"native language configuration, automatic per-language routing, translation tables, and per-language sitemaps without plugins. Astro added solid native i18n routing in version 4. Eleventy and Jekyll handle multiple languages but lean more on conventions and add-ons.",{"q":13283,"a":13286},{"q":13290,"a":13293},{"q":13297,"a":13300},{"q":13304,"a":13307},{},"\u002Fchoosing-the-right-static-site-generator-for-production\u002Fssg-framework-selection-matrix\u002Fpicking-an-ssg-for-a-multi-language-documentation-site",{"title":12730,"description":13360},"choosing-the-right-static-site-generator-for-production\u002Fssg-framework-selection-matrix\u002Fpicking-an-ssg-for-a-multi-language-documentation-site\u002Findex","W9IJPQIyB-8i6f4KxXRiBuII9g8ihTt5BFqxT5ftktA",{"id":4,"title":5,"body":13375,"breadcrumb":13835,"dateModified":743,"datePublished":743,"description":744,"extension":745,"faq":13840,"meta":13845,"navigation":752,"path":753,"seo":13846,"slug":12,"stem":755,"type":756,"__hash__":757},{"type":7,"value":13376,"toc":13814},[13377,13379,13387,13389,13399,13468,13470,13478,13480,13482,13502,13508,13510,13512,13520,13522,13524,13534,13536,13538,13550,13552,13554,13566,13568,13570,13576,13578,13580,13590,13592,13594,13736,13740,13742,13768,13770,13774,13776,13778,13780,13782,13784,13786,13788,13790,13792,13794],[10,13378,5],{"id":12},[14,13380,16,13381,21,13383,27,13385,32],{},[18,13382,20],{},[23,13384,26],{"href":25},[23,13386,31],{"href":30},[34,13388,37],{"id":36},[39,13390,13391,13393,13395,13397],{},[42,13392,44],{},[42,13394,47],{},[42,13396,50],{},[42,13398,53],{},[55,13400,13401,13466],{},[58,13402,66,13404,66,13406,66,13408,66,13414],{"viewBox":60,"role":61,"ariaLabelledBy":13403,"xmlns":65},[63,64],[68,13405,70],{"id":63},[72,13407,74],{"id":64},[76,13409,78,13410,66],{},[80,13411,88,13412,78],{"id":82,"viewBox":83,"refX":84,"refY":85,"markerWidth":86,"markerHeight":86,"orient":87},[90,13413],{"d":92,"fill":93},[95,13415,78,13416,78,13418,78,13420,78,13422,78,13424,78,13426,78,13428,78,13430,78,13432,78,13434,78,13436,78,13438,78,13440,78,13442,78,13444,78,13446,78,13448,78,13450,78,13452,78,13454,78,13456,78,13458,78,13464,66],{"style":97},[99,13417,105],{"x":101,"y":102,"fill":103,"style":104},[107,13419],{"x":109,"y":110,"width":111,"height":112,"rx":113,"fill":114,"opacity":115,"stroke":114,"style":116},[99,13421,122],{"x":119,"y":120,"fill":114,"style":121},[99,13423,127],{"x":119,"y":125,"fill":103,"style":126},[99,13425,131],{"x":119,"y":130,"fill":103,"style":126},[99,13427,135],{"x":119,"y":134,"fill":103,"style":126},[99,13429,139],{"x":119,"y":138,"fill":103,"style":126},[99,13431,143],{"x":119,"y":142,"fill":103,"style":126},[99,13433,147],{"x":119,"y":146,"fill":103,"style":126},[99,13435,151],{"x":119,"y":150,"fill":103,"style":126},[99,13437,155],{"x":119,"y":154,"fill":93,"style":126},[107,13439],{"x":158,"y":159,"width":160,"height":161,"rx":113,"fill":162,"opacity":163,"stroke":164,"style":116},[99,13441,168],{"x":167,"y":119,"fill":103,"style":121},[99,13443,172],{"x":167,"y":171,"fill":93,"style":126},[99,13445,176],{"x":167,"y":175,"fill":93,"style":126},[99,13447,180],{"x":167,"y":179,"fill":93,"style":126},[107,13449],{"x":183,"y":159,"width":184,"height":161,"rx":113,"fill":185,"opacity":186,"stroke":187,"style":116},[99,13451,191],{"x":190,"y":119,"fill":187,"style":121},[99,13453,195],{"x":190,"y":194,"fill":103,"style":126},[99,13455,199],{"x":190,"y":198,"fill":103,"style":126},[99,13457,202],{"x":190,"y":146,"fill":93,"style":126},[95,13459,88,13460,88,13462,78],{"stroke":93,"fill":205,"style":116},[90,13461],{"d":208,"style":209},[90,13463],{"d":212,"style":209},[99,13465,216],{"x":101,"y":215,"fill":93,"style":126},[218,13467,220],{},[34,13469,224],{"id":223},[14,13471,227,13472,232,13474,236,13476,239],{},[229,13473,231],{},[229,13475,235],{},[23,13477,26],{"href":25},[34,13479,243],{"id":242},[14,13481,246],{},[39,13483,13484,13490,13500],{},[42,13485,251,13486,256,13488,260],{},[253,13487,255],{},[253,13489,259],{},[42,13491,13492,266,13494,270,13496,274,13498,278],{},[229,13493,265],{},[229,13495,269],{},[229,13497,273],{},[229,13499,277],{},[42,13501,281],{},[14,13503,284,13504,289,13506,293],{},[23,13505,288],{"href":287},[229,13507,292],{},[34,13509,297],{"id":296},[14,13511,300],{},[39,13513,13514,13516],{},[42,13515,305],{},[42,13517,308,13518,312],{},[18,13519,311],{},[34,13521,316],{"id":315},[14,13523,319],{},[39,13525,13526,13530,13532],{},[42,13527,324,13528,239],{},[23,13529,328],{"href":327},[42,13531,331],{},[42,13533,334],{},[34,13535,338],{"id":337},[14,13537,341],{},[39,13539,13540,13544,13548],{},[42,13541,346,13542,350],{},[229,13543,349],{},[42,13545,353,13546,239],{},[23,13547,357],{"href":356},[42,13549,360],{},[34,13551,364],{"id":363},[14,13553,367],{},[39,13555,13556,13560,13564],{},[42,13557,372,13558,376],{},[18,13559,375],{},[42,13561,379,13562,384],{},[23,13563,383],{"href":382},[42,13565,387],{},[34,13567,391],{"id":390},[14,13569,394],{},[39,13571,13572,13574],{},[42,13573,399],{},[42,13575,402],{},[34,13577,406],{"id":405},[14,13579,409],{},[39,13581,13582,13586,13588],{},[42,13583,414,13584,418],{},[253,13585,417],{},[42,13587,421],{},[42,13589,424],{},[34,13591,428],{"id":427},[14,13593,431],{},[433,13595,13596,13612],{},[436,13597,13598],{},[439,13599,13600,13602,13604,13606,13608,13610],{},[442,13601,444],{},[442,13603,447],{},[442,13605,269],{},[442,13607,265],{},[442,13609,273],{},[442,13611,277],{},[457,13613,13614,13628,13642,13656,13670,13684,13698,13712],{},[439,13615,13616,13618,13620,13622,13624,13626],{},[462,13617,127],{},[462,13619,85],{},[462,13621,468],{},[462,13623,85],{},[462,13625,468],{},[462,13627,475],{},[439,13629,13630,13632,13634,13636,13638,13640],{},[462,13631,131],{},[462,13633,468],{},[462,13635,85],{},[462,13637,475],{},[462,13639,468],{},[462,13641,475],{},[439,13643,13644,13646,13648,13650,13652,13654],{},[462,13645,135],{},[462,13647,496],{},[462,13649,85],{},[462,13651,496],{},[462,13653,468],{},[462,13655,496],{},[439,13657,13658,13660,13662,13664,13666,13668],{},[462,13659,139],{},[462,13661,85],{},[462,13663,468],{},[462,13665,85],{},[462,13667,496],{},[462,13669,475],{},[439,13671,13672,13674,13676,13678,13680,13682],{},[462,13673,143],{},[462,13675,475],{},[462,13677,85],{},[462,13679,85],{},[462,13681,85],{},[462,13683,468],{},[439,13685,13686,13688,13690,13692,13694,13696],{},[462,13687,147],{},[462,13689,496],{},[462,13691,85],{},[462,13693,468],{},[462,13695,496],{},[462,13697,468],{},[439,13699,13700,13702,13704,13706,13708,13710],{},[462,13701,151],{},[462,13703,496],{},[462,13705,468],{},[462,13707,85],{},[462,13709,468],{},[462,13711,496],{},[439,13713,13714,13718,13720,13724,13728,13732],{},[462,13715,13716],{},[229,13717,567],{},[462,13719],{},[462,13721,13722],{},[229,13723,574],{},[462,13725,13726],{},[229,13727,579],{},[462,13729,13730],{},[229,13731,584],{},[462,13733,13734],{},[229,13735,589],{},[14,13737,592,13738,596],{},[253,13739,595],{},[34,13741,600],{"id":599},[39,13743,13744,13748,13752,13756,13760,13764],{},[42,13745,13746,608],{},[229,13747,607],{},[42,13749,13750,614],{},[229,13751,613],{},[42,13753,13754,620],{},[229,13755,619],{},[42,13757,13758,626],{},[229,13759,625],{},[42,13761,13762,632],{},[229,13763,631],{},[42,13765,13766,638],{},[229,13767,637],{},[34,13769,642],{"id":641},[14,13771,645,13772,239],{},[23,13773,26],{"href":25},[34,13775,651],{"id":650},[653,13777,656],{"id":655},[14,13779,659],{},[653,13781,663],{"id":662},[14,13783,666],{},[653,13785,670],{"id":669},[14,13787,673],{},[653,13789,677],{"id":676},[14,13791,680],{},[34,13793,684],{"id":683},[39,13795,13796,13802,13806,13810],{},[42,13797,13798,692,13800,695],{},[229,13799,691],{},[23,13801,26],{"href":25},[42,13803,13804,700],{},[23,13805,328],{"href":327},[42,13807,13808,705],{},[23,13809,357],{"href":356},[42,13811,13812,710],{},[23,13813,31],{"href":30},{"title":712,"searchDepth":713,"depth":713,"links":13815},[13816,13817,13818,13819,13820,13821,13822,13823,13824,13825,13826,13827,13828,13834],{"id":36,"depth":713,"text":37},{"id":223,"depth":713,"text":224},{"id":242,"depth":713,"text":243},{"id":296,"depth":713,"text":297},{"id":315,"depth":713,"text":316},{"id":337,"depth":713,"text":338},{"id":363,"depth":713,"text":364},{"id":390,"depth":713,"text":391},{"id":405,"depth":713,"text":406},{"id":427,"depth":713,"text":428},{"id":599,"depth":713,"text":600},{"id":641,"depth":713,"text":642},{"id":650,"depth":713,"text":651,"children":13829},[13830,13831,13832,13833],{"id":655,"depth":730,"text":656},{"id":662,"depth":730,"text":663},{"id":669,"depth":730,"text":670},{"id":676,"depth":730,"text":677},{"id":683,"depth":713,"text":684},[13836,13837,13838,13839],{"name":737,"item":738},{"name":31,"item":30},{"name":26,"item":25},{"name":5,"item":742},[13841,13842,13843,13844],{"q":656,"a":659},{"q":663,"a":666},{"q":670,"a":673},{"q":677,"a":680},{},{"title":5,"description":744},{"id":13848,"title":13849,"body":13850,"breadcrumb":14799,"dateModified":743,"datePublished":2446,"description":14804,"extension":745,"faq":14805,"meta":14819,"navigation":752,"path":14820,"seo":14821,"slug":13854,"stem":14822,"type":2460,"__hash__":14823},"content\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fcdn-caching-rules-for-ssgs\u002Findex.md","CDN Caching Rules for SSGs",{"type":7,"value":13851,"toc":14776},[13852,13855,13861,13868,13975,13979,13982,13997,14003,14016,14022,14038,14044,14067,14071,14078,14120,14126,14133,14145,14151,14154,14158,14174,14177,14227,14240,14244,14255,14261,14405,14411,14425,14444,14448,14455,14515,14533,14535,14593,14595,14625,14627,14631,14646,14650,14663,14667,14670,14674,14698,14702,14718,14722,14737,14739,14773],[10,13853,13849],{"id":13854},"cdn-caching-rules-for-ssgs",[14,13856,13857,13858,13860],{},"Static sites are the ideal case for aggressive edge caching: the output is deterministic, so most of it can live on a CDN for a long time and never touch your origin again. The entire discipline reduces to one distinction — fingerprinted assets can be cached forever, HTML cannot — plus a clean purge on deploy so a release is reflected instantly without stampeding your origin. Get that split right and you cut Time to First Byte (TTFB) worldwide and stabilize Core Web Vitals as a side effect. This guide sits inside ",[23,13859,5501],{"href":5500},", where edge delivery is the lever that owns TTFB.",[14,13862,13863,13864,13867],{},"This page covers the header architecture, the deploy-time purge, edge-vs-origin TTFB, query-string and ",[253,13865,13866],{},"Vary"," hygiene, and how to validate that the cache is doing what you think it is — each section with config you can paste and before\u002Fafter numbers you can reproduce.",[55,13869,13870,13972],{},[58,13871,66,13876,66,13879,66,13882,66,13965],{"viewBox":13872,"role":61,"ariaLabelledBy":13873,"xmlns":65},"0 0 820 420",[13874,13875],"cdn-flow-title","cdn-flow-desc",[68,13877,13878],{"id":13874},"Edge cache decision flow for a static site request",[72,13880,13881],{"id":13875},"A request arrives at the edge. Fingerprinted assets are served from an immutable one-year cache as a HIT; HTML is checked against the origin with must-revalidate, returning a fast 304 or a fresh body; a deploy purges only HTML and stable paths, leaving asset cache warm.",[95,13883,78,13884,78,13887,78,13889,78,13892,78,13896,78,13900,78,13904,78,13907,78,13910,78,13912,78,13915,78,13918,78,13921,78,13924,78,13927,78,13929,78,13932,78,13935,78,13938,78,13942,78,13944,78,13947,78,13950,78,13953,78,13957,78,13961,66],{"style":813},[99,13885,13886],{"x":1415,"y":4630,"fill":103,"style":1416},"One request, two cache lifetimes, a clean purge on deploy",[107,13888],{"x":3578,"y":849,"width":7852,"height":1420,"rx":823,"fill":824,"opacity":825,"stroke":824,"style":116},[99,13890,13891],{"x":1431,"y":6849,"fill":824,"style":121},"Request hits edge",[99,13893,13895],{"x":1431,"y":13894,"fill":93,"style":126},"118","nearest PoP",[90,13897],{"d":13898,"stroke":93,"fill":205,"style":13899},"M200 102 L250 102","stroke-width:2px;marker-end:url(#cdn-arrow)",[99,13901,13903],{"x":1462,"y":13902,"fill":103,"style":121},"106","Hashed asset?",[90,13905],{"d":13906,"stroke":93,"fill":205,"style":13899},"M290 122 L290 165",[99,13908,907],{"x":13909,"y":161,"fill":187,"style":11900},"305",[107,13911],{"x":194,"y":194,"width":184,"height":1430,"rx":823,"fill":185,"opacity":186,"stroke":187,"style":116},[99,13913,13914],{"x":1462,"y":12813,"fill":187,"style":121},"Serve from edge — HIT",[99,13916,13917],{"x":1462,"y":111,"fill":103,"style":126},"max-age=31536000, immutable",[99,13919,13920],{"x":1462,"y":8710,"fill":93,"style":126},"never revalidated · ~5 ms",[90,13922],{"d":13923,"stroke":93,"fill":205,"style":13899},"M360 102 L520 102",[99,13925,13926],{"x":5320,"y":2527,"fill":2565,"style":882},"no — HTML",[107,13928],{"x":2552,"y":849,"width":184,"height":4682,"rx":823,"fill":162,"opacity":163,"stroke":164,"style":116},[99,13930,13931],{"x":2562,"y":6849,"fill":103,"style":121},"Revalidate with origin",[99,13933,13934],{"x":2562,"y":1431,"fill":93,"style":126},"max-age=0, must-revalidate",[99,13936,13937],{"x":2562,"y":119,"fill":187,"style":126},"304 Not Modified → fast",[99,13939,13941],{"x":2562,"y":13940,"fill":2565,"style":126},"158","or 200 fresh body",[107,13943],{"x":2552,"y":112,"width":184,"height":4682,"rx":823,"fill":114,"opacity":1432,"stroke":114,"style":116},[99,13945,13946],{"x":2562,"y":820,"fill":114,"style":121},"Deploy → scoped purge",[99,13948,13949],{"x":2562,"y":2614,"fill":103,"style":126},"purge HTML + sitemap\u002Ffeed",[99,13951,13952],{"x":2562,"y":1463,"fill":93,"style":126},"asset cache stays warm",[99,13954,13956],{"x":2562,"y":13955,"fill":187,"style":126},"338","no origin stampede",[90,13958],{"d":13959,"stroke":114,"fill":205,"style":13960},"M660 170 L660 248","stroke-width:2px;stroke-dasharray:5 4;marker-end:url(#cdn-arrow)",[99,13962,13964],{"x":1415,"y":13963,"fill":93,"style":126},"392","A new build mints new asset URLs, so caching the old ones forever is always safe.",[76,13966,78,13967,66],{},[80,13968,88,13970,78],{"id":13969,"viewBox":83,"refX":84,"refY":85,"markerWidth":86,"markerHeight":86,"orient":87},"cdn-arrow",[90,13971],{"d":92,"fill":93},[218,13973,13974],{},"Hashed assets are served straight from the edge as immutable HITs; HTML always revalidates so it points at the current assets; a deploy purges only HTML and stable paths, leaving the asset cache warm.",[34,13976,13978],{"id":13977},"the-two-tier-cache-control-architecture","The Two-Tier Cache-Control Architecture",[14,13980,13981],{},"There are only two kinds of files leaving a static build, and they want opposite cache policies.",[14,13983,13984,7242,13987,1850,13990,13993,13994,13996],{},[229,13985,13986],{},"Fingerprinted assets",[253,13988,13989],{},"\u002Fassets\u002Fapp.a1b2c3.js",[253,13991,13992],{},"\u002F_astro\u002Fpage.d4f9.css",", hashed images — carry a content hash in the filename. When the bytes change, the hash changes, so the URL changes. A cached copy of an old URL can therefore never be stale. Cache them for a year and mark them ",[253,13995,11756],{}," so the browser skips even conditional revalidation:",[987,13998,14001],{"className":13999,"code":14000,"language":99,"meta":712},[11603],"public, max-age=31536000, immutable\n",[253,14002,14000],{"__ignoreMap":712},[14,14004,14005,14008,14009,14012,14013,14015],{},[229,14006,14007],{},"HTML"," is the opposite. The URL ",[253,14010,14011],{},"\u002Fguide\u002F"," is stable across builds but its contents change on every deploy, and it must reference the ",[18,14014,311],{}," hashed assets. If you cache HTML long, a returning visitor loads an old shell pointing at asset URLs that no longer exist, and the page breaks. HTML wants:",[987,14017,14020],{"className":14018,"code":14019,"language":99,"meta":712},[11603],"public, max-age=0, must-revalidate\n",[253,14021,14019],{"__ignoreMap":712},[14,14023,14024,14025,14027,14028,14030,14031,14033,14034,14037],{},"Map this to your generator's output directory — Astro ",[253,14026,8885],{},", Eleventy and Jekyll ",[253,14029,7303],{},", Hugo ",[253,14032,8881],{}," — and your fingerprinted assets almost always land under a single prefix you can target. A minimal Netlify ",[253,14035,14036],{},"_headers"," file:",[987,14039,14042],{"className":14040,"code":14041,"language":99,"meta":712},[11603],"\u002Fassets\u002F*\n  Cache-Control: public, max-age=31536000, immutable\n\u002F*.js\n  Cache-Control: public, max-age=31536000, immutable\n\u002F*.css\n  Cache-Control: public, max-age=31536000, immutable\n\u002F*\n  Cache-Control: public, max-age=0, must-revalidate\n",[253,14043,14041],{"__ignoreMap":712},[14,14045,14046,14047,5153,14051,14055,14056,270,14058,14062,14063,14066],{},"The host-specific syntax differs but the values never do. The ",[23,14048,14050],{"href":14049},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fcdn-caching-rules-for-ssgs\u002Fsetting-up-proper-cache-headers-on-netlify\u002F","Netlify recipe",[23,14052,14054],{"href":14053},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fcdn-caching-rules-for-ssgs\u002Fsetting-cache-control-headers-on-cloudflare-pages\u002F","Cloudflare Pages recipe"," apply exactly this split with each platform's parser. Coordinate it with ",[23,14057,2190],{"href":2189},[23,14059,14061],{"href":14060},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Ffont-loading-strategies-for-static-sites\u002F","Font Loading Strategies for Static Sites"," so your largest render-blocking assets are both optimized ",[18,14064,14065],{},"and"," long-cached — an optimized hero that revalidates on every visit is only half the win.",[653,14068,14070],{"id":14069},"beforeafter-on-repeat-visits","Before\u002Fafter on repeat visits",[14,14072,14073,14074,14077],{},"The first visit is identical either way; the entire payoff is on repeat navigation. On a documentation page with ~30 fingerprinted assets, measured with ",[253,14075,14076],{},"curl"," and Chrome DevTools against the deployed URL:",[433,14079,14080,14093],{},[436,14081,14082],{},[439,14083,14084,14087,14090],{},[442,14085,14086],{},"Policy",[442,14088,14089],{},"Origin requests on repeat visit",[442,14091,14092],{},"Repeat-visit load",[457,14094,14095,14106],{},[439,14096,14097,14100,14103],{},[462,14098,14099],{},"Host defaults (no headers)",[462,14101,14102],{},"30 conditional revalidations",[462,14104,14105],{},"640 ms",[439,14107,14108,14114,14117],{},[462,14109,14110,14111,14113],{},"Two-tier (",[253,14112,11756],{}," assets)",[462,14115,14116],{},"1 (HTML only)",[462,14118,14119],{},"180 ms",[14,14121,14122,14123,14125],{},"Thirty round trips collapse to one because ",[253,14124,11756],{}," lets the browser serve every hashed asset from disk without asking the edge.",[34,14127,14129,14132],{"id":14128},"stale-while-revalidate-for-html",[253,14130,14131],{},"stale-while-revalidate"," for HTML",[14,14134,14135,14137,14138,14140,14141,14144],{},[253,14136,13934],{}," is correct but conservative: every HTML request blocks on a revalidation round trip to the edge. Adding ",[253,14139,14131],{}," lets the edge serve the cached HTML ",[18,14142,14143],{},"instantly"," while it refreshes the copy in the background, so a visitor never waits on the revalidation:",[987,14146,14149],{"className":14147,"code":14148,"language":99,"meta":712},[11603],"\u002F*\n  Cache-Control: public, max-age=0, must-revalidate, stale-while-revalidate=60\n",[253,14150,14148],{"__ignoreMap":712},[14,14152,14153],{},"Within the 60-second window, a request that finds slightly-stale HTML is served immediately and triggers an async refresh; the next visitor gets the fresh copy. For most content sites a window of 30–120 seconds is a good balance — long enough to absorb traffic spikes, short enough that a deploy is still effectively instant once you also purge (below). Skip SWR only where seconds-fresh HTML is a hard requirement, such as a status page.",[34,14155,14157],{"id":14156},"cache-invalidation-on-deploy","Cache Invalidation on Deploy",[14,14159,14160,14161,14163,14164,14166,14167,14169,14170,14173],{},"Because fingerprinted assets get brand-new URLs on every build, a deploy does ",[229,14162,3112],{}," need to purge them — their old URLs simply stop being referenced and age out naturally. What a deploy must invalidate is HTML and a few stable, unhashed paths: ",[253,14165,738],{},", the route HTML, ",[253,14168,10396],{},", and any feed. Prefer a ",[229,14171,14172],{},"scoped"," purge (by path or cache tag) over purge-everything, which cold-starts the entire cache and forces a wave of origin fetches.",[14,14175,14176],{},"A Cloudflare scoped purge by URL, run as the last step of your deploy job:",[987,14178,14180],{"className":1912,"code":14179,"language":1914,"meta":712,"style":712},"- name: Purge HTML from CDN cache\n  run: |\n    curl -X POST \\\n      \"https:\u002F\u002Fapi.cloudflare.com\u002Fclient\u002Fv4\u002Fzones\u002F${{ secrets.CF_ZONE }}\u002Fpurge_cache\" \\\n      -H \"Authorization: Bearer ${{ secrets.CF_API_TOKEN }}\" \\\n      -H \"Content-Type: application\u002Fjson\" \\\n      --data '{\"files\":[\"https:\u002F\u002Fexample.com\u002F\",\"https:\u002F\u002Fexample.com\u002Fsitemap.xml\"]}'\n",[253,14181,14182,14193,14202,14207,14212,14217,14222],{"__ignoreMap":712},[995,14183,14184,14186,14188,14190],{"class":997,"line":998},[995,14185,3191],{"class":1618},[995,14187,1922],{"class":1921},[995,14189,1925],{"class":1618},[995,14191,14192],{"class":1023},"Purge HTML from CDN cache\n",[995,14194,14195,14198,14200],{"class":997,"line":713},[995,14196,14197],{"class":1921},"  run",[995,14199,1925],{"class":1618},[995,14201,3215],{"class":1614},[995,14203,14204],{"class":997,"line":730},[995,14205,14206],{"class":1023},"    curl -X POST \\\n",[995,14208,14209],{"class":997,"line":1544},[995,14210,14211],{"class":1023},"      \"https:\u002F\u002Fapi.cloudflare.com\u002Fclient\u002Fv4\u002Fzones\u002F${{ secrets.CF_ZONE }}\u002Fpurge_cache\" \\\n",[995,14213,14214],{"class":997,"line":1550},[995,14215,14216],{"class":1023},"      -H \"Authorization: Bearer ${{ secrets.CF_API_TOKEN }}\" \\\n",[995,14218,14219],{"class":997,"line":1673},[995,14220,14221],{"class":1023},"      -H \"Content-Type: application\u002Fjson\" \\\n",[995,14223,14224],{"class":997,"line":1678},[995,14225,14226],{"class":1023},"      --data '{\"files\":[\"https:\u002F\u002Fexample.com\u002F\",\"https:\u002F\u002Fexample.com\u002Fsitemap.xml\"]}'\n",[14,14228,14229,14230,14233,14234,14237,14238,239],{},"The blunt alternative, ",[253,14231,14232],{},"{\"purge_everything\": true}",", also works but evicts your warm asset cache for no benefit and briefly raises origin load. Reach for cache ",[229,14235,14236],{},"tags"," when you want to purge a logical group (for example, every page in one section) in a single call. Many hosts handle this for you — Netlify and Cloudflare Pages purge HTML automatically on each successful deploy, so on those platforms the manual purge above is only needed for an externally-fronted CDN. Wiring the purge into the release flow belongs to ",[23,14239,5505],{"href":5504},[34,14241,14243],{"id":14242},"edge-delivery-ttfb","Edge Delivery & TTFB",[14,14245,14246,14247,14250,14251,14254],{},"Serving from the nearest point of presence is what actually drives TTFB down — the bytes travel tens of kilometres instead of crossing an ocean to your origin. With the two-tier policy in place, the overwhelming majority of asset requests never reach origin at all, and HTML revalidations return a tiny ",[253,14248,14249],{},"304 Not Modified"," instead of a full body. Measured from three regions with ",[253,14252,14253],{},"curl -w",", moving a site from origin-only delivery to an edge cache with this policy took median asset TTFB from ~210 ms to ~18 ms.",[14,14256,14257,14258,931],{},"For the rare dynamic fragment — a search endpoint, a personalized banner — reach for an edge function rather than falling back to the origin, so even dynamic responses are computed at the PoP. Per-host config can also pin asset rules directly; a Vercel example in ",[253,14259,14260],{},"vercel.json",[987,14262,14266],{"className":14263,"code":14264,"language":14265,"meta":712,"style":712},"language-json shiki shiki-themes github-light github-dark","{\n  \"headers\": [\n    {\n      \"source\": \"\u002Fassets\u002F(.*)\",\n      \"headers\": [\n        { \"key\": \"Cache-Control\", \"value\": \"public, max-age=31536000, immutable\" }\n      ]\n    },\n    {\n      \"source\": \"\u002F(.*)\\\\.html\",\n      \"headers\": [\n        { \"key\": \"Cache-Control\", \"value\": \"public, max-age=0, must-revalidate\" }\n      ]\n    }\n  ]\n}\n","json",[253,14267,14268,14273,14281,14286,14298,14305,14330,14335,14339,14343,14360,14366,14387,14391,14396,14401],{"__ignoreMap":712},[995,14269,14270],{"class":997,"line":998},[995,14271,14272],{"class":1618},"{\n",[995,14274,14275,14278],{"class":997,"line":713},[995,14276,14277],{"class":1010},"  \"headers\"",[995,14279,14280],{"class":1618},": [\n",[995,14282,14283],{"class":997,"line":730},[995,14284,14285],{"class":1618},"    {\n",[995,14287,14288,14291,14293,14296],{"class":997,"line":1544},[995,14289,14290],{"class":1010},"      \"source\"",[995,14292,1925],{"class":1618},[995,14294,14295],{"class":1023},"\"\u002Fassets\u002F(.*)\"",[995,14297,2885],{"class":1618},[995,14299,14300,14303],{"class":997,"line":1550},[995,14301,14302],{"class":1010},"      \"headers\"",[995,14304,14280],{"class":1618},[995,14306,14307,14310,14313,14315,14318,14320,14323,14325,14328],{"class":997,"line":1673},[995,14308,14309],{"class":1618},"        { ",[995,14311,14312],{"class":1010},"\"key\"",[995,14314,1925],{"class":1618},[995,14316,14317],{"class":1023},"\"Cache-Control\"",[995,14319,1850],{"class":1618},[995,14321,14322],{"class":1010},"\"value\"",[995,14324,1925],{"class":1618},[995,14326,14327],{"class":1023},"\"public, max-age=31536000, immutable\"",[995,14329,7475],{"class":1618},[995,14331,14332],{"class":997,"line":1678},[995,14333,14334],{"class":1618},"      ]\n",[995,14336,14337],{"class":997,"line":1693},[995,14338,2964],{"class":1618},[995,14340,14341],{"class":997,"line":1705},[995,14342,14285],{"class":1618},[995,14344,14345,14347,14349,14352,14355,14358],{"class":997,"line":1711},[995,14346,14290],{"class":1010},[995,14348,1925],{"class":1618},[995,14350,14351],{"class":1023},"\"\u002F(.*)",[995,14353,14354],{"class":1010},"\\\\",[995,14356,14357],{"class":1023},".html\"",[995,14359,2885],{"class":1618},[995,14361,14362,14364],{"class":997,"line":1717},[995,14363,14302],{"class":1010},[995,14365,14280],{"class":1618},[995,14367,14368,14370,14372,14374,14376,14378,14380,14382,14385],{"class":997,"line":1726},[995,14369,14309],{"class":1618},[995,14371,14312],{"class":1010},[995,14373,1925],{"class":1618},[995,14375,14317],{"class":1023},[995,14377,1850],{"class":1618},[995,14379,14322],{"class":1010},[995,14381,1925],{"class":1618},[995,14383,14384],{"class":1023},"\"public, max-age=0, must-revalidate\"",[995,14386,7475],{"class":1618},[995,14388,14389],{"class":997,"line":1732},[995,14390,14334],{"class":1618},[995,14392,14393],{"class":997,"line":2967},[995,14394,14395],{"class":1618},"    }\n",[995,14397,14398],{"class":997,"line":2972},[995,14399,14400],{"class":1618},"  ]\n",[995,14402,14403],{"class":997,"line":4147},[995,14404,9008],{"class":1618},[34,14406,14408,14409],{"id":14407},"hit-ratio-hygiene-query-strings-and-vary","Hit-Ratio Hygiene: Query Strings and ",[253,14410,13866],{},[14,14412,14413,14414,14417,14418,1850,14421,14424],{},"A correct policy can still produce a poor hit ratio if the cache key is fragmented. CDNs key on the ",[229,14415,14416],{},"full URL",", including query string, so unnormalized tracking parameters (",[253,14419,14420],{},"?utm_source=...",[253,14422,14423],{},"?ref=...",") turn one cacheable page into thousands of distinct cache entries that each get exactly one hit. Configure the CDN to ignore non-significant query params, or strip them, so all the variants collapse to one key.",[14,14426,14427,14429,14430,14433,14434,8912,14436,14439,14440,14443],{},[253,14428,13866],{}," is the other common ratio-killer. A ",[253,14431,14432],{},"Vary: User-Agent"," splits every object into a separate cached copy per browser string — effectively uncacheable. Keep ",[253,14435,13866],{},[253,14437,14438],{},"Accept-Encoding"," (which you want, for Brotli\u002Fgzip negotiation) and avoid varying on anything high-cardinality. If you do content negotiation for image formats, prefer distinct hashed URLs per format over a ",[253,14441,14442],{},"Vary: Accept",", which many CDNs handle poorly.",[34,14445,14447],{"id":14446},"validation","Validation",[14,14449,14450,14451,14454],{},"Never trust that the cache works — confirm it by reading response headers against the ",[229,14452,14453],{},"deployed"," URL, since local dev does not reproduce edge behavior:",[987,14456,14458],{"className":989,"code":14457,"language":991,"meta":712,"style":712},"# Asset: expect immutable + a cache HIT on the second request\ncurl -sI https:\u002F\u002Fexample.com\u002Fassets\u002Fapp.a1b2c3.js | grep -iE 'cache-control|cf-cache-status|age'\n\n# HTML: expect max-age=0, must-revalidate\ncurl -sI https:\u002F\u002Fexample.com\u002F | grep -i cache-control\n",[253,14459,14460,14465,14487,14491,14496],{"__ignoreMap":712},[995,14461,14462],{"class":997,"line":998},[995,14463,14464],{"class":1001},"# Asset: expect immutable + a cache HIT on the second request\n",[995,14466,14467,14469,14472,14475,14478,14481,14484],{"class":997,"line":713},[995,14468,14076],{"class":1007},[995,14470,14471],{"class":1010}," -sI",[995,14473,14474],{"class":1023}," https:\u002F\u002Fexample.com\u002Fassets\u002Fapp.a1b2c3.js",[995,14476,14477],{"class":1614}," |",[995,14479,14480],{"class":1007}," grep",[995,14482,14483],{"class":1010}," -iE",[995,14485,14486],{"class":1023}," 'cache-control|cf-cache-status|age'\n",[995,14488,14489],{"class":997,"line":730},[995,14490,1541],{"emptyLinePlaceholder":752},[995,14492,14493],{"class":997,"line":1544},[995,14494,14495],{"class":1001},"# HTML: expect max-age=0, must-revalidate\n",[995,14497,14498,14500,14502,14505,14507,14509,14512],{"class":997,"line":1550},[995,14499,14076],{"class":1007},[995,14501,14471],{"class":1010},[995,14503,14504],{"class":1023}," https:\u002F\u002Fexample.com\u002F",[995,14506,14477],{"class":1614},[995,14508,14480],{"class":1007},[995,14510,14511],{"class":1010}," -i",[995,14513,14514],{"class":1023}," cache-control\n",[14,14516,346,14517,14520,14521,14524,14525,14528,14529,14532],{},[253,14518,14519],{},"cf-cache-status: HIT"," (Cloudflare), ",[253,14522,14523],{},"x-cache: HIT"," (Fastly and others), or ",[253,14526,14527],{},"age:"," greater than zero — any of these confirms the object came from cache. Then watch LCP and FCP in ",[229,14530,14531],{},"both"," Lighthouse CI and field RUM: the lab proves the headers are set, the field data proves they helped real users on real networks. A drop in TTFB at the field level is the signal that edge caching is doing its job in production.",[34,14534,2266],{"id":2265},[39,14536,14537,14545,14551,14564,14578,14587],{},[42,14538,14539,14544],{},[229,14540,14541,14542,931],{},"Caching HTML as ",[253,14543,11756],{}," users and crawlers then never see updates without a manual purge, and rollbacks silently fail. HTML must stay short-lived and revalidated.",[42,14546,14547,14550],{},[229,14548,14549],{},"Purging everything on every deploy:"," evicts the warm asset cache for no reason and triggers an origin stampede. Purge only HTML and stable unhashed paths.",[42,14552,14553,14556,14557,738,14560,14563],{},[229,14554,14555],{},"Query-string fragmentation:"," unnormalized ",[253,14558,14559],{},"utm_*",[253,14561,14562],{},"ref"," params shatter the hit ratio because each unique URL is its own cache entry. Ignore or strip non-significant params at the CDN.",[42,14565,14566,14573,14574,8912,14576,239],{},[229,14567,14568,2204,14570,931],{},[253,14569,14432],{},[253,14571,14572],{},"Vary: Cookie"," splits one object into countless variants and effectively disables caching. Limit ",[253,14575,13866],{},[253,14577,14438],{},[42,14579,14580,14586],{},[229,14581,14582,14583,14585],{},"No ",[253,14584,14131],{}," on HTML:"," every HTML request blocks on a synchronous revalidation that spikes TTFB under load. Add a short SWR window.",[42,14588,14589,14592],{},[229,14590,14591],{},"Forgetting the sitemap and feed:"," these unhashed files are easy to leave stale after a deploy. Include them in the scoped purge.",[34,14594,2321],{"id":2320},[39,14596,14597,14603,14608,14611,14618],{},[42,14598,14599,14600,14602],{},"One rule, applied everywhere: ",[253,14601,11756],{},", year-long caching for fingerprinted assets; short-lived, revalidated caching for HTML.",[42,14604,7225,14605,14607],{},[253,14606,14131],{}," to HTML so visitors never block on a revalidation round trip.",[42,14609,14610],{},"On deploy, purge only HTML and stable unhashed paths — never purge everything, and let fingerprinted assets age out on their own.",[42,14612,14613,14614,8912,14616,239],{},"Protect your hit ratio by normalizing query strings and keeping ",[253,14615,13866],{},[253,14617,14438],{},[42,14619,14620,14621,14624],{},"Validate with ",[253,14622,14623],{},"curl -I"," and a cache-status header, then confirm the TTFB win in field RUM, not just the lab.",[34,14626,651],{"id":650},[653,14628,14630],{"id":14629},"should-ssg-html-be-cached-at-the-edge-at-all","Should SSG HTML be cached at the edge at all?",[14,14632,14633,14634,14637,14638,2204,14641,14643,14644,239],{},"Yes, but with a short ",[253,14635,14636],{},"max-age"," plus ",[253,14639,14640],{},"must-revalidate",[253,14642,14131],{},". That keeps a copy at the edge for low TTFB while guaranteeing the browser checks for a newer build before trusting it. The directive you must avoid on HTML is ",[253,14645,11756],{},[653,14647,14649],{"id":14648},"what-max-age-should-fingerprinted-assets-use","What max-age should fingerprinted assets use?",[14,14651,14652,14653,14656,14657,14659,14660,14662],{},"One year (",[253,14654,14655],{},"31536000"," seconds) with ",[253,14658,11756],{},". The content hash in the filename changes whenever the bytes change, so a cached old URL can never be wrong. ",[253,14661,11756],{}," additionally tells the browser to skip conditional revalidation entirely for that file.",[653,14664,14666],{"id":14665},"how-do-i-invalidate-only-what-changed-on-deploy","How do I invalidate only what changed on deploy?",[14,14668,14669],{},"Prefer tag-based or path-based purges triggered by your deploy hook over purge-everything. Since fingerprinted assets get new URLs each build, a deploy really only needs to invalidate HTML and a handful of stable paths like the sitemap and feed.",[653,14671,14673],{"id":14672},"how-do-i-confirm-content-is-actually-served-from-cache","How do I confirm content is actually served from cache?",[14,14675,14676,14677,7048,14680,2204,14683,14686,14687,14690,14691,14694,14695,14697],{},"Read the response headers. Cloudflare sends ",[253,14678,14679],{},"cf-cache-status",[253,14681,14682],{},"HIT",[253,14684,14685],{},"MISS",", Fastly and others send ",[253,14688,14689],{},"x-cache",", and an ",[253,14692,14693],{},"age"," header greater than zero means the object came from cache. Use ",[253,14696,14623],{}," against the deployed URL rather than local dev.",[653,14699,14701],{"id":14700},"why-is-my-cache-hit-ratio-low-even-though-assets-are-immutable","Why is my cache hit ratio low even though assets are immutable?",[14,14703,14704,14705,14707,14708,14710,14711,2204,14714,14717],{},"Usually query-string fragmentation or an over-broad ",[253,14706,13866],{}," header. CDNs key on the full URL including query params, so unnormalized tracking params shatter the hit ratio. A ",[253,14709,13866],{}," on ",[253,14712,14713],{},"User-Agent",[253,14715,14716],{},"Accept"," can also split a single object into many cached variants.",[653,14719,14721],{"id":14720},"does-the-two-tier-policy-work-the-same-on-every-host","Does the two-tier policy work the same on every host?",[14,14723,14724,14725,14727,14728,14730,14731,14733,14734,14736],{},"The reasoning is identical everywhere because it depends on content hashing, not the host. The syntax differs: Netlify uses a ",[253,14726,14036],{}," file, Cloudflare Pages uses ",[253,14729,14036],{}," or transform rules, and Vercel uses a ",[253,14732,5691],{}," array in ",[253,14735,14260],{},". The values you set are the same.",[34,14738,684],{"id":683},[39,14740,14741,14748,14757,14763,14768],{},[42,14742,14743,692,14745,14747],{},[229,14744,691],{},[23,14746,5501],{"href":5500}," — where edge caching fits the TTFB picture.",[42,14749,14750,14753,14754,14756],{},[23,14751,14752],{"href":14049},"Setting Up Proper Cache Headers on Netlify"," — this policy in Netlify's ",[253,14755,14036],{}," syntax.",[42,14758,14759,14762],{},[23,14760,14761],{"href":14053},"Setting Cache-Control Headers on Cloudflare Pages"," — the same policy with Cloudflare syntax and cache-status verification.",[42,14764,14765,14767],{},[23,14766,14061],{"href":14060}," — optimize and long-cache the other render-blocking asset.",[42,14769,14770,14772],{},[23,14771,2190],{"href":2189}," — make sure your largest cached asset is also the smallest it can be.",[1346,14774,14775],{},"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 .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}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 .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}",{"title":712,"searchDepth":713,"depth":713,"links":14777},[14778,14781,14783,14784,14785,14787,14788,14789,14790,14798],{"id":13977,"depth":713,"text":13978,"children":14779},[14780],{"id":14069,"depth":730,"text":14070},{"id":14128,"depth":713,"text":14782},"stale-while-revalidate for HTML",{"id":14156,"depth":713,"text":14157},{"id":14242,"depth":713,"text":14243},{"id":14407,"depth":713,"text":14786},"Hit-Ratio Hygiene: Query Strings and Vary",{"id":14446,"depth":713,"text":14447},{"id":2265,"depth":713,"text":2266},{"id":2320,"depth":713,"text":2321},{"id":650,"depth":713,"text":651,"children":14791},[14792,14793,14794,14795,14796,14797],{"id":14629,"depth":730,"text":14630},{"id":14648,"depth":730,"text":14649},{"id":14665,"depth":730,"text":14666},{"id":14672,"depth":730,"text":14673},{"id":14700,"depth":730,"text":14701},{"id":14720,"depth":730,"text":14721},{"id":683,"depth":713,"text":684},[14800,14801,14802],{"name":737,"item":738},{"name":5501,"item":5500},{"name":13849,"item":14803},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fcdn-caching-rules-for-ssgs\u002F","Maximize edge cache efficiency for static sites: cache fingerprinted assets for a year, revalidate HTML every request, and purge cleanly on deploy to cut TTFB.",[14806,14808,14810,14811,14813,14815],{"q":14630,"a":14807},"Yes, but with a short max-age plus must-revalidate or stale-while-revalidate. That keeps a copy at the edge for low TTFB while guaranteeing the browser checks for a newer build before trusting it. The directive you must avoid on HTML is immutable.",{"q":14649,"a":14809},"One year (31536000 seconds) with immutable. The content hash in the filename changes whenever the bytes change, so a cached old URL can never be wrong. Immutable additionally tells the browser to skip conditional revalidation entirely for that file.",{"q":14666,"a":14669},{"q":14673,"a":14812},"Read the response headers. Cloudflare sends cf-cache-status with HIT or MISS, Fastly and others send x-cache, and an age header greater than zero means the object came from cache. Use curl -I against the deployed URL rather than local dev.",{"q":14701,"a":14814},"Usually query-string fragmentation or an over-broad Vary header. CDNs key on the full URL including query params, so unnormalized tracking params shatter the hit ratio. A Vary on User-Agent or Accept-Encoding can also split a single object into many cached variants.",{"q":14721,"a":14816},{"The reasoning is identical everywhere because it depends on content hashing, not the host":14817},{" The syntax differs":14818},"Netlify uses a _headers file, Cloudflare Pages uses _headers or transform rules, and Vercel uses a headers array in vercel.json. The values you set are the same.",{},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fcdn-caching-rules-for-ssgs",{"title":13849,"description":14804},"performance-optimization-core-web-vitals-for-ssgs\u002Fcdn-caching-rules-for-ssgs\u002Findex","NDktKijLUGf74ZBoGu48SniBzLCGcCeKKygi318lU54",{"id":14825,"title":14826,"body":14827,"breadcrumb":15449,"dateModified":743,"datePublished":743,"description":15454,"extension":745,"faq":15455,"meta":15466,"navigation":752,"path":15467,"seo":15468,"slug":14831,"stem":15469,"type":756,"__hash__":15470},"content\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fcdn-caching-rules-for-ssgs\u002Fsetting-cache-control-headers-on-cloudflare-pages\u002Findex.md","Cache-Control Headers on Cloudflare Pages",{"type":7,"value":14828,"toc":15430},[14829,14832,14850,14852,14877,14959,14965,14971,14977,14995,15017,15021,15031,15060,15063,15069,15078,15093,15099,15111,15125,15131,15142,15144,15151,15215,15221,15223,15290,15292,15319,15321,15325,15334,15338,15350,15354,15381,15385,15394,15398,15407,15409,15427],[10,14830,14761],{"id":14831},"setting-cache-control-headers-on-cloudflare-pages",[14,14833,14834,14835,14838,14839,14841,14842,14844,14845,14847,14848,239],{},"Cloudflare Pages serves your static output from Cloudflare's global edge, but two separate layers decide how long a response lives: the ",[253,14836,14837],{},"Cache-Control"," header browsers obey, and Cloudflare's own edge cache. Getting both right means a ",[253,14840,14036],{}," file for the browser contract plus a Cache Rule when you want HTML held at the edge. The underlying policy is the same two-tier split described in ",[23,14843,13849],{"href":14803}," — fingerprinted assets cached for a year, HTML revalidated — and it sits inside the broader ",[23,14846,5501],{"href":5500}," effort. This is the Cloudflare-specific companion to ",[23,14849,14752],{"href":14049},[34,14851,37],{"id":36},[39,14853,14854,14857,14863,14874],{},[42,14855,14856],{},"A site already deploying to Cloudflare Pages from Astro, Hugo, Eleventy, or Jekyll — all of which emit content-hashed asset filenames.",[42,14858,14859,14860,14862],{},"Repository access so the ",[253,14861,14036],{}," file is version-controlled in the build output rather than configured by hand.",[42,14864,14865,14867,14868,270,14870,14873],{},[253,14866,14076],{}," available locally to inspect ",[253,14869,14837],{},[253,14871,14872],{},"CF-Cache-Status"," against the deployed URL.",[42,14875,14876],{},"For edge-caching HTML, a zone on Cloudflare where you can add a Cache Rule (Pages projects on a custom domain qualify).",[55,14878,14879,14956],{},[58,14880,66,14884,66,14887,66,14890,66,14949],{"viewBox":4608,"role":61,"ariaLabelledBy":14881,"xmlns":65},[14882,14883],"cfpages-flow-title","cfpages-flow-desc",[68,14885,14886],{"id":14882},"Request flow through Cloudflare Pages edge cache",[72,14888,14889],{"id":14883},"A browser request hits a Cloudflare edge node; a hashed asset returns CF-Cache-Status HIT directly from the edge, while an HTML document defaults to DYNAMIC and is fetched from the Pages origin unless a Cache Rule stores it.",[95,14891,78,14892,78,14895,78,14897,78,14901,78,14903,78,14905,78,14909,78,14912,78,14915,78,14917,78,14921,78,14925,78,14927,78,14930,78,14933,78,14945,66],{"style":813},[99,14893,14894],{"x":816,"y":109,"fill":103,"style":104},"Two paths through the Cloudflare edge",[107,14896],{"x":109,"y":1431,"width":1431,"height":1430,"rx":823,"fill":824,"opacity":186,"stroke":824,"style":116},[99,14898,14900],{"x":873,"y":14899,"fill":824,"style":121},"155","Browser",[99,14902,14623],{"x":873,"y":138,"fill":93,"style":126},[107,14904],{"x":158,"y":1431,"width":161,"height":1430,"rx":823,"fill":114,"opacity":186,"stroke":114,"style":116},[99,14906,14908],{"x":14907,"y":161,"fill":114,"style":121},"375","Cloudflare edge",[99,14910,14911],{"x":14907,"y":10805,"fill":93,"style":126},"300+ POPs",[99,14913,14914],{"x":14907,"y":175,"fill":93,"style":126},"checks edge cache",[107,14916],{"x":9750,"y":110,"width":119,"height":1420,"rx":823,"fill":185,"opacity":850,"stroke":187,"style":116},[99,14918,14920],{"x":14919,"y":584,"fill":187,"style":121},"670","Asset HIT",[99,14922,14924],{"x":14919,"y":14923,"fill":93,"style":126},"108","served from edge",[107,14926],{"x":9750,"y":12795,"width":119,"height":1420,"rx":823,"fill":2564,"opacity":825,"stroke":2565,"style":116},[99,14928,14929],{"x":14919,"y":1437,"fill":2565,"style":121},"HTML DYNAMIC",[99,14931,14932],{"x":14919,"y":150,"fill":93,"style":126},"to Pages origin",[95,14934,88,14935,88,14939,88,14942,78],{"stroke":93,"fill":205,"style":116},[90,14936],{"d":14937,"style":14938},"M150 160 L298 160","marker-end:url(#cfpages-arrow)",[90,14940],{"d":14941,"style":14938},"M450 150 L598 100",[90,14943],{"d":14944,"style":14938},"M450 175 L598 220",[99,14946,14948],{"x":14947,"y":12791,"fill":93,"style":126},"225","GET \u002Fasset or \u002Fpage",[76,14950,78,14951,66],{},[80,14952,88,14954,78],{"id":14953,"viewBox":83,"refX":84,"refY":85,"markerWidth":86,"markerHeight":86,"orient":87},"cfpages-arrow",[90,14955],{"d":92,"fill":93},[218,14957,14958],{},"Hashed assets reach the edge cache and return CF-Cache-Status HIT; HTML defaults to DYNAMIC and is fetched from the Pages origin unless a Cache Rule sets an Edge TTL for it.",[34,14960,8896,14962,14964],{"id":14961},"the-_headers-file-the-browser-contract",[253,14963,14036],{}," File: The Browser Contract",[14,14966,14967,14968,14970],{},"Cloudflare Pages reads a ",[253,14969,14036],{}," file from the root of your build output, using the same syntax as Netlify. Long-cache hashed assets and revalidate HTML:",[987,14972,14975],{"className":14973,"code":14974,"language":99,"meta":712},[11603],"\u002F*.html\n  Cache-Control: public, max-age=0, must-revalidate\n\u002Fassets\u002F*\n  Cache-Control: public, max-age=31536000, immutable\n\u002F*.js\n  Cache-Control: public, max-age=31536000, immutable\n\u002F*.css\n  Cache-Control: public, max-age=31536000, immutable\n\u002F_astro\u002F*\n  Cache-Control: public, max-age=31536000, immutable\n",[253,14976,14974],{"__ignoreMap":712},[14,14978,14979,14980,14983,14984,1850,14986,10335,14989,14991,14992,14994],{},"This works because Astro (",[253,14981,14982],{},"\u002F_astro\u002F","), Hugo, Eleventy, and Jekyll all emit content-hashed asset filenames — a fresh build produces new URLs, so caching the old ones forever is safe. Make sure your output directory is the one Pages publishes (",[253,14985,2242],{},[253,14987,14988],{},"public",[253,14990,2245],{},") and that the ",[253,14993,14036],{}," file lands at its root, not in a subfolder.",[14,14996,14997,14998,15000,15001,692,15004,15006,15007,15010,15011,15013,15014,15016],{},"One important distinction: on Cloudflare Pages the ",[253,14999,14036],{}," file controls the ",[229,15002,15003],{},"browser",[253,15005,14837],{}," value, and it does cause the edge to cache static ",[229,15008,15009],{},"assets"," with a long TTL. It does ",[229,15012,3112],{},", by itself, make Cloudflare cache ",[229,15015,14007],{}," at the edge. That is the second layer.",[34,15018,15020],{"id":15019},"the-second-tier-a-cache-rule-for-html","The Second Tier: A Cache Rule for HTML",[14,15022,15023,15024,15026,15027,15030],{},"By default Cloudflare treats HTML documents as dynamic and forwards them to the Pages origin, which is why ",[253,15025,14623],{}," on a page shows ",[253,15028,15029],{},"CF-Cache-Status: DYNAMIC",". For a static site that is wasteful — the HTML is identical for every visitor between deploys. Add a Cache Rule in the dashboard (Caching → Cache Rules) to hold HTML at the edge with a short TTL plus stale-while-revalidate:",[39,15032,15033,15045],{},[42,15034,15035,692,15038,15041,15042],{},[229,15036,15037],{},"When incoming requests match:",[253,15039,15040],{},"URI Path ends with \"\u002F\""," OR ",[253,15043,15044],{},"URI Path ends with \".html\"",[42,15046,15047,15050,15051,15054,15055,692,15057,260],{},[229,15048,15049],{},"Then:"," Eligible for cache → Edge TTL: Override to ",[229,15052,15053],{},"300 seconds"," → Browser TTL: Respect origin (your ",[253,15056,14036],{},[253,15058,15059],{},"max-age=0",[14,15061,15062],{},"This caches the document at the edge for five minutes while still letting the browser revalidate on every navigation. With stale-while-revalidate semantics, the first request after the TTL lapses still serves the cached copy instantly while the edge refreshes in the background. Because a new Pages deployment invalidates the build, you never serve HTML older than your deploy cadence.",[34,15064,15066,15067],{"id":15065},"verifying-with-curl-i","Verifying with ",[253,15068,14623],{},[14,15070,15071,15072,15074,15075,15077],{},"Local dev never reproduces edge headers, so inspect the deployed URL. Read both ",[253,15073,14837],{}," (the browser contract) and ",[253,15076,14872],{}," (the edge result):",[987,15079,15081],{"className":989,"code":15080,"language":991,"meta":712,"style":712},"curl -I https:\u002F\u002Fyour-site.pages.dev\u002F_astro\u002Fapp.abc123.js\n",[253,15082,15083],{"__ignoreMap":712},[995,15084,15085,15087,15090],{"class":997,"line":998},[995,15086,14076],{"class":1007},[995,15088,15089],{"class":1010}," -I",[995,15091,15092],{"class":1023}," https:\u002F\u002Fyour-site.pages.dev\u002F_astro\u002Fapp.abc123.js\n",[987,15094,15097],{"className":15095,"code":15096,"language":99,"meta":712},[11603],"HTTP\u002F2 200\ncache-control: public, max-age=31536000, immutable\ncf-cache-status: HIT\n",[253,15098,15096],{"__ignoreMap":712},[14,15100,15101,15102,15104,15105,15107,15108,15110],{},"A hashed asset should reach ",[253,15103,14682],{}," after the first request warms that edge node. The first request may report ",[253,15106,14685],{}," — the edge fetched from origin and is now storing the response — and a second request should flip it to ",[253,15109,14682],{},". For an HTML route after your Cache Rule is live:",[987,15112,15114],{"className":989,"code":15113,"language":991,"meta":712,"style":712},"curl -I https:\u002F\u002Fyour-site.pages.dev\u002Fguide\u002F\n",[253,15115,15116],{"__ignoreMap":712},[995,15117,15118,15120,15122],{"class":997,"line":998},[995,15119,14076],{"class":1007},[995,15121,15089],{"class":1010},[995,15123,15124],{"class":1023}," https:\u002F\u002Fyour-site.pages.dev\u002Fguide\u002F\n",[987,15126,15129],{"className":15127,"code":15128,"language":99,"meta":712},[11603],"cache-control: public, max-age=0, must-revalidate\ncf-cache-status: HIT\n",[253,15130,15128],{"__ignoreMap":712},[14,15132,15133,15134,15137,15138,15141],{},"If HTML still reports ",[253,15135,15136],{},"DYNAMIC",", the Cache Rule did not match — check that your path expression covers both trailing-slash directory URLs and ",[253,15139,15140],{},".html"," files.",[34,15143,1166],{"id":1165},[14,15145,15146,15147,15150],{},"On a documentation site with roughly 28 hashed assets per page served from Cloudflare Pages, adding the explicit two-tier policy produced a clear repeat-visit and HTML-delivery improvement. Numbers below are median of 20 ",[253,15148,15149],{},"curl -w '%{time_total}'"," runs from a fixed location, with edge nodes warmed:",[433,15152,15153,15168],{},[436,15154,15155],{},[439,15156,15157,15159,15162,15165],{},[442,15158,940],{},[442,15160,15161],{},"CF-Cache-Status (HTML)",[442,15163,15164],{},"HTML TTFB",[442,15166,15167],{},"Repeat-visit asset requests to origin",[457,15169,15170,15185,15201],{},[439,15171,15172,15178,15180,15182],{},[462,15173,15174,15175,15177],{},"Pages defaults (no ",[253,15176,14036],{},", no Cache Rule)",[462,15179,15136],{},[462,15181,10950],{},[462,15183,15184],{},"28 conditional revalidations",[439,15186,15187,15194,15196,15199],{},[462,15188,15189,15191,15192,982],{},[253,15190,14036],{}," only (assets ",[253,15193,11756],{},[462,15195,15136],{},[462,15197,15198],{},"205 ms",[462,15200,14116],{},[439,15202,15203,15208,15210,15213],{},[462,15204,15205,15207],{},[253,15206,14036],{}," + HTML Cache Rule",[462,15209,14682],{},[462,15211,15212],{},"38 ms",[462,15214,14116],{},[14,15216,15217,15218,15220],{},"The asset win comes entirely from ",[253,15219,11756],{}," letting the browser skip revalidation on repeat navigation. The HTML TTFB drop from 210 ms to 38 ms comes from the Cache Rule serving the document from the nearest edge node instead of the Pages origin. First-visit, cold-cache numbers are unchanged — this is repeat-traffic and edge-locality optimization.",[34,15222,600],{"id":599},[39,15224,15225,15234,15249,15260,15266],{},[42,15226,15227,15231,15232,239],{},[229,15228,15229,14585],{},[253,15230,11756],{}," visitors load an old shell that points at hashed assets that no longer exist, producing broken pages. Always keep HTML on ",[253,15233,13934],{},[42,15235,15236,15242,15243,15245,15246,15248],{},[229,15237,15238,15239,15241],{},"Expecting ",[253,15240,14036],{}," to cache HTML at the edge:"," it does not on Pages. HTML stays ",[253,15244,15136],{}," until you add a Cache Rule. This is the most common surprise when moving from Netlify, where the ",[253,15247,14036],{}," file alone shapes more of the behavior.",[42,15250,15251,15256,15257,15259],{},[229,15252,15253,15255],{},[253,15254,14036],{}," in the wrong directory:"," the file must sit at the root of the published output. If it is nested, Pages ignores it silently and every path falls back to defaults. Confirm with ",[253,15258,14623],{}," after deploy.",[42,15261,15262,15265],{},[229,15263,15264],{},"Cache Rule TTL too long:"," a multi-hour Edge TTL on HTML can outlive a deploy if the deploy does not purge that path. Keep HTML Edge TTL short (300 s) and lean on per-deploy invalidation.",[42,15267,15268,15270,15271,15273,15274,15277,15278,15280,15281,15283,15284,15286,15287,15289],{},[229,15269,637],{}," the ",[253,15272,14036],{}," policy reverts with a one-line ",[253,15275,15276],{},"git revert"," plus a redeploy. A Cache Rule is removed in the dashboard or via API; after either change, ",[253,15279,14623],{}," and confirm ",[253,15282,14872],{}," returns ",[253,15285,14685],{}," then ",[253,15288,14682],{}," again. To force-clear a stuck asset, purge by URL rather than Purge Everything, which cold-starts every edge node.",[34,15291,642],{"id":641},[14,15293,15294,15295,15297,15298,15300,15301,15304,15305,15307,15308,270,15310,15312,15313,15315,15316,15318],{},"On Cloudflare Pages the task is two layers, not one: a correct ",[253,15296,14036],{}," file gives hashed assets a year-long ",[253,15299,11756],{}," browser cache and a long edge TTL, while a Cache Rule earns HTML the ",[253,15302,15303],{},"CF-Cache-Status: HIT"," that drops document TTFB to edge speed. Verify both with ",[253,15306,14623],{},", reading ",[253,15309,14837],{},[253,15311,14872],{}," together. The two-tier reasoning is identical to every other host — see ",[23,15314,14752],{"href":14049}," for the Netlify syntax, where the ",[253,15317,14036],{}," file carries more of the weight.",[34,15320,651],{"id":650},[653,15322,15324],{"id":15323},"why-does-my-html-show-cf-cache-status-dynamic","Why does my HTML show CF-Cache-Status DYNAMIC?",[14,15326,15327,15328,15330,15331,15333],{},"Cloudflare does not cache HTML at the edge by default, so document requests report ",[253,15329,15136],{}," and go to the Pages origin. To cache HTML you must add a Cache Rule that sets Edge TTL for the HTML paths; the ",[253,15332,14036],{}," file alone controls browser caching, not edge caching, for HTML.",[653,15335,15337],{"id":15336},"do-_headers-and-cache-rules-conflict","Do _headers and Cache Rules conflict?",[14,15339,15340,15341,15343,15344,15346,15347,15349],{},"They operate at different layers. The ",[253,15342,14036],{}," file sets the ",[253,15345,14837],{}," response header that browsers obey. Cache Rules control how long Cloudflare's edge stores the response. Use ",[253,15348,14036],{}," for the browser contract and a Cache Rule when you also want HTML held at the edge with stale-while-revalidate.",[653,15351,15353],{"id":15352},"how-do-i-confirm-an-asset-is-served-from-the-edge","How do I confirm an asset is served from the edge?",[14,15355,7633,15356,15358,15359,15361,15362,15364,15365,15367,15368,8912,15370,15372,15373,692,15375,15377,15378,15380],{},[253,15357,14623],{}," against the asset URL and read the ",[253,15360,14872],{}," header. ",[253,15363,14682],{}," means it came from the edge cache, ",[253,15366,14685],{}," means the edge fetched from origin and is now storing it, and a second request should flip ",[253,15369,14685],{},[253,15371,14682],{},". Hashed assets with a one-year ",[253,15374,11756],{},[253,15376,14837],{}," should reach ",[253,15379,14682],{}," quickly.",[653,15382,15384],{"id":15383},"does-cloudflare-honor-immutable-on-hashed-assets","Does Cloudflare honor immutable on hashed assets?",[14,15386,15387,15388,15390,15391,15393],{},"Yes. Browsers that understand ",[253,15389,11756],{}," skip conditional revalidation for those hashed files until ",[253,15392,14636],{}," expires, and Cloudflare keeps them at the edge for the Edge TTL. Because the filename changes on every build, caching the old URL forever is safe.",[653,15395,15397],{"id":15396},"how-do-i-purge-after-a-bad-header-deploy","How do I purge after a bad header deploy?",[14,15399,15400,15401,15283,15403,15286,15405,239],{},"A new Pages deployment serves new hashed asset URLs, so stale assets age out naturally. For HTML or an unhashed file, purge by URL or use Purge Everything in the dashboard, then re-request and confirm ",[253,15402,14872],{},[253,15404,14685],{},[253,15406,14682],{},[34,15408,684],{"id":683},[39,15410,15411,15418,15423],{},[42,15412,15413,692,15415,15417],{},[229,15414,691],{},[23,15416,13849],{"href":14803}," — the host-agnostic two-tier policy.",[42,15419,15420,15422],{},[23,15421,14752],{"href":14049}," — the same policy with Netlify syntax.",[42,15424,15425,14747],{},[23,15426,5501],{"href":5500},[1346,15428,15429],{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}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);}",{"title":712,"searchDepth":713,"depth":713,"links":15431},[15432,15433,15435,15436,15438,15439,15440,15441,15448],{"id":36,"depth":713,"text":37},{"id":14961,"depth":713,"text":15434},"The _headers File: The Browser Contract",{"id":15019,"depth":713,"text":15020},{"id":15065,"depth":713,"text":15437},"Verifying with curl -I",{"id":1165,"depth":713,"text":1166},{"id":599,"depth":713,"text":600},{"id":641,"depth":713,"text":642},{"id":650,"depth":713,"text":651,"children":15442},[15443,15444,15445,15446,15447],{"id":15323,"depth":730,"text":15324},{"id":15336,"depth":730,"text":15337},{"id":15352,"depth":730,"text":15353},{"id":15383,"depth":730,"text":15384},{"id":15396,"depth":730,"text":15397},{"id":683,"depth":713,"text":684},[15450,15451,15452,15453],{"name":737,"item":738},{"name":5501,"item":5500},{"name":13849,"item":14803},{"name":14826,"item":14053},"Set Cache-Control on Cloudflare Pages with a _headers file and Cache Rules — year-long immutable assets, revalidated HTML, and CF-Cache-Status HIT verified with curl.",[15456,15458,15460,15462,15464],{"q":15324,"a":15457},"Cloudflare does not cache HTML at the edge by default, so document requests report DYNAMIC and go to the Pages origin. To cache HTML you must add a Cache Rule that sets Edge TTL for the HTML paths; the _headers file alone controls browser caching, not edge caching, for HTML.",{"q":15337,"a":15459},"They operate at different layers. The _headers file sets the Cache-Control response header that browsers obey. Cache Rules control how long Cloudflare's edge stores the response. Use _headers for the browser contract and a Cache Rule when you also want HTML held at the edge with stale-while-revalidate.",{"q":15353,"a":15461},"Run curl -I against the asset URL and read the CF-Cache-Status header. HIT means it came from the edge cache, MISS means the edge fetched from origin and is now storing it, and a second request should flip MISS to HIT. Hashed assets with a one-year immutable Cache-Control should reach HIT quickly.",{"q":15384,"a":15463},"Yes. Browsers that understand immutable skip conditional revalidation for those hashed files until max-age expires, and Cloudflare keeps them at the edge for the Edge TTL. Because the filename changes on every build, caching the old URL forever is safe.",{"q":15397,"a":15465},"A new Pages deployment serves new hashed asset URLs, so stale assets age out naturally. For HTML or an unhashed file, purge by URL or use Purge Everything in the dashboard, then re-request and confirm CF-Cache-Status returns MISS then HIT.",{},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fcdn-caching-rules-for-ssgs\u002Fsetting-cache-control-headers-on-cloudflare-pages",{"title":14826,"description":15454},"performance-optimization-core-web-vitals-for-ssgs\u002Fcdn-caching-rules-for-ssgs\u002Fsetting-cache-control-headers-on-cloudflare-pages\u002Findex","KOU3rhawZl3iaumM_b1pXU2fHLG5hYh53OymiLWlwnU",{"id":15472,"title":15473,"body":15474,"breadcrumb":15881,"dateModified":743,"datePublished":2446,"description":15887,"extension":745,"faq":15888,"meta":15896,"navigation":752,"path":15897,"seo":15898,"slug":15478,"stem":15899,"type":756,"__hash__":15900},"content\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fcdn-caching-rules-for-ssgs\u002Fsetting-up-proper-cache-headers-on-netlify\u002Findex.md","Proper Cache Headers on Netlify for SSGs",{"type":7,"value":15475,"toc":15859},[15476,15479,15492,15494,15510,15566,15570,15576,15590,15599,15605,15615,15621,15624,15634,15643,15645,15648,15686,15692,15696,15716,15718,15764,15766,15784,15786,15790,15793,15800,15806,15815,15825,15832,15838,15840,15857],[10,15477,14752],{"id":15478},"setting-up-proper-cache-headers-on-netlify",[14,15480,15481,15482,15484,15485,15487,15488,27,15490,32],{},"Netlify serves your static output from its edge, but the cache behavior is only as good as the ",[253,15483,14837],{}," headers you set in a ",[253,15486,14036],{}," file. The rule is the same two-tier split as any CDN — fingerprinted assets cached for a year, HTML revalidated every time — applied with Netlify's specific syntax. This is the Netlify-specific companion to ",[23,15489,13849],{"href":14803},[23,15491,5501],{"href":5500},[34,15493,37],{"id":36},[39,15495,15496,15499,15505],{},[42,15497,15498],{},"A site already deploying to Netlify from Astro, Hugo, Eleventy, or Jekyll (all emit content-hashed asset filenames).",[42,15500,15501,15502,15504],{},"Access to the repository so the ",[253,15503,14036],{}," file is version-controlled rather than set in the dashboard.",[42,15506,15507,15509],{},[253,15508,14076],{}," available locally to inspect response headers against the deployed URL.",[55,15511,15512,15563],{},[58,15513,66,15518,66,15521,66,15524],{"viewBox":15514,"role":61,"ariaLabelledBy":15515,"xmlns":65},"0 0 720 280",[15516,15517],"cache-tier-title","cache-tier-desc",[68,15519,15520],{"id":15516},"Two-tier cache policy on Netlify",[72,15522,15523],{"id":15517},"Hashed assets receive a one-year immutable cache, while HTML receives max-age zero with must-revalidate, so a new deploy is reflected immediately while assets stay cached.",[95,15525,78,15526,78,15529,78,15531,78,15534,78,15538,78,15541,78,15543,78,15546,78,15548,78,15552,78,15555,78,15558,78,15560,66],{"style":813},[99,15527,15528],{"x":3474,"y":4630,"fill":103,"style":104},"One _headers file, two cache lifetimes",[107,15530],{"x":3578,"y":849,"width":1462,"height":7852,"rx":113,"fill":185,"opacity":825,"stroke":187,"style":116},[99,15532,15533],{"x":853,"y":4682,"fill":187,"style":121},"Hashed assets",[99,15535,15537],{"x":853,"y":15536,"fill":93,"style":859},"126","app.abc123.js · main.d4f.css",[99,15539,15540],{"x":853,"y":7852,"fill":103,"style":859},"max-age=31536000,",[99,15542,11756],{"x":853,"y":160,"fill":103,"style":859},[99,15544,15545],{"x":853,"y":3500,"fill":93,"style":126},"cached 1 year · never revalidated",[107,15547],{"x":167,"y":849,"width":1462,"height":7852,"rx":113,"fill":2564,"opacity":115,"stroke":2565,"style":116},[99,15549,15551],{"x":15550,"y":4682,"fill":2565,"style":121},"535","HTML documents",[99,15553,15554],{"x":15550,"y":15536,"fill":93,"style":859},"index.html · \u002Fguide\u002F",[99,15556,15557],{"x":15550,"y":7852,"fill":103,"style":859},"max-age=0,",[99,15559,14640],{"x":15550,"y":160,"fill":103,"style":859},[99,15561,15562],{"x":15550,"y":3500,"fill":93,"style":126},"revalidated every request",[218,15564,15565],{},"Hashed asset URLs change on every build, so caching the old ones forever is safe; HTML must revalidate so it points at the current assets.",[34,15567,15569],{"id":15568},"diagnosing-stale-or-wrong-headers","Diagnosing Stale or Wrong Headers",[14,15571,15572,15573,15575],{},"Inspect what Netlify actually sends with ",[253,15574,14076],{}," against your deployed URL — local dev does not fully reproduce edge headers:",[987,15577,15579],{"className":989,"code":15578,"language":991,"meta":712,"style":712},"curl -I https:\u002F\u002Fyour-site.netlify.app\u002Fassets\u002Fapp.abc123.js\n",[253,15580,15581],{"__ignoreMap":712},[995,15582,15583,15585,15587],{"class":997,"line":998},[995,15584,14076],{"class":1007},[995,15586,15089],{"class":1010},[995,15588,15589],{"class":1023}," https:\u002F\u002Fyour-site.netlify.app\u002Fassets\u002Fapp.abc123.js\n",[14,15591,15592,15593,15595,15596,15598],{},"Confirm the ",[253,15594,14837],{}," value matches your intent, and check the deploy log for ",[253,15597,14036],{}," parse warnings — a malformed rule is dropped silently and falls back to Netlify's defaults.",[34,15600,8896,15602,15604],{"id":15601},"the-_headers-file",[253,15603,14036],{}," File",[14,15606,15607,15608,15610,15611,15614],{},"Create ",[253,15609,14036],{}," at the root of your ",[229,15612,15613],{},"publish"," directory (or project root — Netlify copies it). Long-cache hashed assets; revalidate HTML:",[987,15616,15619],{"className":15617,"code":15618,"language":99,"meta":712},[11603],"\u002F*.html\n  Cache-Control: public, max-age=0, must-revalidate\n\u002Fassets\u002F*\n  Cache-Control: public, max-age=31536000, immutable\n\u002F*.js\n  Cache-Control: public, max-age=31536000, immutable\n\u002F*.css\n  Cache-Control: public, max-age=31536000, immutable\n",[253,15620,15618],{"__ignoreMap":712},[14,15622,15623],{},"This works because Astro, Hugo, Eleventy, and Jekyll all emit content-hashed asset filenames — a new build produces new URLs, so caching the old ones forever is safe. Make sure your pipeline preserves those hashes.",[34,15625,15627,15628,15630,15631],{"id":15626},"why-html-needs-must-revalidate-not-no-store","Why HTML Needs ",[253,15629,14640],{},", Not ",[253,15632,15633],{},"no-store",[14,15635,15636,15637,15639,15640,15642],{},"HTML must always reflect the latest build so it references the current hashed assets. Use ",[253,15638,13934],{},": the browser revalidates with the edge before serving, but the response can still be cached at the CDN. Avoid ",[253,15641,15633],{}," — it bypasses the CDN entirely, pushing every request to origin and hurting TTFB.",[34,15644,1166],{"id":1165},[14,15646,15647],{},"On a documentation site with ~30 hashed assets per page, switching from Netlify's defaults to this explicit two-tier policy produced a clear repeat-visit improvement:",[433,15649,15650,15661],{},[436,15651,15652],{},[439,15653,15654,15656,15659],{},[442,15655,940],{},[442,15657,15658],{},"Repeat-visit requests to origin",[442,15660,14092],{},[457,15662,15663,15675],{},[439,15664,15665,15670,15673],{},[462,15666,15667,15668,982],{},"Netlify defaults (no ",[253,15669,14036],{},[462,15671,15672],{},"31 conditional revalidations",[462,15674,14105],{},[439,15676,15677,15682,15684],{},[462,15678,15679,15680,14113],{},"Two-tier policy (",[253,15681,11756],{},[462,15683,14116],{},[462,15685,14119],{},[14,15687,15688,15689,15691],{},"The first visit is identical; the win is entirely on repeat navigation, where ",[253,15690,11756],{}," lets the browser skip revalidation for every hashed asset.",[34,15693,15695],{"id":15694},"validating-the-deploy","Validating the Deploy",[14,15697,15698,15699,15701,15702,15704,15705,15707,15708,738,15710,15712,15713,15715],{},"Netlify automatically invalidates its cache on each successful deploy, so you don't manage purges manually. To verify headers, deploy a preview and ",[253,15700,14623],{}," the asset and an HTML route; confirm the asset shows the one-year ",[253,15703,11756],{}," value and the HTML shows ",[253,15706,13934],{},". (Netlify doesn't expose a simple ",[253,15709,14682],{},[253,15711,14685],{}," cache-status header the way Cloudflare does, so rely on the ",[253,15714,14837],{}," values and the deploy log rather than a status header.)",[34,15717,600],{"id":599},[39,15719,15720,15727,15736,15744,15756],{},[42,15721,15722,15726],{},[229,15723,15724,14585],{},[253,15725,11756],{}," users load an old shell that points at assets that no longer exist → broken pages. Keep HTML revalidated.",[42,15728,15729,15733,15734,239],{},[229,15730,14582,15731,14585],{},[253,15732,14640],{}," browsers may serve a stale document indefinitely. Always pair HTML with ",[253,15735,13934],{},[42,15737,15738,15741,15742,15259],{},[229,15739,15740],{},"Malformed globs:"," Netlify's parser is strict; a bad rule is silently dropped and the path falls back to defaults. Verify with ",[253,15743,14076],{},[42,15745,15746,15749,15750,256,15752,15755],{},[229,15747,15748],{},"Dashboard header rules:"," prefer ",[253,15751,14036],{},[253,15753,15754],{},"netlify.toml",") so the config is version-controlled and reviewable.",[42,15757,15758,15760,15761,15763],{},[229,15759,637],{}," because the policy lives in a committed ",[253,15762,14036],{}," file, reverting is a one-line git revert plus a redeploy — no cache state to untangle, since Netlify re-applies headers on the next deploy.",[34,15765,642],{"id":641},[14,15767,15768,15769,15771,15772,15774,15775,15777,15778,15780,15781,15783],{},"On Netlify the whole task is a correct ",[253,15770,14036],{}," file: ",[253,15773,11756],{}," year-long caching for hashed assets, ",[253,15776,13934],{}," for HTML, and ",[253,15779,14623],{}," on a deploy preview to confirm. Get that file right and Netlify's edge does the rest automatically on every deploy. The same two-tier reasoning applies on every host — see ",[23,15782,14761],{"href":14053}," for the Cloudflare syntax.",[34,15785,651],{"id":650},[653,15787,15789],{"id":15788},"how-do-i-clear-netlifys-cache-after-changing-headers","How do I clear Netlify's cache after changing headers?",[14,15791,15792],{},"You don't need to — Netlify purges on each successful deploy. For a quick local check, hard-refresh or append a throwaway query string to bypass the browser cache.",[653,15794,15796,15797,15799],{"id":15795},"why-are-my-_headers-rules-ignored","Why are my ",[253,15798,14036],{}," rules ignored?",[14,15801,15802,15803,15805],{},"Usually a syntax error or wrong location. Keep it in the publish directory, check the deploy log for parse warnings, and verify with ",[253,15804,14623],{}," after deploy. A malformed rule is dropped silently and the path falls back to Netlify defaults.",[653,15807,15809,15810,2204,15812,15814],{"id":15808},"should-i-use-no-cache-or-no-store-for-html","Should I use ",[253,15811,11760],{},[253,15813,15633],{}," for HTML?",[14,15816,2360,15817,1781,15819,15821,15822,15824],{},[253,15818,11760],{},[253,15820,13934],{},"). ",[253,15823,15633],{}," bypasses the CDN and raises origin load and TTFB, which defeats the purpose of serving from the edge.",[653,15826,15828,15829,15831],{"id":15827},"how-does-netlify-treat-immutable","How does Netlify treat ",[253,15830,11756],{},"?",[14,15833,15834,15835,15837],{},"It honors the directive, so browsers skip conditional revalidation for those hashed assets and serve them straight from cache until the ",[253,15836,14636],{}," expires.",[34,15839,684],{"id":683},[39,15841,15842,15848,15853],{},[42,15843,15844,692,15846,15417],{},[229,15845,691],{},[23,15847,13849],{"href":14803},[42,15849,15850,15852],{},[23,15851,14761],{"href":14053}," — the same policy with Cloudflare syntax.",[42,15854,15855,14747],{},[23,15856,5501],{"href":5500},[1346,15858,15429],{},{"title":712,"searchDepth":713,"depth":713,"links":15860},[15861,15862,15863,15865,15867,15868,15869,15870,15871,15880],{"id":36,"depth":713,"text":37},{"id":15568,"depth":713,"text":15569},{"id":15601,"depth":713,"text":15864},"The _headers File",{"id":15626,"depth":713,"text":15866},"Why HTML Needs must-revalidate, Not no-store",{"id":1165,"depth":713,"text":1166},{"id":15694,"depth":713,"text":15695},{"id":599,"depth":713,"text":600},{"id":641,"depth":713,"text":642},{"id":650,"depth":713,"text":651,"children":15872},[15873,15874,15876,15878],{"id":15788,"depth":730,"text":15789},{"id":15795,"depth":730,"text":15875},"Why are my _headers rules ignored?",{"id":15808,"depth":730,"text":15877},"Should I use no-cache or no-store for HTML?",{"id":15827,"depth":730,"text":15879},"How does Netlify treat immutable?",{"id":683,"depth":713,"text":684},[15882,15883,15884,15885],{"name":737,"item":738},{"name":5501,"item":5500},{"name":13849,"item":14803},{"name":15886,"item":14049},"Proper Cache Headers on Netlify","Configure Cache-Control headers in Netlify _headers files — fingerprinted assets cached for a year, HTML revalidated every request, with exact Netlify syntax.",[15889,15890,15892,15894],{"q":15789,"a":15792},{"q":15875,"a":15891},"Usually a syntax error or wrong location. Keep the file in the publish directory, check the deploy log for parse warnings, and verify with curl -I after deploy. A malformed rule is dropped silently and the path falls back to Netlify defaults.",{"q":15877,"a":15893},"Use no-cache (max-age=0, must-revalidate). no-store bypasses the CDN entirely and raises origin load and TTFB, which defeats the purpose of serving from the edge.",{"q":15879,"a":15895},"It honors the directive, so browsers skip conditional revalidation for those hashed assets and serve them straight from cache until the max-age expires.",{},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fcdn-caching-rules-for-ssgs\u002Fsetting-up-proper-cache-headers-on-netlify",{"title":15473,"description":15887},"performance-optimization-core-web-vitals-for-ssgs\u002Fcdn-caching-rules-for-ssgs\u002Fsetting-up-proper-cache-headers-on-netlify\u002Findex","zvU7SepZRuch15sTu5XFkvFavZwGb-ngJrZgPUNTtkM",{"id":15902,"title":14061,"body":15903,"breadcrumb":16879,"dateModified":743,"datePublished":2446,"description":16883,"extension":745,"faq":16884,"meta":16894,"navigation":752,"path":16895,"seo":16896,"slug":15907,"stem":16897,"type":2460,"__hash__":16898},"content\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Ffont-loading-strategies-for-static-sites\u002Findex.md",{"type":7,"value":15904,"toc":16857},[15905,15908,15918,15924,16018,16022,16028,16047,16056,16060,16074,16081,16168,16178,16184,16194,16247,16253,16259,16316,16328,16332,16347,16393,16413,16417,16426,16561,16568,16572,16631,16635,16638,16657,16671,16673,16723,16725,16763,16765,16769,16783,16787,16797,16801,16804,16808,16814,16818,16821,16825,16828,16830,16854],[10,15906,14061],{"id":15907},"font-loading-strategies-for-static-sites",[14,15909,15910,15911,15914,15915,15917],{},"Web fonts are one of the two most common causes of layout shift and slow text rendering — images being the other. On a static site you have full control to fix both at build time, before a single byte ships. The strategy is four moves: subset the font to the glyphs you use, self-host it to kill a third-party connection, preload only the one critical face above the fold, and set ",[253,15912,15913],{},"font-display"," so text is never invisible. Done well, fonts contribute essentially nothing to Cumulative Layout Shift (CLS) and stop blocking your Largest Contentful Paint (LCP). This guide sits inside ",[23,15916,5501],{"href":5500},", where fonts share the asset-pipeline lever with images.",[14,15919,15920,15921,15923],{},"This page walks the full timeline of a font load, then each fix in turn — baseline, self-hosting, preload and ",[253,15922,15913],{},", subsetting, and metric-matched fallbacks — with config and before\u002Fafter numbers.",[55,15925,15926,16015],{},[58,15927,66,15932,66,15935,66,15938,66,16008],{"viewBox":15928,"role":61,"ariaLabelledBy":15929,"xmlns":65},"0 0 840 380",[15930,15931],"font-tl-title","font-tl-desc",[68,15933,15934],{"id":15930},"Font-loading timeline and its CLS impact",[72,15936,15937],{"id":15931},"Two timelines compared. The unoptimized path shows a render-blocking CSS request, FOIT with invisible text, a late font swap and a large layout shift. The optimized path shows a preloaded self-hosted font, FOUT with a metric-matched fallback, an early swap and near-zero CLS.",[95,15939,78,15940,78,15943,78,15946,78,15948,78,15950,78,15954,78,15956,78,15959,78,15961,78,15964,78,15968,78,15970,78,15974,78,15977,78,15980,78,15983,78,15985,78,15989,78,15991,78,15994,78,15997,78,15999,78,16002,78,16005,66],{"style":813},[99,15941,15942],{"x":5338,"y":2521,"fill":103,"style":1416},"Same font, two loading paths",[99,15944,15945],{"x":5393,"y":828,"fill":2565,"style":2597},"Unoptimized",[997,15947],{"x1":5393,"y1":2527,"x2":2516,"y2":2527,"stroke":2592,"style":116},[107,15949],{"x":5393,"y":6849,"width":161,"height":4630,"rx":876,"fill":2564,"opacity":850,"stroke":2565,"style":878},[99,15951,15953],{"x":15952,"y":1431,"fill":103,"style":126},"95","blocking CSS req",[107,15955],{"x":160,"y":6849,"width":142,"height":4630,"rx":876,"fill":162,"opacity":877,"stroke":164,"style":878},[99,15957,15958],{"x":820,"y":1431,"fill":103,"style":126},"FOIT — invisible text",[107,15960],{"x":167,"y":6849,"width":161,"height":4630,"rx":876,"fill":114,"opacity":186,"stroke":114,"style":878},[99,15962,15963],{"x":3518,"y":1431,"fill":103,"style":126},"font downloads",[90,15965],{"d":15966,"stroke":2565,"fill":205,"style":15967},"M545 115 L585 115","stroke-width:2px;marker-end:url(#font-arrow)",[107,15969],{"x":7842,"y":6849,"width":160,"height":4630,"rx":876,"fill":2564,"opacity":850,"stroke":2565,"style":878},[99,15971,15973],{"x":15972,"y":1431,"fill":2565,"style":882},"680","late swap → big shift",[99,15975,15976],{"x":15972,"y":6153,"fill":2565,"style":126},"CLS ≈ 0.18",[99,15978,15979],{"x":5393,"y":146,"fill":187,"style":2597},"Optimized",[997,15981],{"x1":5393,"y1":15982,"x2":2516,"y2":15982,"stroke":2592,"style":116},"236",[107,15984],{"x":5393,"y":6872,"width":161,"height":4630,"rx":876,"fill":824,"opacity":186,"stroke":824,"style":878},[99,15986,15988],{"x":15952,"y":15987,"fill":103,"style":126},"264","preload (self-host)",[107,15990],{"x":160,"y":6872,"width":160,"height":4630,"rx":876,"fill":185,"opacity":850,"stroke":187,"style":878},[99,15992,15993],{"x":5332,"y":15987,"fill":103,"style":126},"FOUT — fallback shown",[90,15995],{"d":15996,"stroke":187,"fill":205,"style":15967},"M365 259 L405 259",[107,15998],{"x":1415,"y":6872,"width":142,"height":4630,"rx":876,"fill":185,"opacity":850,"stroke":187,"style":878},[99,16000,16001],{"x":906,"y":15987,"fill":187,"style":882},"early swap, metric-matched",[99,16003,16004],{"x":906,"y":5421,"fill":187,"style":126},"CLS ≈ 0.00",[99,16006,16007],{"x":5338,"y":215,"fill":93,"style":126},"Preload + a size-adjusted fallback turns a visible reflow into an invisible swap.",[76,16009,78,16010,66],{},[80,16011,88,16013,78],{"id":16012,"viewBox":83,"refX":84,"refY":85,"markerWidth":86,"markerHeight":86,"orient":87},"font-arrow",[90,16014],{"d":92,"fill":93},[218,16016,16017],{},"The unoptimized path blocks on a CSS request, hides text (FOIT), then swaps late into a big layout shift; the optimized path preloads a self-hosted font and swaps early into a metric-matched fallback, so CLS stays near zero.",[34,16019,16021],{"id":16020},"baseline-the-current-cost","Baseline the Current Cost",[14,16023,16024,16025,16027],{},"Measure before changing anything. Run Lighthouse against a deployed URL and note total font transfer size, render-blocking requests, and whether ",[253,16026,15913],{}," is set:",[987,16029,16031],{"className":989,"code":16030,"language":991,"meta":712,"style":712},"lighthouse https:\u002F\u002Fyour-site.example.com --output=json --output-path=audit.json\n",[253,16032,16033],{"__ignoreMap":712},[995,16034,16035,16038,16041,16044],{"class":997,"line":998},[995,16036,16037],{"class":1007},"lighthouse",[995,16039,16040],{"class":1023}," https:\u002F\u002Fyour-site.example.com",[995,16042,16043],{"class":1010}," --output=json",[995,16045,16046],{"class":1010}," --output-path=audit.json\n",[14,16048,16049,16050,270,16052,16055],{},"In the JSON, look at the ",[253,16051,15913],{},[253,16053,16054],{},"render-blocking-resources"," audits, and in the network waterfall identify which weights and styles actually appear above the fold. Most sites load far more font than the initial viewport needs — extra weights, italic variants, and scripts that never render before the user scrolls. Write down the current CLS and LCP for the page with the most prominent typography; those are the two numbers every fix below should move.",[34,16057,16059],{"id":16058},"self-host-instead-of-the-google-fonts-cdn","Self-Host Instead of the Google Fonts CDN",[14,16061,16062,16063,16066,16067,16070,16071,239],{},"The single highest-leverage change is to stop loading fonts from a third party. The classic Google Fonts embed costs you a render-blocking stylesheet request to ",[253,16064,16065],{},"fonts.googleapis.com",", a DNS lookup and TLS handshake to ",[253,16068,16069],{},"fonts.gstatic.com",", and a font URL you cannot reliably preload because it is generated server-side. Self-hosting removes all of that: the font lives on your origin, ships from the same edge cache as the rest of the site, and gets a stable filename you can ",[23,16072,16073],{"href":14803},"cache for a year as immutable",[14,16075,16076,16077,16080],{},"Download the ",[253,16078,16079],{},"woff2"," files once, commit them to your repo, and declare them locally:",[987,16082,16086],{"className":16083,"code":16084,"language":16085,"meta":712,"style":712},"language-css shiki shiki-themes github-light github-dark","@font-face {\n  font-family: 'Inter';\n  src: url('\u002Ffonts\u002Finter-latin.woff2') format('woff2');\n  font-weight: 400 700;          \u002F* a variable font covers the range in one file *\u002F\n  font-display: swap;\n}\n","css",[253,16087,16088,16095,16107,16134,16152,16164],{"__ignoreMap":712},[995,16089,16090,16093],{"class":997,"line":998},[995,16091,16092],{"class":1614},"@font-face",[995,16094,8802],{"class":1618},[995,16096,16097,16100,16102,16105],{"class":997,"line":713},[995,16098,16099],{"class":1010},"  font-family",[995,16101,1925],{"class":1618},[995,16103,16104],{"class":1023},"'Inter'",[995,16106,1628],{"class":1618},[995,16108,16109,16112,16114,16117,16119,16122,16124,16127,16129,16132],{"class":997,"line":730},[995,16110,16111],{"class":1010},"  src",[995,16113,1925],{"class":1618},[995,16115,16116],{"class":1010},"url",[995,16118,1799],{"class":1618},[995,16120,16121],{"class":1023},"'\u002Ffonts\u002Finter-latin.woff2'",[995,16123,1811],{"class":1618},[995,16125,16126],{"class":1010},"format",[995,16128,1799],{"class":1618},[995,16130,16131],{"class":1023},"'woff2'",[995,16133,5829],{"class":1618},[995,16135,16136,16139,16141,16143,16146,16149],{"class":997,"line":1544},[995,16137,16138],{"class":1010},"  font-weight",[995,16140,1925],{"class":1618},[995,16142,101],{"class":1010},[995,16144,16145],{"class":1010}," 700",[995,16147,16148],{"class":1618},";          ",[995,16150,16151],{"class":1001},"\u002F* a variable font covers the range in one file *\u002F\n",[995,16153,16154,16157,16159,16162],{"class":997,"line":1550},[995,16155,16156],{"class":1010},"  font-display",[995,16158,1925],{"class":1618},[995,16160,16161],{"class":1010},"swap",[995,16163,1628],{"class":1618},[995,16165,16166],{"class":997,"line":1673},[995,16167,9008],{"class":1618},[14,16169,16170,16171,16173,16174,239],{},"Measured on a content site, replacing the Google Fonts embed with self-hosted ",[253,16172,16079],{}," removed two cross-origin connections and cut LCP from 2.4 s to 1.9 s on a throttled mobile profile — before any other font work. The full step-by-step, including how it eliminates layout shift, is in ",[23,16175,16177],{"href":16176},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Ffont-loading-strategies-for-static-sites\u002Fself-hosting-google-fonts-to-eliminate-layout-shift\u002F","Self-Hosting Google Fonts to Eliminate Layout Shift",[34,16179,16181,16182],{"id":16180},"preloading-font-display","Preloading & ",[253,16183,15913],{},[14,16185,16186,16187,16190,16191,16193],{},"Preload ",[229,16188,16189],{},"only"," the one face that renders above the fold, and always set ",[253,16192,15913],{}," so the browser shows fallback text immediately rather than hiding it:",[987,16195,16199],{"className":16196,"code":16197,"language":16198,"meta":712,"style":712},"language-html shiki shiki-themes github-light github-dark","\u003Clink rel=\"preload\" href=\"\u002Ffonts\u002Finter-latin.woff2\" as=\"font\" type=\"font\u002Fwoff2\" crossorigin>\n","html",[253,16200,16201],{"__ignoreMap":712},[995,16202,16203,16206,16209,16212,16214,16217,16220,16222,16225,16228,16230,16233,16236,16238,16241,16244],{"class":997,"line":998},[995,16204,16205],{"class":1618},"\u003C",[995,16207,16208],{"class":1921},"link",[995,16210,16211],{"class":1007}," rel",[995,16213,7317],{"class":1618},[995,16215,16216],{"class":1023},"\"preload\"",[995,16218,16219],{"class":1007}," href",[995,16221,7317],{"class":1618},[995,16223,16224],{"class":1023},"\"\u002Ffonts\u002Finter-latin.woff2\"",[995,16226,16227],{"class":1007}," as",[995,16229,7317],{"class":1618},[995,16231,16232],{"class":1023},"\"font\"",[995,16234,16235],{"class":1007}," type",[995,16237,7317],{"class":1618},[995,16239,16240],{"class":1023},"\"font\u002Fwoff2\"",[995,16242,16243],{"class":1007}," crossorigin",[995,16245,16246],{"class":1618},">\n",[14,16248,16249,16252],{},[253,16250,16251],{},"crossorigin"," is mandatory on a font preload even for a same-origin file — fonts are always fetched in CORS mode, and without the attribute the preload request does not match the real font request, so the browser downloads the font twice and wastes the hint entirely.",[14,16254,16255,16256,16258],{},"For ",[253,16257,15913],{},", the practical policy is:",[433,16260,16261,16274],{},[436,16262,16263],{},[439,16264,16265,16268,16271],{},[442,16266,16267],{},"Value",[442,16269,16270],{},"Behavior",[442,16272,16273],{},"Use for",[457,16275,16276,16288,16301],{},[439,16277,16278,16282,16285],{},[462,16279,16280],{},[253,16281,16161],{},[462,16283,16284],{},"Fallback shown immediately, swaps to web font when ready (FOUT)",[462,16286,16287],{},"Body and heading text",[439,16289,16290,16295,16298],{},[462,16291,16292],{},[253,16293,16294],{},"optional",[462,16296,16297],{},"Fallback shown; web font used only if it loads almost instantly",[462,16299,16300],{},"Non-critical or decorative faces",[439,16302,16303,16310,16313],{},[462,16304,16305,3270,16308],{},[253,16306,16307],{},"block",[253,16309,12831],{},[462,16311,16312],{},"Text hidden up to ~3 s waiting for the font (FOIT)",[462,16314,16315],{},"Avoid",[14,16317,16318,16320,16321,16324,16325,16327],{},[253,16319,16161],{}," keeps text readable from the first paint, which is what you want for content. The remaining risk is the ",[18,16322,16323],{},"shift"," when the real font swaps in — and that is solved by a metric-matched fallback, below. Reserve ",[253,16326,16294],{}," for faces where you would rather never shift than guarantee the custom font shows.",[34,16329,16331],{"id":16330},"subsetting-in-the-build","Subsetting in the Build",[14,16333,16334,16335,16338,16339,16342,16343,16346],{},"Most of a font's weight is glyphs you never render. Strip them with ",[253,16336,16337],{},"fonttools","' ",[253,16340,16341],{},"pyftsubset",", which processes ",[229,16344,16345],{},"one font file at a time"," to a single output:",[987,16348,16350],{"className":989,"code":16349,"language":991,"meta":712,"style":712},"pyftsubset inter.woff2 \\\n  --unicodes=\"U+0000-00FF,U+0131,U+0152-0153,U+2000-206F,U+2212\" \\\n  --layout-features=\"kern,liga\" \\\n  --flavor=woff2 \\\n  --output-file=inter-latin.woff2\n",[253,16351,16352,16361,16371,16381,16388],{"__ignoreMap":712},[995,16353,16354,16356,16359],{"class":997,"line":998},[995,16355,16341],{"class":1007},[995,16357,16358],{"class":1023}," inter.woff2",[995,16360,3002],{"class":1010},[995,16362,16363,16366,16369],{"class":997,"line":713},[995,16364,16365],{"class":1010},"  --unicodes=",[995,16367,16368],{"class":1023},"\"U+0000-00FF,U+0131,U+0152-0153,U+2000-206F,U+2212\"",[995,16370,3002],{"class":1010},[995,16372,16373,16376,16379],{"class":997,"line":730},[995,16374,16375],{"class":1010},"  --layout-features=",[995,16377,16378],{"class":1023},"\"kern,liga\"",[995,16380,3002],{"class":1010},[995,16382,16383,16386],{"class":997,"line":1544},[995,16384,16385],{"class":1010},"  --flavor=woff2",[995,16387,3002],{"class":1010},[995,16389,16390],{"class":997,"line":1550},[995,16391,16392],{"class":1010},"  --output-file=inter-latin.woff2\n",[14,16394,16395,16396,16398,16399,16402,16403,16406,16407,16409,16410,16412],{},"Wire it into the build with a small script that loops over each face — ",[253,16397,16341],{}," has no directory or glob mode — and have your generator copy the subset output into its public directory. Keep the layout features you actually use (",[253,16400,16401],{},"kern"," for kerning, ",[253,16404,16405],{},"liga"," for standard ligatures); dropping them shaves a little more but can visibly degrade text. Coordinate the step with ",[23,16408,2190],{"href":2189}," so every heavy asset is processed in one build pass. For multi-script sites, run ",[253,16411,16341],{}," per locale with locale-specific Unicode ranges and load each subset only on the pages that need it. Measure the byte delta on your own files rather than assuming a fixed percentage.",[34,16414,16416],{"id":16415},"metric-matched-fallbacks","Metric-Matched Fallbacks",[14,16418,16419,16420,16422,16423,16425],{},"A ",[253,16421,16161],{}," with a fallback that has different metrics — different x-height, character width, or line height — still shifts layout when the real font arrives, because the text reflows to a new size. Eliminate that by declaring a fallback ",[253,16424,16092],{}," over a system font with override descriptors tuned to match the web font's metrics:",[987,16427,16429],{"className":16083,"code":16428,"language":16085,"meta":712,"style":712},"@font-face {\n  font-family: 'Inter Fallback';\n  src: local('Arial');\n  size-adjust: 107%;\n  ascent-override: 90%;\n  descent-override: 22%;\n  line-gap-override: 0%;\n}\n\nbody {\n  font-family: 'Inter', 'Inter Fallback', system-ui, sans-serif;\n}\n",[253,16430,16431,16437,16448,16464,16479,16492,16506,16519,16523,16527,16533,16557],{"__ignoreMap":712},[995,16432,16433,16435],{"class":997,"line":998},[995,16434,16092],{"class":1614},[995,16436,8802],{"class":1618},[995,16438,16439,16441,16443,16446],{"class":997,"line":713},[995,16440,16099],{"class":1010},[995,16442,1925],{"class":1618},[995,16444,16445],{"class":1023},"'Inter Fallback'",[995,16447,1628],{"class":1618},[995,16449,16450,16452,16454,16457,16459,16462],{"class":997,"line":730},[995,16451,16111],{"class":1010},[995,16453,1925],{"class":1618},[995,16455,16456],{"class":1010},"local",[995,16458,1799],{"class":1618},[995,16460,16461],{"class":1023},"'Arial'",[995,16463,5829],{"class":1618},[995,16465,16466,16469,16471,16474,16477],{"class":997,"line":1544},[995,16467,16468],{"class":1010},"  size-adjust",[995,16470,1925],{"class":1618},[995,16472,16473],{"class":1010},"107",[995,16475,16476],{"class":1614},"%",[995,16478,1628],{"class":1618},[995,16480,16481,16484,16486,16488,16490],{"class":997,"line":1550},[995,16482,16483],{"class":1010},"  ascent-override",[995,16485,1925],{"class":1618},[995,16487,873],{"class":1010},[995,16489,16476],{"class":1614},[995,16491,1628],{"class":1618},[995,16493,16494,16497,16499,16502,16504],{"class":997,"line":1673},[995,16495,16496],{"class":1010},"  descent-override",[995,16498,1925],{"class":1618},[995,16500,16501],{"class":1010},"22",[995,16503,16476],{"class":1614},[995,16505,1628],{"class":1618},[995,16507,16508,16511,16513,16515,16517],{"class":997,"line":1678},[995,16509,16510],{"class":1010},"  line-gap-override",[995,16512,1925],{"class":1618},[995,16514,2515],{"class":1010},[995,16516,16476],{"class":1614},[995,16518,1628],{"class":1618},[995,16520,16521],{"class":997,"line":1693},[995,16522,9008],{"class":1618},[995,16524,16525],{"class":997,"line":1705},[995,16526,1541],{"emptyLinePlaceholder":752},[995,16528,16529,16531],{"class":997,"line":1711},[995,16530,10255],{"class":1921},[995,16532,8802],{"class":1618},[995,16534,16535,16537,16539,16541,16543,16545,16547,16550,16552,16555],{"class":997,"line":1717},[995,16536,16099],{"class":1010},[995,16538,1925],{"class":1618},[995,16540,16104],{"class":1023},[995,16542,1850],{"class":1618},[995,16544,16445],{"class":1023},[995,16546,1850],{"class":1618},[995,16548,16549],{"class":1010},"system-ui",[995,16551,1850],{"class":1618},[995,16553,16554],{"class":1010},"sans-serif",[995,16556,1628],{"class":1618},[995,16558,16559],{"class":997,"line":1726},[995,16560,9008],{"class":1618},[14,16562,16563,16564,16567],{},"With the fallback occupying the same space as ",[253,16565,16566],{},"Inter",", the swap from fallback to web font moves no pixels — the visible glyphs change but the box does not. On a documentation hero this is the difference between a measured CLS of 0.18 and 0.00. Tooling can compute the override values for you, but the principle is simple: make the fallback the same size as the real thing.",[653,16569,16571],{"id":16570},"beforeafter","Before\u002Fafter",[433,16573,16574,16589],{},[436,16575,16576],{},[439,16577,16578,16581,16584,16587],{},[442,16579,16580],{},"Stage",[442,16582,16583],{},"Font bytes (above fold)",[442,16585,16586],{},"CLS",[442,16588,10936],{},[457,16590,16591,16604,16618],{},[439,16592,16593,16596,16599,16601],{},[462,16594,16595],{},"Google Fonts embed, no preload",[462,16597,16598],{},"142 KB",[462,16600,886],{},[462,16602,16603],{},"2.4 s",[439,16605,16606,16611,16613,16615],{},[462,16607,16608,16609],{},"Self-hosted + preload + ",[253,16610,16161],{},[462,16612,16598],{},[462,16614,11992],{},[462,16616,16617],{},"1.9 s",[439,16619,16620,16623,16626,16629],{},[462,16621,16622],{},"+ subset to Latin + metric-matched fallback",[462,16624,16625],{},"38 KB",[462,16627,16628],{},"0.00",[462,16630,1206],{},[34,16632,16634],{"id":16633},"validation-monitoring","Validation & Monitoring",[14,16636,16637],{},"Gate deploys on a font-aware Lighthouse budget and watch the field data too:",[987,16639,16641],{"className":989,"code":16640,"language":991,"meta":712,"style":712},"lhci autorun --collect.settings.preset=desktop --assert.preset=lighthouse:recommended\n",[253,16642,16643],{"__ignoreMap":712},[995,16644,16645,16648,16651,16654],{"class":997,"line":998},[995,16646,16647],{"class":1007},"lhci",[995,16649,16650],{"class":1023}," autorun",[995,16652,16653],{"class":1010}," --collect.settings.preset=desktop",[995,16655,16656],{"class":1010}," --assert.preset=lighthouse:recommended\n",[14,16658,16659,16660,16663,16664,16667,16668,16670],{},"Fail the build when ",[253,16661,16662],{},"cumulative-layout-shift"," exceeds 0.1, track ",[253,16665,16666],{},"largest-contentful-paint"," for hero typography, and confirm the preloaded face is being used (not refetched) by checking the network panel against a deployed preview. Add Web Vitals RUM so you see real fallback behavior on slow connections, where a missing ",[253,16669,15913],{}," or an unmatched fallback hurts most.",[34,16672,2266],{"id":2265},[39,16674,16675,16681,16689,16703,16709,16715],{},[42,16676,16677,16680],{},[229,16678,16679],{},"Over-preloading:"," preloading every weight competes with critical CSS and JS and delays LCP. Preload only the one face visible first.",[42,16682,16683,16688],{},[229,16684,16685,16686,931],{},"Missing ",[253,16687,16251],{}," omitting it on a font preload causes a duplicate fetch and wastes the preload entirely.",[42,16690,16691,16696,16697,16700,16701,239],{},[229,16692,16693,16695],{},[253,16694,16161],{}," without a tuned fallback:"," a fallback with different metrics still reflows the page on swap. Add ",[253,16698,16699],{},"size-adjust"," and the override descriptors, or use ",[253,16702,16294],{},[42,16704,16705,16708],{},[229,16706,16707],{},"Loading the Google Fonts CDN for a font you could self-host:"," it adds a third-party connection and an unpreloadable URL on the critical path.",[42,16710,16711,16714],{},[229,16712,16713],{},"Shipping the full glyph set:"," a multi-script font you use only in Latin wastes most of its bytes. Subset it.",[42,16716,16717,16720,16721,239],{},[229,16718,16719],{},"Serving fonts without long caching:"," a self-hosted font that revalidates on every visit throws away half the benefit. Cache it as ",[253,16722,11756],{},[34,16724,2321],{"id":2320},[39,16726,16727,16730,16735,16745,16748,16760],{},[42,16728,16729],{},"Self-host fonts to remove a third-party connection and earn a preloadable, long-cacheable URL.",[42,16731,16732,16733,239],{},"Preload only the one above-the-fold face, always with ",[253,16734,16251],{},[42,16736,16737,16738,16741,16742,16744],{},"Set ",[253,16739,16740],{},"font-display: swap"," for content text; ",[253,16743,16294],{}," only for non-critical faces.",[42,16746,16747],{},"Subset to the glyph range you actually render — measure the byte delta on your own files.",[42,16749,16750,16751,1850,16753,1850,16756,16759],{},"Kill the swap shift with a metric-matched fallback (",[253,16752,16699],{},[253,16754,16755],{},"ascent-override",[253,16757,16758],{},"descent-override","); aim for CLS 0.00.",[42,16761,16762],{},"Gate it all on a Lighthouse CLS budget so a future font addition can't silently regress Core Web Vitals.",[34,16764,651],{"id":650},[653,16766,16768],{"id":16767},"which-font-display-value-should-i-use","Which font-display value should I use?",[14,16770,2360,16771,16773,16774,16776,16777,16779,16780,16782],{},[253,16772,16161],{}," for body and heading text so the page is readable immediately with a fallback, and switches to the web font when it loads. Use ",[253,16775,16294],{}," for non-critical or decorative faces where you would rather never shift layout than guarantee the custom font appears. Avoid ",[253,16778,16307],{}," and the default ",[253,16781,12831],{},", which can hide text for up to three seconds.",[653,16784,16786],{"id":16785},"how-do-i-stop-fonts-from-causing-layout-shift","How do I stop fonts from causing layout shift?",[14,16788,16789,16790,1850,16792,3706,16794,16796],{},"Two moves. Preload the one critical face so it arrives before first paint, and define a metric-matched fallback with ",[253,16791,16699],{},[253,16793,16755],{},[253,16795,16758],{}," so the fallback occupies the same space as the web font. With both in place the swap is invisible and CLS stays near zero.",[653,16798,16800],{"id":16799},"should-i-self-host-or-use-google-fonts-cdn","Should I self-host or use Google Fonts CDN?",[14,16802,16803],{},"Self-host. It removes a third-party connection and DNS lookup on the critical path, lets you preload with the correct same-origin URL, and gives you a stable filename you can cache for a year. Self-hosting also avoids a render-blocking stylesheet request to fonts.googleapis.com before any font even downloads.",[653,16805,16807],{"id":16806},"why-is-crossorigin-required-on-a-font-preload","Why is crossorigin required on a font preload?",[14,16809,16810,16811,16813],{},"Fonts are always fetched in CORS mode, even same-origin. If the preload link omits ",[253,16812,16251],{},", its request does not match the actual font request, so the browser downloads the font twice and wastes the preload entirely.",[653,16815,16817],{"id":16816},"how-much-can-subsetting-save","How much can subsetting save?",[14,16819,16820],{},"It depends on the font and the glyph range you keep. A full variable font covering many scripts can be several hundred kilobytes; restricting it to the Latin range you actually use commonly removes a large share of that. Measure your own before-and-after with the byte size of the output file rather than assuming a fixed percentage.",[653,16822,16824],{"id":16823},"do-variable-fonts-help-performance","Do variable fonts help performance?",[14,16826,16827],{},"Often yes, when you need several weights of the same family. One variable file replaces multiple static weight files, so you ship one request instead of four or five. If you only use a single weight, a subset static font can still be smaller. Measure both.",[34,16829,684],{"id":683},[39,16831,16832,16839,16844,16849],{},[42,16833,16834,692,16836,16838],{},[229,16835,691],{},[23,16837,5501],{"href":5500}," — where fonts fit the LCP and CLS picture.",[42,16840,16841,16843],{},[23,16842,16177],{"href":16176}," — the full self-hosting and metric-matching recipe.",[42,16845,16846,16848],{},[23,16847,2190],{"href":2189}," — the other asset that drives LCP and CLS.",[42,16850,16851,16853],{},[23,16852,13849],{"href":14803}," — cache your self-hosted fonts as immutable for a year.",[1346,16855,16856],{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}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 .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}",{"title":712,"searchDepth":713,"depth":713,"links":16858},[16859,16860,16861,16863,16864,16867,16868,16869,16870,16878],{"id":16020,"depth":713,"text":16021},{"id":16058,"depth":713,"text":16059},{"id":16180,"depth":713,"text":16862},"Preloading & font-display",{"id":16330,"depth":713,"text":16331},{"id":16415,"depth":713,"text":16416,"children":16865},[16866],{"id":16570,"depth":730,"text":16571},{"id":16633,"depth":713,"text":16634},{"id":2265,"depth":713,"text":2266},{"id":2320,"depth":713,"text":2321},{"id":650,"depth":713,"text":651,"children":16871},[16872,16873,16874,16875,16876,16877],{"id":16767,"depth":730,"text":16768},{"id":16785,"depth":730,"text":16786},{"id":16799,"depth":730,"text":16800},{"id":16806,"depth":730,"text":16807},{"id":16816,"depth":730,"text":16817},{"id":16823,"depth":730,"text":16824},{"id":683,"depth":713,"text":684},[16880,16881,16882],{"name":737,"item":738},{"name":5501,"item":5500},{"name":14061,"item":14060},"Eliminate font-driven layout shift and slow text: subset, self-host, preload the critical face, and set font-display deliberately — all at build time on a static site.",[16885,16887,16889,16890,16892,16893],{"q":16768,"a":16886},"Use swap for body and heading text so the page is readable immediately with a fallback, and switches to the web font when it loads. Use optional for non-critical or decorative faces where you would rather never shift layout than guarantee the custom font appears. Avoid block and the default auto, which can hide text for up to three seconds.",{"q":16786,"a":16888},"Two moves. Preload the one critical face so it arrives before first paint, and define a metric-matched fallback with size-adjust, ascent-override, and descent-override so the fallback occupies the same space as the web font. With both in place the swap is invisible and CLS stays near zero.",{"q":16800,"a":16803},{"q":16807,"a":16891},"Fonts are always fetched in CORS mode, even same-origin. If the preload link omits crossorigin, its request does not match the actual font request, so the browser downloads the font twice and wastes the preload entirely.",{"q":16817,"a":16820},{"q":16824,"a":16827},{},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Ffont-loading-strategies-for-static-sites",{"title":14061,"description":16883},"performance-optimization-core-web-vitals-for-ssgs\u002Ffont-loading-strategies-for-static-sites\u002Findex","p_knSCa5kIehwhfzF9MAh8ceNTk9AvHgKPjp4BCV7RA",{"id":16900,"title":16901,"body":16902,"breadcrumb":17723,"dateModified":743,"datePublished":743,"description":17728,"extension":745,"faq":17729,"meta":17737,"navigation":752,"path":17738,"seo":17739,"slug":16906,"stem":17740,"type":756,"__hash__":17741},"content\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Ffont-loading-strategies-for-static-sites\u002Fself-hosting-google-fonts-to-eliminate-layout-shift\u002Findex.md","Self-Host Google Fonts to Eliminate Layout Shift",{"type":7,"value":16903,"toc":17705},[16904,16907,16922,16924,16943,16947,16957,17040,17044,17057,17129,17152,17156,17165,17392,17407,17411,17414,17454,17460,17462,17468,17539,17544,17546,17616,17618,17627,17629,17633,17636,17640,17646,17650,17663,17667,17670,17674,17677,17679,17702],[10,16905,16177],{"id":16906},"self-hosting-google-fonts-to-eliminate-layout-shift",[14,16908,16909,16910,16912,16913,16915,16916,16918,16919,16921],{},"Loading fonts from ",[253,16911,16065],{}," looks free, but it costs you a third-party connection and a chunk of Cumulative Layout Shift (CLS). The fix is to self-host: download the font files, subset them, preload the critical weight, and pair ",[253,16914,16740],{}," with a metric-matched fallback so the swap is invisible. This is a font-specific recipe within ",[23,16917,14061],{"href":14060},", part of the broader ",[23,16920,5501],{"href":5500}," effort. Fonts are, alongside images, one of the two assets most responsible for CLS and a slow LCP.",[34,16923,37],{"id":36},[39,16925,16926,16934,16937],{},[42,16927,16928,16929,8912,16932,239],{},"A static site (Astro, Hugo, Eleventy, or Jekyll) currently loading a Google Fonts stylesheet via a ",[253,16930,16931],{},"\u003Clink>",[253,16933,16065],{},[42,16935,16936],{},"A way to place files in the build output and reference them with a stable, hashed path so they can be cached for a year.",[42,16938,16939,16940,16942],{},"Lighthouse or ",[253,16941,16647],{}," plus Chrome DevTools to measure CLS and the third-party connection before and after.",[34,16944,16946],{"id":16945},"why-the-hosted-stylesheet-hurts","Why the Hosted Stylesheet Hurts",[14,16948,16949,16950,16953,16954,16956],{},"When the browser parses ",[253,16951,16952],{},"\u003Clink href=\"https:\u002F\u002Ffonts.googleapis.com\u002Fcss2?family=Inter...\">",", it must do a DNS lookup, a TLS handshake, and a CSS round trip to a separate origin before it even learns the font file URL — which lives on yet another origin, ",[253,16955,16069],{},". While all of that resolves, text paints in a fallback font with different metrics. When the web font finally arrives, the line box resizes and the text reflows. That reflow is exactly what the layout-instability API records as CLS.",[55,16958,16959,17037],{},[58,16960,66,16965,66,16968,66,16971],{"viewBox":16961,"role":61,"ariaLabelledBy":16962,"xmlns":65},"0 0 760 300",[16963,16964],"selffont-tl-title","selffont-tl-desc",[68,16966,16967],{"id":16963},"Third-party versus self-hosted font load timeline",[72,16969,16970],{"id":16964},"The third-party timeline spends time on DNS, TLS, and a CSS round trip to two Google origins before the font paints, causing a reflow; the self-hosted timeline preloads the file from the same origin and paints once with no shift.",[95,16972,78,16973,78,16976,78,16979,78,16981,78,16986,78,16989,78,16993,78,16996,78,17000,78,17003,78,17007,78,17010,78,17013,78,17015,78,17018,78,17021,78,17025,78,17028,78,17030,78,17033,66],{"style":97},[99,16974,16975],{"x":816,"y":102,"fill":103,"style":104},"Same origin, preloaded, paints once",[99,16977,16978],{"x":5393,"y":1430,"fill":2565,"style":2597},"Google CDN",[107,16980],{"x":1431,"y":1420,"width":873,"height":5418,"rx":876,"fill":2564,"opacity":850,"stroke":2565,"style":878},[99,16982,16985],{"x":16983,"y":16984,"fill":103,"style":4658},"165","82","DNS+TLS",[107,16987],{"x":16988,"y":1420,"width":159,"height":5418,"rx":876,"fill":2564,"opacity":850,"stroke":2565,"style":878},"216",[99,16990,16992],{"x":16991,"y":16984,"fill":103,"style":4658},"271","CSS round trip",[107,16994],{"x":3586,"y":1420,"width":4682,"height":5418,"rx":876,"fill":162,"opacity":16995,"stroke":164,"style":878},"0.28",[99,16997,16999],{"x":16998,"y":16984,"fill":103,"style":4658},"382","font fetch",[107,17001],{"x":17002,"y":1420,"width":873,"height":5418,"rx":876,"fill":2564,"opacity":163,"stroke":2565,"style":878},"438",[99,17004,17006],{"x":17005,"y":16984,"fill":103,"style":4658},"483","REFLOW",[99,17008,17009],{"x":9750,"y":16984,"fill":2565,"style":2624},"CLS 0.14",[99,17011,17012],{"x":5393,"y":175,"fill":187,"style":2597},"Self-hosted",[107,17014],{"x":1431,"y":11232,"width":7852,"height":5418,"rx":876,"fill":185,"opacity":163,"stroke":187,"style":878},[99,17016,17017],{"x":142,"y":198,"fill":103,"style":4658},"preload (same origin)",[107,17019],{"x":17020,"y":11232,"width":159,"height":5418,"rx":876,"fill":185,"opacity":163,"stroke":187,"style":878},"286",[99,17022,17024],{"x":17023,"y":198,"fill":103,"style":4658},"341","paint, no shift",[99,17026,17027],{"x":9750,"y":198,"fill":187,"style":2624},"CLS 0.01",[997,17029],{"x1":1431,"y1":184,"x2":3571,"y2":184,"stroke":2592,"style":116},[99,17031,17032],{"x":1431,"y":8714,"fill":93,"style":11285},"0 ms",[99,17034,17036],{"x":15972,"y":8714,"fill":93,"style":17035},"font-size:11px;text-anchor:end","time →",[218,17038,17039],{},"The hosted path spends time on two extra origins before a reflow; the self-hosted path preloads from your own domain and the metric-matched fallback paints once.",[34,17041,17043],{"id":17042},"step-1-download-and-subset","Step 1: Download and Subset",[14,17045,17046,17047,17050,17051,17053,17054,17056],{},"Pull the exact files you use. The simplest reliable route is the ",[253,17048,17049],{},"google-webfonts-helper"," tool or ",[253,17052,16337],{},". With ",[253,17055,16337],{}," you can also subset to the unicode ranges you actually serve, which shrinks the file substantially:",[987,17058,17060],{"className":989,"code":17059,"language":991,"meta":712,"style":712},"pip install fonttools brotli\n# Subset Inter regular + bold to the latin range, output woff2\npyftsubset Inter-Regular.ttf \\\n  --unicodes=\"U+0000-00FF,U+2013-2014,U+2018-201A,U+201C-201E,U+2022,U+2026\" \\\n  --flavor=woff2 --output-file=inter-regular.woff2\npyftsubset Inter-Bold.ttf \\\n  --unicodes=\"U+0000-00FF,U+2013-2014,U+2018-201A,U+201C-201E,U+2022,U+2026\" \\\n  --flavor=woff2 --output-file=inter-bold.woff2\n",[253,17061,17062,17075,17080,17089,17098,17105,17114,17122],{"__ignoreMap":712},[995,17063,17064,17067,17069,17072],{"class":997,"line":998},[995,17065,17066],{"class":1007},"pip",[995,17068,1555],{"class":1023},[995,17070,17071],{"class":1023}," fonttools",[995,17073,17074],{"class":1023}," brotli\n",[995,17076,17077],{"class":997,"line":713},[995,17078,17079],{"class":1001},"# Subset Inter regular + bold to the latin range, output woff2\n",[995,17081,17082,17084,17087],{"class":997,"line":730},[995,17083,16341],{"class":1007},[995,17085,17086],{"class":1023}," Inter-Regular.ttf",[995,17088,3002],{"class":1010},[995,17090,17091,17093,17096],{"class":997,"line":1544},[995,17092,16365],{"class":1010},[995,17094,17095],{"class":1023},"\"U+0000-00FF,U+2013-2014,U+2018-201A,U+201C-201E,U+2022,U+2026\"",[995,17097,3002],{"class":1010},[995,17099,17100,17102],{"class":997,"line":1550},[995,17101,16385],{"class":1010},[995,17103,17104],{"class":1010}," --output-file=inter-regular.woff2\n",[995,17106,17107,17109,17112],{"class":997,"line":1673},[995,17108,16341],{"class":1007},[995,17110,17111],{"class":1023}," Inter-Bold.ttf",[995,17113,3002],{"class":1010},[995,17115,17116,17118,17120],{"class":997,"line":1678},[995,17117,16365],{"class":1010},[995,17119,17095],{"class":1023},[995,17121,3002],{"class":1010},[995,17123,17124,17126],{"class":997,"line":1693},[995,17125,16385],{"class":1010},[995,17127,17128],{"class":1010}," --output-file=inter-bold.woff2\n",[14,17130,17131,17132,8912,17134,17137,17138,17141,17142,17145,17146,17148,17149,239],{},"Subsetting to the latin range took Inter Regular from a 48 KB full ",[253,17133,16079],{},[229,17135,17136],{},"17 KB",". Place the files where your generator fingerprints static assets (",[253,17139,17140],{},"public\u002Ffonts\u002F"," in Astro\u002FEleventy, ",[253,17143,17144],{},"static\u002Ffonts\u002F"," or an asset pipeline in Hugo) so they inherit the one-year ",[253,17147,11756],{}," cache from your ",[23,17150,17151],{"href":14803},"CDN caching rules",[34,17153,17155],{"id":17154},"step-2-declare-font-face-with-a-metric-matched-fallback","Step 2: Declare @font-face with a Metric-Matched Fallback",[14,17157,17158,17159,17161,17162,17164],{},"Define the real face with ",[253,17160,16740],{},", then define a fallback ",[253,17163,16092],{}," that wraps the local system font with overrides so its box matches Inter. This is the part that drives CLS toward zero:",[987,17166,17168],{"className":16083,"code":17167,"language":16085,"meta":712,"style":712},"@font-face {\n  font-family: 'Inter';\n  font-style: normal;\n  font-weight: 400;\n  font-display: swap;\n  src: url('\u002Ffonts\u002Finter-regular.woff2') format('woff2');\n}\n\n\u002F* Metric-matched fallback so the swap does not reflow *\u002F\n@font-face {\n  font-family: 'Inter Fallback';\n  src: local('Arial');\n  ascent-override: 90%;\n  descent-override: 22.5%;\n  line-gap-override: 0%;\n  size-adjust: 107%;\n}\n\n:root { --font-sans: 'Inter', 'Inter Fallback', system-ui, sans-serif; }\nbody { font-family: var(--font-sans); }\n",[253,17169,17170,17176,17186,17198,17208,17218,17241,17245,17249,17254,17260,17270,17284,17296,17309,17321,17333,17337,17341,17370],{"__ignoreMap":712},[995,17171,17172,17174],{"class":997,"line":998},[995,17173,16092],{"class":1614},[995,17175,8802],{"class":1618},[995,17177,17178,17180,17182,17184],{"class":997,"line":713},[995,17179,16099],{"class":1010},[995,17181,1925],{"class":1618},[995,17183,16104],{"class":1023},[995,17185,1628],{"class":1618},[995,17187,17188,17191,17193,17196],{"class":997,"line":730},[995,17189,17190],{"class":1010},"  font-style",[995,17192,1925],{"class":1618},[995,17194,17195],{"class":1010},"normal",[995,17197,1628],{"class":1618},[995,17199,17200,17202,17204,17206],{"class":997,"line":1544},[995,17201,16138],{"class":1010},[995,17203,1925],{"class":1618},[995,17205,101],{"class":1010},[995,17207,1628],{"class":1618},[995,17209,17210,17212,17214,17216],{"class":997,"line":1550},[995,17211,16156],{"class":1010},[995,17213,1925],{"class":1618},[995,17215,16161],{"class":1010},[995,17217,1628],{"class":1618},[995,17219,17220,17222,17224,17226,17228,17231,17233,17235,17237,17239],{"class":997,"line":1673},[995,17221,16111],{"class":1010},[995,17223,1925],{"class":1618},[995,17225,16116],{"class":1010},[995,17227,1799],{"class":1618},[995,17229,17230],{"class":1023},"'\u002Ffonts\u002Finter-regular.woff2'",[995,17232,1811],{"class":1618},[995,17234,16126],{"class":1010},[995,17236,1799],{"class":1618},[995,17238,16131],{"class":1023},[995,17240,5829],{"class":1618},[995,17242,17243],{"class":997,"line":1678},[995,17244,9008],{"class":1618},[995,17246,17247],{"class":997,"line":1693},[995,17248,1541],{"emptyLinePlaceholder":752},[995,17250,17251],{"class":997,"line":1705},[995,17252,17253],{"class":1001},"\u002F* Metric-matched fallback so the swap does not reflow *\u002F\n",[995,17255,17256,17258],{"class":997,"line":1711},[995,17257,16092],{"class":1614},[995,17259,8802],{"class":1618},[995,17261,17262,17264,17266,17268],{"class":997,"line":1717},[995,17263,16099],{"class":1010},[995,17265,1925],{"class":1618},[995,17267,16445],{"class":1023},[995,17269,1628],{"class":1618},[995,17271,17272,17274,17276,17278,17280,17282],{"class":997,"line":1726},[995,17273,16111],{"class":1010},[995,17275,1925],{"class":1618},[995,17277,16456],{"class":1010},[995,17279,1799],{"class":1618},[995,17281,16461],{"class":1023},[995,17283,5829],{"class":1618},[995,17285,17286,17288,17290,17292,17294],{"class":997,"line":1732},[995,17287,16483],{"class":1010},[995,17289,1925],{"class":1618},[995,17291,873],{"class":1010},[995,17293,16476],{"class":1614},[995,17295,1628],{"class":1618},[995,17297,17298,17300,17302,17305,17307],{"class":997,"line":2967},[995,17299,16496],{"class":1010},[995,17301,1925],{"class":1618},[995,17303,17304],{"class":1010},"22.5",[995,17306,16476],{"class":1614},[995,17308,1628],{"class":1618},[995,17310,17311,17313,17315,17317,17319],{"class":997,"line":2972},[995,17312,16510],{"class":1010},[995,17314,1925],{"class":1618},[995,17316,2515],{"class":1010},[995,17318,16476],{"class":1614},[995,17320,1628],{"class":1618},[995,17322,17323,17325,17327,17329,17331],{"class":997,"line":4147},[995,17324,16468],{"class":1010},[995,17326,1925],{"class":1618},[995,17328,16473],{"class":1010},[995,17330,16476],{"class":1614},[995,17332,1628],{"class":1618},[995,17334,17335],{"class":997,"line":4158},[995,17336,9008],{"class":1618},[995,17338,17339],{"class":997,"line":4168},[995,17340,1541],{"emptyLinePlaceholder":752},[995,17342,17343,17346,17348,17351,17353,17355,17357,17359,17361,17363,17365,17367],{"class":997,"line":4174},[995,17344,17345],{"class":1007},":root",[995,17347,10130],{"class":1618},[995,17349,17350],{"class":1784},"--font-sans",[995,17352,1925],{"class":1618},[995,17354,16104],{"class":1023},[995,17356,1850],{"class":1618},[995,17358,16445],{"class":1023},[995,17360,1850],{"class":1618},[995,17362,16549],{"class":1010},[995,17364,1850],{"class":1618},[995,17366,16554],{"class":1010},[995,17368,17369],{"class":1618},"; }\n",[995,17371,17373,17375,17377,17380,17382,17385,17387,17389],{"class":997,"line":17372},20,[995,17374,10255],{"class":1921},[995,17376,10130],{"class":1618},[995,17378,17379],{"class":1010},"font-family",[995,17381,1925],{"class":1618},[995,17383,17384],{"class":1010},"var",[995,17386,1799],{"class":1618},[995,17388,17350],{"class":1784},[995,17390,17391],{"class":1618},"); }\n",[14,17393,8896,17394,270,17396,17399,17400,2204,17403,17406],{},[253,17395,16699],{},[253,17397,17398],{},"*-override"," descriptors make Arial occupy almost exactly the same vertical and horizontal space as Inter, so when Inter swaps in, nothing moves. Generate the override values with a tool such as ",[253,17401,17402],{},"fontaine",[253,17404,17405],{},"capsize"," rather than guessing.",[34,17408,17410],{"id":17409},"step-3-preload-the-critical-weight","Step 3: Preload the Critical Weight",[14,17412,17413],{},"Preload only the file needed for above-the-fold text — usually the regular body weight — so it is fetched in parallel with the HTML, not after the CSS:",[987,17415,17417],{"className":16196,"code":17416,"language":16198,"meta":712,"style":712},"\u003Clink rel=\"preload\" href=\"\u002Ffonts\u002Finter-regular.woff2\" as=\"font\" type=\"font\u002Fwoff2\" crossorigin>\n",[253,17418,17419],{"__ignoreMap":712},[995,17420,17421,17423,17425,17427,17429,17431,17433,17435,17438,17440,17442,17444,17446,17448,17450,17452],{"class":997,"line":998},[995,17422,16205],{"class":1618},[995,17424,16208],{"class":1921},[995,17426,16211],{"class":1007},[995,17428,7317],{"class":1618},[995,17430,16216],{"class":1023},[995,17432,16219],{"class":1007},[995,17434,7317],{"class":1618},[995,17436,17437],{"class":1023},"\"\u002Ffonts\u002Finter-regular.woff2\"",[995,17439,16227],{"class":1007},[995,17441,7317],{"class":1618},[995,17443,16232],{"class":1023},[995,17445,16235],{"class":1007},[995,17447,7317],{"class":1618},[995,17449,16240],{"class":1023},[995,17451,16243],{"class":1007},[995,17453,16246],{"class":1618},[14,17455,17456,17457,17459],{},"Keep the ",[253,17458,16251],{}," attribute even for same-origin fonts; without it the preload is treated as a separate request and downloads twice. Do not preload every weight — the bold or italic faces can load lazily without affecting the first paint, and preloading them competes with the LCP image for bandwidth.",[34,17461,1166],{"id":1165},[14,17463,17464,17465,17467],{},"On a documentation home page, switching from the hosted ",[253,17466,16065],{}," stylesheet to this self-hosted, subset, metric-matched setup produced a clear CLS and connection win. CLS is the field-style value from a Lighthouse mobile run (median of 5), connections counted from the DevTools network panel:",[433,17469,17470,17487],{},[436,17471,17472],{},[439,17473,17474,17477,17479,17482,17485],{},[442,17475,17476],{},"Setup",[442,17478,16586],{},[442,17480,17481],{},"Third-party origins",[442,17483,17484],{},"Font bytes (regular)",[442,17486,10936],{},[457,17488,17489,17508,17522],{},[439,17490,17491,17497,17499,17502,17505],{},[462,17492,17493,17494,17496],{},"Hosted Google Fonts (",[253,17495,16161],{},", no fallback metrics)",[462,17498,186],{},[462,17500,17501],{},"2 (googleapis, gstatic)",[462,17503,17504],{},"48 KB",[462,17506,17507],{},"2.6s",[439,17509,17510,17513,17515,17517,17519],{},[462,17511,17512],{},"Self-hosted + preload, no metric fallback",[462,17514,11992],{},[462,17516,2515],{},[462,17518,17136],{},[462,17520,17521],{},"2.1s",[439,17523,17524,17529,17532,17534,17536],{},[462,17525,16608,17526,17528],{},[253,17527,16699],{}," fallback",[462,17530,17531],{},"0.01",[462,17533,2515],{},[462,17535,17136],{},[462,17537,17538],{},"2.0s",[14,17540,17541,17542,239],{},"Removing the two Google origins eliminated a DNS lookup and TLS handshake, which is most of the LCP gain. The metric-matched fallback is what takes CLS from 0.06 down to 0.01 — the swap no longer reflows the text. Pair this with image work so a stable text box is not undone by an unsized hero — see ",[23,17543,2190],{"href":2189},[34,17545,600],{"id":599},[39,17547,17548,17559,17575,17589,17599,17605],{},[42,17549,17550,17555,17556,239],{},[229,17551,9469,17552,17554],{},[253,17553,16251],{}," on the preload:"," the font downloads twice, wasting the preload entirely. Always include it for ",[253,17557,17558],{},"as=\"font\"",[42,17560,17561,692,17569,17571,17572,17574],{},[229,17562,17563,17566,17567,931],{},[253,17564,17565],{},"font-display: block"," instead of ",[253,17568,16161],{},[253,17570,16307],{}," hides text for up to 3 seconds (a flash of invisible text), hurting LCP. Use ",[253,17573,16161],{}," so fallback text shows immediately, and rely on the metric fallback to avoid the shift.",[42,17576,17577,17580,17581,17583,17584,738,17586,17588],{},[229,17578,17579],{},"Skipping the metric-matched fallback:"," plain ",[253,17582,16161],{}," still reflows when the real font arrives. The ",[253,17585,16699],{},[253,17587,17398],{}," fallback is the piece that actually kills CLS.",[42,17590,17591,17594,17595,17598],{},[229,17592,17593],{},"Over-subsetting:"," a strict latin subset can drop accented names, currency symbols, or smart quotes. Test pages with those characters; widen to ",[253,17596,17597],{},"latin-ext"," or specific ranges as needed.",[42,17600,17601,17604],{},[229,17602,17603],{},"Unhashed font paths:"," if the font URL is not fingerprinted, you cannot cache it for a year safely. Route fonts through your generator's hashed-asset pipeline.",[42,17606,17607,17609,17610,17612,17613,17615],{},[229,17608,637],{}," the change is the ",[253,17611,16092],{}," CSS, the preload tag, and the committed font files. Reverting is a ",[253,17614,15276],{}," plus redeploy; no third-party state to undo, since you removed the external dependency rather than added one.",[34,17617,642],{"id":641},[14,17619,17620,17621,17623,17624,17626],{},"Self-hosting Google Fonts removes two third-party origins and, combined with subsetting, a preload of the critical weight, ",[253,17622,16740],{},", and a ",[253,17625,16699],{}," metric-matched fallback, drives CLS from 0.14 to near zero while trimming font bytes and LCP. The font files become first-party hashed assets you cache for a year. Measure the CLS and LCP delta with Lighthouse on the specific page before and after, and confirm the network panel shows zero requests to Google origins.",[34,17628,651],{"id":650},[653,17630,17632],{"id":17631},"does-self-hosting-google-fonts-violate-the-license","Does self-hosting Google Fonts violate the license?",[14,17634,17635],{},"No. The fonts on Google Fonts are released under open licenses such as the SIL Open Font License, which explicitly permits redistribution and self-hosting. You can download the files and serve them from your own domain without attribution in the page, though you should keep the license file with the fonts.",[653,17637,17639],{"id":17638},"why-does-loading-fonts-from-googles-cdn-cause-layout-shift","Why does loading fonts from Google's CDN cause layout shift?",[14,17641,17642,17643,17645],{},"The request to ",[253,17644,16065],{}," is a separate origin that requires a DNS lookup, TLS handshake, and a CSS round trip before the font file is even requested. While that resolves, text renders in a fallback font with different metrics, then reflows when the web font arrives, which registers as cumulative layout shift.",[653,17647,17649],{"id":17648},"what-is-size-adjust-and-why-does-it-matter","What is size-adjust and why does it matter?",[14,17651,17652,17654,17655,17657,17658,270,17660,17662],{},[253,17653,16699],{}," is a descriptor on the ",[253,17656,16092],{}," fallback that scales the fallback font's glyphs so its line box closely matches the web font. Combined with ",[253,17659,16755],{},[253,17661,16758],{},", it makes the swap from fallback to web font nearly invisible, which drives the layout shift contribution toward zero.",[653,17664,17666],{"id":17665},"should-i-preload-every-font-file","Should I preload every font file?",[14,17668,17669],{},"No. Preload only the one or two files needed for above-the-fold text, typically the regular and bold weights of your body or heading face. Preloading every weight competes for bandwidth with the LCP image and can make things slower, not faster.",[653,17671,17673],{"id":17672},"will-subsetting-break-non-latin-characters","Will subsetting break non-Latin characters?",[14,17675,17676],{},"It can. A latin subset drops glyphs for other scripts and some punctuation or symbols. If your content includes accented names, currency symbols, or other scripts, subset to latin-ext or the specific unicode ranges you need, and test pages that use those characters.",[34,17678,684],{"id":683},[39,17680,17681,17688,17692,17697],{},[42,17682,17683,692,17685,17687],{},[229,17684,691],{},[23,17686,14061],{"href":14060}," — the full font-loading picture.",[42,17689,17690,16848],{},[23,17691,2190],{"href":2189},[42,17693,17694,17696],{},[23,17695,13849],{"href":14803}," — caching the self-hosted font files for a year.",[42,17698,17699,17701],{},[23,17700,5501],{"href":5500}," — where CLS and LCP fit overall.",[1346,17703,17704],{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}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 .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}",{"title":712,"searchDepth":713,"depth":713,"links":17706},[17707,17708,17709,17710,17711,17712,17713,17714,17715,17722],{"id":36,"depth":713,"text":37},{"id":16945,"depth":713,"text":16946},{"id":17042,"depth":713,"text":17043},{"id":17154,"depth":713,"text":17155},{"id":17409,"depth":713,"text":17410},{"id":1165,"depth":713,"text":1166},{"id":599,"depth":713,"text":600},{"id":641,"depth":713,"text":642},{"id":650,"depth":713,"text":651,"children":17716},[17717,17718,17719,17720,17721],{"id":17631,"depth":730,"text":17632},{"id":17638,"depth":730,"text":17639},{"id":17648,"depth":730,"text":17649},{"id":17665,"depth":730,"text":17666},{"id":17672,"depth":730,"text":17673},{"id":683,"depth":713,"text":684},[17724,17725,17726,17727],{"name":737,"item":738},{"name":5501,"item":5500},{"name":14061,"item":14060},{"name":16901,"item":16176},"Self-host Google Fonts to kill CLS and a third-party connection: download, subset, preload, font-display swap, and a size-adjust fallback with measured before\u002Fafter CLS.",[17730,17731,17733,17735,17736],{"q":17632,"a":17635},{"q":17639,"a":17732},"The request to fonts.googleapis.com is a separate origin that requires a DNS lookup, TLS handshake, and a CSS round trip before the font file is even requested. While that resolves, text renders in a fallback font with different metrics, then reflows when the web font arrives, which registers as cumulative layout shift.",{"q":17649,"a":17734},"size-adjust is a descriptor on the @font-face fallback that scales the fallback font's glyphs so its line box closely matches the web font. Combined with ascent-override and descent-override, it makes the swap from fallback to web font nearly invisible, which drives the layout shift contribution toward zero.",{"q":17666,"a":17669},{"q":17673,"a":17676},{},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Ffont-loading-strategies-for-static-sites\u002Fself-hosting-google-fonts-to-eliminate-layout-shift",{"title":16901,"description":17728},"performance-optimization-core-web-vitals-for-ssgs\u002Ffont-loading-strategies-for-static-sites\u002Fself-hosting-google-fonts-to-eliminate-layout-shift\u002Findex","49qrvIfT_ipffAT4AqcZxkG_kw84_26O61BnNDWQtZQ",{"id":17743,"title":17744,"body":17745,"breadcrumb":18349,"dateModified":743,"datePublished":743,"description":18355,"extension":745,"faq":18356,"meta":18362,"navigation":752,"path":18363,"seo":18364,"slug":17749,"stem":18365,"type":756,"__hash__":18366},"content\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fimage-optimization-pipelines-in-astro\u002Fbuilding-an-image-cdn-pipeline-for-static-sites\u002Findex.md","Building an Image CDN Pipeline for Static Sites",{"type":7,"value":17746,"toc":18332},[17747,17750,17759,17761,17774,17778,17788,17893,17897,17900,18040,18064,18068,18080,18104,18114,18118,18124,18192,18195,18197,18255,18257,18266,18268,18272,18275,18279,18282,18286,18289,18293,18296,18300,18303,18305,18329],[10,17748,17744],{"id":17749},"building-an-image-cdn-pipeline-for-static-sites",[14,17751,17752,17753,17755,17756,17758],{},"Build-time image processing is the right default — but it stops scaling when your image set is large, changes often, or comes from a CMS you do not control at build time. At that point an image CDN, which transforms images on demand at the edge from a URL, becomes the better tool. This recipe covers when to offload transforms, how URL-based transforms and edge caching work, and what they cost. It extends ",[23,17754,2190],{"href":2189}," — where the default is build-time transforms — within the broader ",[23,17757,5501],{"href":5500}," effort.",[34,17760,37],{"id":36},[39,17762,17763,17766,17769],{},[42,17764,17765],{},"A static site that currently has many images or pulls images from a CMS, object store, or user uploads.",[42,17767,17768],{},"An account on one image CDN: Cloudflare Images, imgix, or Netlify Image CDN are the common choices for static sites.",[42,17770,17771,17773],{},[253,17772,14076],{}," to inspect cache-status headers and a browser\u002FWebPageTest profile to measure delivered bytes and LCP.",[34,17775,17777],{"id":17776},"build-time-vs-cdn-the-decision","Build-Time vs CDN: The Decision",[14,17779,17780,17781,17783,17784,17787],{},"Build-time transforms (covered in the parent guide via ",[253,17782,2117],{}," and Sharp) run once during the build, ship deterministic files, and add zero runtime dependency. Their cost is ",[229,17785,17786],{},"build duration"," and the fact that every image must exist at build time. An image CDN moves the transform to request time: you reference a source image through a transform URL, the edge generates and caches the variant on first request, and serves it from cache thereafter. The trade is a runtime dependency and per-image cost in exchange for builds that no longer process images and the ability to transform images that did not exist at build time.",[55,17789,17790,17890],{},[58,17791,66,17795,66,17798,66,17801,66,17883],{"viewBox":11210,"role":61,"ariaLabelledBy":17792,"xmlns":65},[17793,17794],"imgcdn-arch-title","imgcdn-arch-desc",[68,17796,17797],{"id":17793},"Build-time versus image-CDN transform architecture",[72,17799,17800],{"id":17794},"The build-time path processes a source image with Sharp during the build into the deploy output; the CDN path leaves the source untouched and transforms it on demand at the edge from a URL, caching the result.",[95,17802,78,17803,78,17806,78,17809,78,17811,78,17816,78,17818,78,17821,78,17824,78,17826,78,17829,78,17832,78,17834,78,17836,78,17838,78,17843,78,17847,78,17849,78,17853,78,17856,78,17871,78,17874,78,17877,78,17880,66],{"style":97},[99,17804,17805],{"x":167,"y":102,"fill":103,"style":104},"Where the transform happens",[99,17807,17808],{"x":5393,"y":828,"fill":114,"style":2597},"Build-time",[107,17810],{"x":1431,"y":3559,"width":159,"height":5380,"rx":3579,"fill":162,"opacity":877,"stroke":164,"style":116},[99,17812,17815],{"x":17813,"y":17814,"fill":103,"style":126},"175","86","source.jpg",[107,17817],{"x":820,"y":3559,"width":1431,"height":5380,"rx":3579,"fill":114,"opacity":186,"stroke":114,"style":116},[99,17819,17820],{"x":6144,"y":1430,"fill":114,"style":126},"Sharp at build",[99,17822,17823],{"x":6144,"y":833,"fill":93,"style":4658},"once",[107,17825],{"x":863,"y":3559,"width":1431,"height":5380,"rx":3579,"fill":185,"opacity":886,"stroke":187,"style":116},[99,17827,17828],{"x":906,"y":17814,"fill":103,"style":126},"in deploy output",[99,17830,17831],{"x":5393,"y":142,"fill":824,"style":2597},"Image CDN",[107,17833],{"x":1431,"y":160,"width":159,"height":5380,"rx":3579,"fill":162,"opacity":877,"stroke":164,"style":116},[99,17835,17815],{"x":17813,"y":6160,"fill":103,"style":126},[107,17837],{"x":820,"y":160,"width":2563,"height":5380,"rx":3579,"fill":824,"opacity":186,"stroke":824,"style":116},[99,17839,17842],{"x":17840,"y":17841,"fill":824,"style":126},"345","202","edge transform",[99,17844,17846],{"x":17840,"y":17845,"fill":93,"style":4658},"218","on first request",[107,17848],{"x":863,"y":160,"width":2563,"height":5380,"rx":3579,"fill":185,"opacity":886,"stroke":187,"style":116},[99,17850,17852],{"x":17851,"y":17841,"fill":103,"style":126},"515","edge cache HIT",[99,17854,17855],{"x":17851,"y":17845,"fill":93,"style":4658},"thereafter",[95,17857,88,17858,88,17862,88,17865,88,17868,78],{"stroke":93,"fill":205,"style":116},[90,17859],{"d":17860,"style":17861},"M230 82 L278 82","marker-end:url(#imgcdn-arrow)",[90,17863],{"d":17864,"style":17861},"M400 82 L448 82",[90,17866],{"d":17867,"style":17861},"M230 204 L278 204",[90,17869],{"d":17870,"style":17861},"M410 204 L448 204",[99,17872,17873],{"x":12784,"y":17814,"fill":93,"style":11285},"slow build,",[99,17875,17876],{"x":12784,"y":3485,"fill":93,"style":11285},"zero runtime",[99,17878,17879],{"x":12784,"y":5402,"fill":93,"style":11285},"fast build,",[99,17881,17882],{"x":12784,"y":111,"fill":93,"style":11285},"per-image cost",[76,17884,78,17885,66],{},[80,17886,88,17888,78],{"id":17887,"viewBox":83,"refX":84,"refY":85,"markerWidth":86,"markerHeight":86,"orient":87},"imgcdn-arrow",[90,17889],{"d":92,"fill":93},[218,17891,17892],{},"Build-time processing runs Sharp once into the deploy output; the image CDN leaves the source alone and transforms on demand at the edge, caching the result for later requests.",[34,17894,17896],{"id":17895},"url-based-transforms","URL-Based Transforms",[14,17898,17899],{},"The defining feature of an image CDN is that the transform is encoded in the URL — resize, format, and quality are query parameters or path segments. The exact syntax differs by provider:",[987,17901,17903],{"className":16196,"code":17902,"language":16198,"meta":712,"style":712},"\u003C!-- imgix: query params -->\n\u003Cimg src=\"https:\u002F\u002Fyoursite.imgix.net\u002Fhero.jpg?w=1200&auto=format,compress&q=75\"\n     width=\"1200\" height=\"675\" alt=\"Dashboard overview\">\n\n\u003C!-- Cloudflare Images: transform path -->\n\u003Cimg src=\"https:\u002F\u002Fyoursite.com\u002Fcdn-cgi\u002Fimage\u002Fwidth=1200,format=auto,quality=75\u002Fhero.jpg\"\n     width=\"1200\" height=\"675\" alt=\"Dashboard overview\">\n\n\u003C!-- Netlify Image CDN: \u002F.netlify\u002Fimages endpoint -->\n\u003Cimg src=\"\u002F.netlify\u002Fimages?url=\u002Fhero.jpg&w=1200&fm=avif&q=75\"\n     width=\"1200\" height=\"675\" alt=\"Dashboard overview\">\n",[253,17904,17905,17910,17924,17952,17956,17961,17974,17996,18000,18005,18018],{"__ignoreMap":712},[995,17906,17907],{"class":997,"line":998},[995,17908,17909],{"class":1001},"\u003C!-- imgix: query params -->\n",[995,17911,17912,17914,17916,17919,17921],{"class":997,"line":713},[995,17913,16205],{"class":1618},[995,17915,61],{"class":1921},[995,17917,17918],{"class":1007}," src",[995,17920,7317],{"class":1618},[995,17922,17923],{"class":1023},"\"https:\u002F\u002Fyoursite.imgix.net\u002Fhero.jpg?w=1200&auto=format,compress&q=75\"\n",[995,17925,17926,17929,17931,17934,17937,17939,17942,17945,17947,17950],{"class":997,"line":730},[995,17927,17928],{"class":1007},"     width",[995,17930,7317],{"class":1618},[995,17932,17933],{"class":1023},"\"1200\"",[995,17935,17936],{"class":1007}," height",[995,17938,7317],{"class":1618},[995,17940,17941],{"class":1023},"\"675\"",[995,17943,17944],{"class":1007}," alt",[995,17946,7317],{"class":1618},[995,17948,17949],{"class":1023},"\"Dashboard overview\"",[995,17951,16246],{"class":1618},[995,17953,17954],{"class":997,"line":1544},[995,17955,1541],{"emptyLinePlaceholder":752},[995,17957,17958],{"class":997,"line":1550},[995,17959,17960],{"class":1001},"\u003C!-- Cloudflare Images: transform path -->\n",[995,17962,17963,17965,17967,17969,17971],{"class":997,"line":1673},[995,17964,16205],{"class":1618},[995,17966,61],{"class":1921},[995,17968,17918],{"class":1007},[995,17970,7317],{"class":1618},[995,17972,17973],{"class":1023},"\"https:\u002F\u002Fyoursite.com\u002Fcdn-cgi\u002Fimage\u002Fwidth=1200,format=auto,quality=75\u002Fhero.jpg\"\n",[995,17975,17976,17978,17980,17982,17984,17986,17988,17990,17992,17994],{"class":997,"line":1678},[995,17977,17928],{"class":1007},[995,17979,7317],{"class":1618},[995,17981,17933],{"class":1023},[995,17983,17936],{"class":1007},[995,17985,7317],{"class":1618},[995,17987,17941],{"class":1023},[995,17989,17944],{"class":1007},[995,17991,7317],{"class":1618},[995,17993,17949],{"class":1023},[995,17995,16246],{"class":1618},[995,17997,17998],{"class":997,"line":1693},[995,17999,1541],{"emptyLinePlaceholder":752},[995,18001,18002],{"class":997,"line":1705},[995,18003,18004],{"class":1001},"\u003C!-- Netlify Image CDN: \u002F.netlify\u002Fimages endpoint -->\n",[995,18006,18007,18009,18011,18013,18015],{"class":997,"line":1711},[995,18008,16205],{"class":1618},[995,18010,61],{"class":1921},[995,18012,17918],{"class":1007},[995,18014,7317],{"class":1618},[995,18016,18017],{"class":1023},"\"\u002F.netlify\u002Fimages?url=\u002Fhero.jpg&w=1200&fm=avif&q=75\"\n",[995,18019,18020,18022,18024,18026,18028,18030,18032,18034,18036,18038],{"class":997,"line":1717},[995,18021,17928],{"class":1007},[995,18023,7317],{"class":1618},[995,18025,17933],{"class":1023},[995,18027,17936],{"class":1007},[995,18029,7317],{"class":1618},[995,18031,17941],{"class":1023},[995,18033,17944],{"class":1007},[995,18035,7317],{"class":1618},[995,18037,17949],{"class":1023},[995,18039,16246],{"class":1618},[14,18041,18042,256,18045,18048,18049,18051,18052,18054,18055,738,18057,18060,18061,18063],{},[253,18043,18044],{},"format=auto",[253,18046,18047],{},"auto=format",") negotiates AVIF\u002FWebP from the request ",[253,18050,14716],{}," header, so a single URL serves the smallest format each browser can decode. Generate a responsive ",[253,18053,3720],{}," by emitting the same URL at several widths. Note that the ",[253,18056,9286],{},[253,18058,18059],{},"height"," attributes on the ",[253,18062,3847],{}," are still required to reserve layout space and avoid CLS — the CDN controls bytes, not the rendered box.",[34,18065,18067],{"id":18066},"edge-caching-behavior","Edge Caching Behavior",[14,18069,18070,18071,18073,18074,18076,18077,18079],{},"Each distinct transform URL is cached at the edge. The first request is a ",[253,18072,14685],{}," that pays the transform latency; every subsequent request for that exact URL is a ",[253,18075,14682],{},". Verify with ",[253,18078,14623],{}," and read the provider's cache-status header:",[987,18081,18083],{"className":989,"code":18082,"language":991,"meta":712,"style":712},"curl -I \"https:\u002F\u002Fyoursite.com\u002Fcdn-cgi\u002Fimage\u002Fwidth=1200,format=auto\u002Fhero.jpg\"\n# first:  cf-cache-status: MISS  (edge transformed and stored)\n# second: cf-cache-status: HIT   (served from edge cache)\n",[253,18084,18085,18094,18099],{"__ignoreMap":712},[995,18086,18087,18089,18091],{"class":997,"line":998},[995,18088,14076],{"class":1007},[995,18090,15089],{"class":1010},[995,18092,18093],{"class":1023}," \"https:\u002F\u002Fyoursite.com\u002Fcdn-cgi\u002Fimage\u002Fwidth=1200,format=auto\u002Fhero.jpg\"\n",[995,18095,18096],{"class":997,"line":713},[995,18097,18098],{"class":1001},"# first:  cf-cache-status: MISS  (edge transformed and stored)\n",[995,18100,18101],{"class":997,"line":730},[995,18102,18103],{"class":1001},"# second: cf-cache-status: HIT   (served from edge cache)\n",[14,18105,18106,18107,18110,18111,18113],{},"Because the cost driver is the number of ",[229,18108,18109],{},"distinct"," source images and transforms — not total requests — a high-traffic page with a fixed image set is cheap: nearly every request is a HIT. Warm the critical above-the-fold transforms (a scripted ",[253,18112,14076],{}," over your LCP image URLs after deploy) so the first real visitor does not eat the MISS latency.",[34,18115,18117],{"id":18116},"measured-impact-and-cost","Measured Impact and Cost",[14,18119,18120,18121,18123],{},"On a marketing site with about 220 content images sourced from a headless CMS, moving from build-time processing to a CDN removed the image step from the build entirely and kept delivery fast. Build time from ",[253,18122,595],{},", delivery from a WebPageTest mobile profile (median of 5), warmed edge cache:",[433,18125,18126,18143],{},[436,18127,18128],{},[439,18129,18130,18132,18135,18138,18140],{},[442,18131,2135],{},[442,18133,18134],{},"Build duration",[442,18136,18137],{},"Delivered hero",[442,18139,10936],{},[442,18141,18142],{},"Monthly cost (≈220 sources)",[457,18144,18145,18161,18178],{},[439,18146,18147,18150,18153,18156,18158],{},[462,18148,18149],{},"Build-time Sharp (all 220)",[462,18151,18152],{},"142s",[462,18154,18155],{},"180 KB",[462,18157,2169],{},[462,18159,18160],{},"$0 (compute in CI)",[439,18162,18163,18166,18169,18172,18175],{},[462,18164,18165],{},"Image CDN, cold edge",[462,18167,18168],{},"18s",[462,18170,18171],{},"184 KB",[462,18173,18174],{},"2.0s (MISS on hero)",[462,18176,18177],{},"~$5–9",[439,18179,18180,18183,18185,18187,18190],{},[462,18181,18182],{},"Image CDN, warmed edge",[462,18184,18168],{},[462,18186,18171],{},[462,18188,18189],{},"1.7s (HIT on hero)",[462,18191,18177],{},[14,18193,18194],{},"The build dropped from 142s to 18s because images are no longer processed in CI. Delivered bytes and warmed LCP are essentially the same as build-time — the edge transform produces equivalent AVIF. The cost is a few dollars a month driven by the ~220 distinct sources, plus the discipline of warming critical transforms so the hero is a HIT for the first visitor. For a small, stable image set, build-time still wins on simplicity and zero runtime dependency; the CDN earns its place when the image set is large or CMS-driven.",[34,18196,600],{"id":599},[39,18198,18199,18208,18217,18234,18240],{},[42,18200,18201,18204,18205,18207],{},[229,18202,18203],{},"Unbounded transform variations:"," if widths or quality come from arbitrary user input, you can generate thousands of distinct URLs that never reach a HIT and inflate cost. Constrain ",[253,18206,3720],{}," to a fixed set of widths (e.g. 400\u002F800\u002F1200).",[42,18209,18210,18213,18214,18216],{},[229,18211,18212],{},"Cold MISS on the LCP image:"," the first visitor after a deploy pays the transform latency on the hero. Warm above-the-fold transforms with a post-deploy ",[253,18215,14076],{}," script.",[42,18218,18219,18222,18223,738,18225,256,18227,18230,18231,18233],{},[229,18220,18221],{},"Missing dimensions:"," the CDN shrinks bytes but the browser still needs ",[253,18224,9286],{},[253,18226,18059],{},[253,18228,18229],{},"aspect-ratio",") to reserve space, or CLS spikes — same rule as build-time. See ",[23,18232,16177],{"href":16176}," for the parallel CLS discipline on fonts.",[42,18235,18236,18239],{},[229,18237,18238],{},"Runtime dependency:"," if the CDN is down, images break at request time, whereas build-time files are just static assets. Weigh this for critical pages.",[42,18241,18242,18244,18245,692,18247,18249,18250,18252,18253,239],{},[229,18243,637],{}," because the transform lives in the image URL, reverting to build-time means swapping the ",[253,18246,3847],{},[253,18248,1579],{}," back to ",[253,18251,2117],{}," output (or your generator's pipeline) and redeploying. Keep the source images in the repo or object store so the build path stays available — see the parent ",[23,18254,2190],{"href":2189},[34,18256,642],{"id":641},[14,18258,18259,18260,18262,18263,18265],{},"An image CDN is not a replacement for build-time transforms — it is the right tool when the image set is large, volatile, or CMS-driven, and when build duration from image processing has become a problem. URL-based transforms with ",[253,18261,18044],{}," and a fixed-width ",[253,18264,3720],{},", served from a warmed edge cache, deliver bytes and LCP equivalent to build-time while cutting the build to seconds. The common best policy is a hybrid: build-time for the few critical above-the-fold images, CDN for the large remainder of body images. Whichever you choose, always render explicit dimensions and measure the LCP delta on the page you changed.",[34,18267,651],{"id":650},[653,18269,18271],{"id":18270},"when-should-i-use-an-image-cdn-instead-of-build-time-transforms","When should I use an image CDN instead of build-time transforms?",[14,18273,18274],{},"Use an image CDN when your image set is large or changes often, when images come from a CMS or user uploads you do not control at build time, or when build duration from image processing is unacceptable. Stick with build-time transforms for small, stable image sets where you want zero runtime dependency and predictable cost.",[653,18276,18278],{"id":18277},"does-an-image-cdn-slow-down-the-first-request","Does an image CDN slow down the first request?",[14,18280,18281],{},"The first request for a given transform is a cache MISS and pays the transform latency at the edge, typically a few hundred milliseconds. Every subsequent request for that exact URL is a cache HIT served in tens of milliseconds. Warm critical above-the-fold transforms so the first real visitor does not pay the miss.",[653,18283,18285],{"id":18284},"how-do-image-cdns-charge","How do image CDNs charge?",[14,18287,18288],{},"Most charge on some combination of unique transformations or stored originals plus delivery bandwidth. Cloudflare Images bills on images stored and images delivered, imgix on origin images and bandwidth, and Netlify Image CDN on transformed source images per month. The cost driver is the number of distinct source images, not total requests, because repeated requests for the same URL are cache hits.",[653,18290,18292],{"id":18291},"can-i-combine-build-time-and-cdn-transforms","Can I combine build-time and CDN transforms?",[14,18294,18295],{},"Yes, and it is often the best policy. Process the few critical above-the-fold images at build time so they ship in the HTML with no runtime dependency, and route the large remainder of content images through the CDN. This keeps the LCP image fast and deterministic while avoiding a slow build for hundreds of body images.",[653,18297,18299],{"id":18298},"how-do-i-avoid-layout-shift-with-cdn-images","How do I avoid layout shift with CDN images?",[14,18301,18302],{},"Always render explicit width and height or an aspect-ratio on the img element regardless of where the bytes come from. The CDN controls the byte size, not the rendered box, so without reserved dimensions you still get cumulative layout shift.",[34,18304,684],{"id":683},[39,18306,18307,18314,18319,18324],{},[42,18308,18309,692,18311,18313],{},[229,18310,691],{},[23,18312,2190],{"href":2189}," — the build-time default this guide extends.",[42,18315,18316,18318],{},[23,18317,3852],{"href":3851}," — the framework-native build-time approach.",[42,18320,18321,18323],{},[23,18322,13849],{"href":14803}," — how edge caching of transformed URLs behaves.",[42,18325,18326,18328],{},[23,18327,5501],{"href":5500}," — where image delivery fits the LCP picture.",[1346,18330,18331],{},"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 .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}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 .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}",{"title":712,"searchDepth":713,"depth":713,"links":18333},[18334,18335,18336,18337,18338,18339,18340,18341,18348],{"id":36,"depth":713,"text":37},{"id":17776,"depth":713,"text":17777},{"id":17895,"depth":713,"text":17896},{"id":18066,"depth":713,"text":18067},{"id":18116,"depth":713,"text":18117},{"id":599,"depth":713,"text":600},{"id":641,"depth":713,"text":642},{"id":650,"depth":713,"text":651,"children":18342},[18343,18344,18345,18346,18347],{"id":18270,"depth":730,"text":18271},{"id":18277,"depth":730,"text":18278},{"id":18284,"depth":730,"text":18285},{"id":18291,"depth":730,"text":18292},{"id":18298,"depth":730,"text":18299},{"id":683,"depth":713,"text":684},[18350,18351,18352,18353],{"name":737,"item":738},{"name":5501,"item":5500},{"name":2190,"item":2189},{"name":17744,"item":18354},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fimage-optimization-pipelines-in-astro\u002Fbuilding-an-image-cdn-pipeline-for-static-sites\u002F","When to offload image transforms to an image CDN versus build time: URL-based transforms, edge caching, and cost, compared across Cloudflare Images, imgix, and Netlify.",[18357,18358,18359,18360,18361],{"q":18271,"a":18274},{"q":18278,"a":18281},{"q":18285,"a":18288},{"q":18292,"a":18295},{"q":18299,"a":18302},{},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fimage-optimization-pipelines-in-astro\u002Fbuilding-an-image-cdn-pipeline-for-static-sites",{"title":17744,"description":18355},"performance-optimization-core-web-vitals-for-ssgs\u002Fimage-optimization-pipelines-in-astro\u002Fbuilding-an-image-cdn-pipeline-for-static-sites\u002Findex","qIQdCFsL2PrSfNyK16u9GyM7n8-qHH7b7FGDuBYhXK4",{"id":18368,"title":2190,"body":18369,"breadcrumb":19263,"dateModified":743,"datePublished":2446,"description":19267,"extension":745,"faq":19268,"meta":19278,"navigation":752,"path":19279,"seo":19280,"slug":18373,"stem":19281,"type":2460,"__hash__":19282},"content\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fimage-optimization-pipelines-in-astro\u002Findex.md",{"type":7,"value":18370,"toc":19246},[18371,18374,18383,18478,18482,18487,18505,18530,18593,18613,18617,18628,18688,18694,18698,18707,18714,18759,18763,18766,18971,18974,19013,19029,19033,19036,19064,19073,19075,19124,19126,19152,19154,19158,19164,19168,19171,19175,19187,19191,19197,19201,19219,19221,19244],[10,18372,2190],{"id":18373},"image-optimization-pipelines-in-astro",[14,18375,18376,18377,18379,18380,18382],{},"Images are usually the largest thing on a page, so optimizing them at build time is the highest-leverage performance work you can do. Astro handles this natively through ",[253,18378,2117],{}," — it generates resized, modern-format images during the build with no runtime cost. This guide covers the component setup, the Sharp service, the CI checks that keep it honest, and the measurement to prove it worked. It fits the broader ",[23,18381,5501],{"href":5500}," effort, where images are typically the Largest Contentful Paint (LCP) element.",[55,18384,18385,18475],{},[58,18386,66,18390,66,18393,66,18396,66,18468],{"viewBox":16961,"role":61,"ariaLabelledBy":18387,"xmlns":65},[18388,18389],"img-pipe-title","img-pipe-desc",[68,18391,18392],{"id":18388},"Build-time image pipeline in Astro",[72,18394,18395],{"id":18389},"A source JPEG flows through the Sharp service, which emits AVIF and WebP variants at three widths, producing a responsive srcset served to the browser.",[95,18397,78,18398,78,18400,78,18404,78,18408,78,18410,78,18415,78,18418,78,18422,78,18436,78,18438,78,18440,78,18443,78,18447,78,18465,66],{"style":813},[107,18399],{"x":5393,"y":159,"width":2563,"height":849,"rx":823,"fill":162,"opacity":877,"stroke":164,"style":116},[99,18401,18403],{"x":18402,"y":119,"fill":103,"style":121},"85","hero.jpg",[99,18405,18407],{"x":18402,"y":18406,"fill":93,"style":126},"162","1.4 MB source",[107,18409],{"x":142,"y":4682,"width":161,"height":873,"rx":823,"fill":114,"opacity":186,"stroke":114,"style":116},[99,18411,18414],{"x":18412,"y":18413,"fill":114,"style":121},"275","135","Sharp service",[99,18416,18417],{"x":18412,"y":13940,"fill":93,"style":126},"resize + encode",[99,18419,18421],{"x":18412,"y":18420,"fill":93,"style":126},"176","at build time",[95,18423,88,18424,88,18426,88,18431,88,18433,78],{},[107,18425],{"x":1415,"y":110,"width":161,"height":5380,"rx":3579,"fill":185,"opacity":886,"stroke":187,"style":116},[99,18427,18430],{"x":18428,"y":18429,"fill":103,"style":829},"485","89","AVIF · 400\u002F800\u002F1200",[107,18432],{"x":1415,"y":15536,"width":161,"height":5380,"rx":3579,"fill":824,"opacity":850,"stroke":824,"style":116},[99,18434,18435],{"x":18428,"y":14899,"fill":103,"style":829},"WebP · 400\u002F800\u002F1200",[107,18437],{"x":6175,"y":4682,"width":2563,"height":873,"rx":823,"fill":2564,"opacity":825,"stroke":2565,"style":116},[99,18439,3720],{"x":11255,"y":2535,"fill":2565,"style":121},[99,18441,18442],{"x":11255,"y":14899,"fill":93,"style":126},"browser picks",[99,18444,18446],{"x":11255,"y":18445,"fill":93,"style":126},"173","~120-280 KB",[95,18448,88,18449,88,18453,88,18456,88,18459,88,18462,78],{"stroke":93,"fill":205,"style":116},[90,18450],{"d":18451,"style":18452},"M150 145 L198 145","marker-end:url(#img-arrow)",[90,18454],{"d":18455,"style":18452},"M350 135 L408 84",[90,18457],{"d":18458,"style":18452},"M350 155 L408 150",[90,18460],{"d":18461,"style":18452},"M560 84 L608 130",[90,18463],{"d":18464,"style":18452},"M560 150 L608 150",[99,18466,18467],{"x":816,"y":109,"fill":103,"style":104},"One source in, responsive modern formats out",[76,18469,78,18470,66],{},[80,18471,88,18473,78],{"id":18472,"viewBox":83,"refX":84,"refY":85,"markerWidth":86,"markerHeight":86,"orient":87},"img-arrow",[90,18474],{"d":92,"fill":93},[218,18476,18477],{},"A single 1.4 MB source becomes AVIF and WebP variants at three widths; the browser downloads the smallest that fits — often under 280 KB.",[34,18479,18481],{"id":18480},"native-setup","Native Setup",[14,18483,18484,18486],{},[253,18485,2117],{}," ships with Astro; you only need Sharp installed for the transforms:",[987,18488,18490],{"className":989,"code":18489,"language":991,"meta":712,"style":712},"npm i -D sharp\n",[253,18491,18492],{"__ignoreMap":712},[995,18493,18494,18496,18499,18502],{"class":997,"line":998},[995,18495,1527],{"class":1007},[995,18497,18498],{"class":1023}," i",[995,18500,18501],{"class":1010}," -D",[995,18503,18504],{"class":1023}," sharp\n",[14,18506,18507,18508,18511,18512,3946,18515,18517,18518,18521,18522,18524,18525,18527,18528,931],{},"For a single optimized image with explicit dimensions (which prevents layout shift), use ",[253,18509,18510],{},"\u003CImage>",". To emit ",[229,18513,18514],{},"multiple formats",[253,18516,2162],{}," — note that ",[253,18519,18520],{},"formats"," (plural) is a ",[253,18523,2162],{}," prop, while ",[253,18526,18510],{}," takes a single ",[253,18529,16126],{},[987,18531,18533],{"className":10854,"code":18532,"language":10856,"meta":712,"style":712},"---\nimport { Picture } from 'astro:assets';\nimport hero from '..\u002Fassets\u002Fhero.jpg';\n---\n\u003CPicture\n  src={hero}\n  alt=\"Dashboard analytics overview\"\n  widths={[400, 800, 1200]}\n  sizes=\"(max-width: 800px) 100vw, 1200px\"\n  formats={['avif', 'webp']}\n  quality={80}\n\u002F>\n",[253,18534,18535,18539,18544,18549,18553,18558,18563,18568,18573,18578,18583,18588],{"__ignoreMap":712},[995,18536,18537],{"class":997,"line":998},[995,18538,8106],{},[995,18540,18541],{"class":997,"line":713},[995,18542,18543],{},"import { Picture } from 'astro:assets';\n",[995,18545,18546],{"class":997,"line":730},[995,18547,18548],{},"import hero from '..\u002Fassets\u002Fhero.jpg';\n",[995,18550,18551],{"class":997,"line":1544},[995,18552,8106],{},[995,18554,18555],{"class":997,"line":1550},[995,18556,18557],{},"\u003CPicture\n",[995,18559,18560],{"class":997,"line":1673},[995,18561,18562],{},"  src={hero}\n",[995,18564,18565],{"class":997,"line":1678},[995,18566,18567],{},"  alt=\"Dashboard analytics overview\"\n",[995,18569,18570],{"class":997,"line":1693},[995,18571,18572],{},"  widths={[400, 800, 1200]}\n",[995,18574,18575],{"class":997,"line":1705},[995,18576,18577],{},"  sizes=\"(max-width: 800px) 100vw, 1200px\"\n",[995,18579,18580],{"class":997,"line":1711},[995,18581,18582],{},"  formats={['avif', 'webp']}\n",[995,18584,18585],{"class":997,"line":1717},[995,18586,18587],{},"  quality={80}\n",[995,18589,18590],{"class":997,"line":1726},[995,18591,18592],{},"\u002F>\n",[14,18594,18595,18596,18598,18599,18601,18602,18605,18606,18609,18610,18612],{},"This generates a responsive ",[253,18597,3720],{}," across the widths and serves AVIF\u002FWebP with a fallback, all at build time. In our measurement, swapping a single 1.4 MB hero JPEG for this ",[253,18600,2162],{}," setup cut the delivered hero from ",[229,18603,18604],{},"1.4 MB to 180 KB"," and moved LCP from ",[229,18607,18608],{},"3.1s to 1.7s"," on a throttled mid-tier mobile profile. Align image budgets with ",[23,18611,14061],{"href":14060}," for unified asset tracking.",[34,18614,18616],{"id":18615},"configuring-the-sharp-service","Configuring the Sharp Service",[14,18618,18619,18620,18622,18623,270,18625,18627],{},"The default Sharp service is fine for most sites; you only configure it when you need to raise limits or swap services. Set it in ",[253,18621,12966],{}," (per-image options like ",[253,18624,9291],{},[253,18626,16126],{}," live on the component, not in global config):",[987,18629,18631],{"className":1600,"code":18630,"language":1602,"meta":712,"style":712},"\u002F\u002F astro.config.mjs\nimport { defineConfig, sharpImageService } from 'astro\u002Fconfig';\n\nexport default defineConfig({\n  image: {\n    service: sharpImageService(),\n  },\n});\n",[253,18632,18633,18637,18650,18654,18664,18669,18680,18684],{"__ignoreMap":712},[995,18634,18635],{"class":997,"line":998},[995,18636,1609],{"class":1001},[995,18638,18639,18641,18644,18646,18648],{"class":997,"line":713},[995,18640,1615],{"class":1614},[995,18642,18643],{"class":1618}," { defineConfig, sharpImageService } ",[995,18645,1622],{"class":1614},[995,18647,1625],{"class":1023},[995,18649,1628],{"class":1618},[995,18651,18652],{"class":997,"line":730},[995,18653,1541],{"emptyLinePlaceholder":752},[995,18655,18656,18658,18660,18662],{"class":997,"line":1544},[995,18657,1681],{"class":1614},[995,18659,1684],{"class":1614},[995,18661,1687],{"class":1007},[995,18663,1690],{"class":1618},[995,18665,18666],{"class":997,"line":1550},[995,18667,18668],{"class":1618},"  image: {\n",[995,18670,18671,18674,18677],{"class":997,"line":1673},[995,18672,18673],{"class":1618},"    service: ",[995,18675,18676],{"class":1007},"sharpImageService",[995,18678,18679],{"class":1618},"(),\n",[995,18681,18682],{"class":997,"line":1678},[995,18683,1729],{"class":1618},[995,18685,18686],{"class":997,"line":1693},[995,18687,1735],{"class":1618},[14,18689,18690,18691,18693],{},"For remote images, fetch and cache them locally before the build to avoid re-downloading, or configure an allowed-domains list for Astro's remote image handling. The same build-time compression idea applies framework-agnostically — see ",[23,18692,3852],{"href":3851}," for the Hugo equivalent using its native image methods.",[34,18695,18697],{"id":18696},"choosing-formats-and-quality","Choosing Formats and Quality",[14,18699,18700,18701,18704,18705,239],{},"AVIF is the most efficient widely supported format — typically 20-30% smaller than WebP at matched quality — but it encodes more slowly at build time. The pragmatic policy is to emit both: list ",[253,18702,18703],{},"['avif', 'webp']"," so AVIF-capable browsers get the smallest file and everyone else falls back to WebP, with the original format as the final fallback inside ",[253,18706,2162],{},[14,18708,18709,18710,18713],{},"For quality, ",[253,18711,18712],{},"quality={80}"," is the sweet spot for photographic content; below 60 you start to see visible artifacting on gradients. For flat illustrations or screenshots with text, prefer lossless WebP or keep them as optimized PNG\u002FSVG, since lossy compression smears thin edges.",[433,18715,18716,18728],{},[436,18717,18718],{},[439,18719,18720,18723,18726],{},[442,18721,18722],{},"Source (1200px hero)",[442,18724,18725],{},"Bytes",[442,18727,10936],{},[457,18729,18730,18739,18750],{},[439,18731,18732,18735,18737],{},[462,18733,18734],{},"Original JPEG q90",[462,18736,11036],{},[462,18738,9416],{},[439,18740,18741,18744,18747],{},[462,18742,18743],{},"WebP q80",[462,18745,18746],{},"240 KB",[462,18748,18749],{},"1.9s",[439,18751,18752,18755,18757],{},[462,18753,18754],{},"AVIF q80",[462,18756,18155],{},[462,18758,2169],{},[34,18760,18762],{"id":18761},"ci-validation-caching","CI Validation & Caching",[14,18764,18765],{},"Add a fail-fast check so an oversized source image breaks the build instead of bloating the site:",[987,18767,18769],{"className":989,"code":18768,"language":991,"meta":712,"style":712},"#!\u002Fusr\u002Fbin\u002Fenv bash\nMAX=1048576   # 1 MB\nfind src\u002Fassets -type f \\( -name '*.png' -o -name '*.jpg' -o -name '*.jpeg' \\) | while read -r img; do\n  size=$(stat -c%s \"$img\" 2>\u002Fdev\u002Fnull || stat -f%z \"$img\")\n  if [ \"$size\" -gt \"$MAX\" ]; then\n    echo \"FAIL: $img exceeds 1MB ($size bytes)\"; exit 1\n  fi\ndone\necho \"PASS: all source images within limit\"\n",[253,18770,18771,18776,18789,18850,18896,18927,18953,18958,18963],{"__ignoreMap":712},[995,18772,18773],{"class":997,"line":998},[995,18774,18775],{"class":1001},"#!\u002Fusr\u002Fbin\u002Fenv bash\n",[995,18777,18778,18781,18783,18786],{"class":997,"line":713},[995,18779,18780],{"class":1618},"MAX",[995,18782,7317],{"class":1614},[995,18784,18785],{"class":1023},"1048576",[995,18787,18788],{"class":1001},"   # 1 MB\n",[995,18790,18791,18794,18797,18800,18803,18806,18809,18812,18815,18817,18820,18822,18824,18827,18830,18832,18835,18838,18841,18844,18847],{"class":997,"line":730},[995,18792,18793],{"class":1007},"find",[995,18795,18796],{"class":1023}," src\u002Fassets",[995,18798,18799],{"class":1010}," -type",[995,18801,18802],{"class":1023}," f",[995,18804,18805],{"class":1010}," \\(",[995,18807,18808],{"class":1010}," -name",[995,18810,18811],{"class":1023}," '*.png'",[995,18813,18814],{"class":1010}," -o",[995,18816,18808],{"class":1010},[995,18818,18819],{"class":1023}," '*.jpg'",[995,18821,18814],{"class":1010},[995,18823,18808],{"class":1010},[995,18825,18826],{"class":1023}," '*.jpeg'",[995,18828,18829],{"class":1010}," \\)",[995,18831,14477],{"class":1614},[995,18833,18834],{"class":1614}," while",[995,18836,18837],{"class":1010}," read",[995,18839,18840],{"class":1010}," -r",[995,18842,18843],{"class":1023}," img",[995,18845,18846],{"class":1618},"; ",[995,18848,18849],{"class":1614},"do\n",[995,18851,18852,18855,18857,18860,18863,18866,18868,18871,18874,18877,18880,18882,18885,18888,18890,18892,18894],{"class":997,"line":1544},[995,18853,18854],{"class":1618},"  size",[995,18856,7317],{"class":1614},[995,18858,18859],{"class":1618},"$(",[995,18861,18862],{"class":1010},"stat",[995,18864,18865],{"class":1010}," -c%s",[995,18867,4983],{"class":1023},[995,18869,18870],{"class":1618},"$img",[995,18872,18873],{"class":1023},"\"",[995,18875,18876],{"class":1614}," 2>",[995,18878,18879],{"class":1023},"\u002Fdev\u002Fnull",[995,18881,9333],{"class":1614},[995,18883,18884],{"class":1010}," stat",[995,18886,18887],{"class":1010}," -f%z",[995,18889,4983],{"class":1023},[995,18891,18870],{"class":1618},[995,18893,18873],{"class":1023},[995,18895,1835],{"class":1618},[995,18897,18898,18901,18904,18906,18909,18911,18914,18916,18919,18921,18924],{"class":997,"line":1550},[995,18899,18900],{"class":1614},"  if",[995,18902,18903],{"class":1618}," [ ",[995,18905,18873],{"class":1023},[995,18907,18908],{"class":1618},"$size",[995,18910,18873],{"class":1023},[995,18912,18913],{"class":1614}," -gt",[995,18915,4983],{"class":1023},[995,18917,18918],{"class":1618},"$MAX",[995,18920,18873],{"class":1023},[995,18922,18923],{"class":1618}," ]; ",[995,18925,18926],{"class":1614},"then\n",[995,18928,18929,18932,18935,18937,18940,18942,18945,18947,18950],{"class":997,"line":1673},[995,18930,18931],{"class":1010},"    echo",[995,18933,18934],{"class":1023}," \"FAIL: ",[995,18936,18870],{"class":1618},[995,18938,18939],{"class":1023}," exceeds 1MB (",[995,18941,18908],{"class":1618},[995,18943,18944],{"class":1023}," bytes)\"",[995,18946,18846],{"class":1618},[995,18948,18949],{"class":1010},"exit",[995,18951,18952],{"class":1010}," 1\n",[995,18954,18955],{"class":997,"line":1678},[995,18956,18957],{"class":1614},"  fi\n",[995,18959,18960],{"class":997,"line":1693},[995,18961,18962],{"class":1614},"done\n",[995,18964,18965,18968],{"class":997,"line":1705},[995,18966,18967],{"class":1010},"echo",[995,18969,18970],{"class":1023}," \"PASS: all source images within limit\"\n",[14,18972,18973],{},"Persist Astro's build cache so processed images aren't regenerated every run:",[987,18975,18977],{"className":1912,"code":18976,"language":1914,"meta":712,"style":712},"- uses: actions\u002Fcache@v4\n  with:\n    path: node_modules\u002F.astro\n    key: ${{ runner.os }}-astro-${{ hashFiles('src\u002Fassets\u002F**') }}\n",[253,18978,18979,18989,18995,19004],{"__ignoreMap":712},[995,18980,18981,18983,18985,18987],{"class":997,"line":998},[995,18982,3191],{"class":1618},[995,18984,1978],{"class":1921},[995,18986,1925],{"class":1618},[995,18988,3198],{"class":1023},[995,18990,18991,18993],{"class":997,"line":713},[995,18992,3203],{"class":1921},[995,18994,1946],{"class":1618},[995,18996,18997,18999,19001],{"class":997,"line":730},[995,18998,3210],{"class":1921},[995,19000,1925],{"class":1618},[995,19002,19003],{"class":1023},"node_modules\u002F.astro\n",[995,19005,19006,19008,19010],{"class":997,"line":1544},[995,19007,3235],{"class":1921},[995,19009,1925],{"class":1618},[995,19011,19012],{"class":1023},"${{ runner.os }}-astro-${{ hashFiles('src\u002Fassets\u002F**') }}\n",[14,19014,19015,19016,19018,19019,19022,19023,19025,19026,19028],{},"On a 400-image content site, caching ",[253,19017,3170],{}," cut the image-processing portion of the build from ",[229,19020,19021],{},"95s to 12s",". The same caching discipline applies to other generators — see ",[23,19024,5002],{"href":5001},". Coordinate with ",[23,19027,10896],{"href":10895}," on image-heavy interactive pages so optimized images aren't undone by excess JS.",[34,19030,19032],{"id":19031},"measurement","Measurement",[14,19034,19035],{},"Gate deploys on a Lighthouse budget and track the LCP delta on the page whose hero you changed:",[987,19037,19039],{"className":989,"code":19038,"language":991,"meta":712,"style":712},"npx lhci autorun \\\n  --collect.url=https:\u002F\u002Fpreview-deploy-url.example.com \\\n  --assert.preset=lighthouse:recommended\n",[253,19040,19041,19052,19059],{"__ignoreMap":712},[995,19042,19043,19045,19048,19050],{"class":997,"line":998},[995,19044,1079],{"class":1007},[995,19046,19047],{"class":1023}," lhci",[995,19049,16650],{"class":1023},[995,19051,3002],{"class":1010},[995,19053,19054,19057],{"class":997,"line":713},[995,19055,19056],{"class":1010},"  --collect.url=https:\u002F\u002Fpreview-deploy-url.example.com",[995,19058,3002],{"class":1010},[995,19060,19061],{"class":997,"line":730},[995,19062,19063],{"class":1010},"  --assert.preset=lighthouse:recommended\n",[14,19065,19066,19067,19069,19070,19072],{},"Automated pipelines strip EXIF, so keep ",[253,19068,8266],{}," text in your content (not in image metadata) to preserve accessibility. Pair the lab number with field data — a CDN that serves the wrong ",[253,19071,13866],{}," header can defeat format negotiation in production even when the lab looks perfect.",[34,19074,2266],{"id":2265},[39,19076,19077,19092,19105,19114],{},[42,19078,19079,692,19082,19084,19085,270,19088,19091],{},[229,19080,19081],{},"Lazy-loading the hero:",[253,19083,4897],{}," on the LCP image delays it. Use ",[253,19086,19087],{},"loading=\"eager\"",[253,19089,19090],{},"fetchpriority=\"high\""," for above-the-fold visuals.",[42,19093,19094,19096,19097,19099,19100,738,19102,19104],{},[229,19095,18221],{}," without width\u002Fheight (or ",[253,19098,18229],{},"), the browser can't reserve space and CLS spikes. ",[253,19101,18510],{},[253,19103,2162],{}," require dimensions for local images, which is exactly why they help.",[42,19106,19107,19110,19111,19113],{},[229,19108,19109],{},"Build timeouts on huge media dirs:"," processing hundreds of large images can exhaust a runner. Cache ",[253,19112,3170],{},", limit Sharp concurrency, or offload originals to a CDN.",[42,19115,19116,19119,19120,19123],{},[229,19117,19118],{},"Over-aggressive quality cuts:"," dropping below ",[253,19121,19122],{},"quality={60}"," to chase bytes produces visible banding; reach for a smaller width instead.",[34,19125,2321],{"id":2320},[39,19127,19128,19140,19143,19149],{},[42,19129,19130,19131,19134,19135,738,19137,19139],{},"Let Astro do the work: ",[253,19132,19133],{},"npm i sharp",", then ",[253,19136,18510],{},[253,19138,2162],{}," with explicit dimensions.",[42,19141,19142],{},"Emit AVIF and WebP together so every browser gets the smallest file it can decode.",[42,19144,19145,19146,19148],{},"Protect the pipeline with a CI size check and a cached ",[253,19147,3170],{}," build.",[42,19150,19151],{},"Always measure the LCP delta on the specific page you changed — images are usually the LCP element.",[34,19153,651],{"id":650},[653,19155,19157],{"id":19156},"does-astro-optimize-images-at-build-time-or-runtime","Does Astro optimize images at build time or runtime?",[14,19159,19160,19161,19163],{},"Build time. Optimized files are written to ",[253,19162,8885],{}," with zero runtime cost, so there is no server-side resizing penalty when a visitor loads the page.",[653,19165,19167],{"id":19166},"how-do-i-handle-remote-images","How do I handle remote images?",[14,19169,19170],{},"Fetch and cache them locally before the build, or configure Astro's allowed remote domains for its image service so it can process them during the build instead of at request time.",[653,19172,19174],{"id":19173},"can-i-bypass-optimization-for-a-specific-asset","Can I bypass optimization for a specific asset?",[14,19176,19177,19178,19180,19181,19183,19184,19186],{},"Yes — use a plain ",[253,19179,3847],{}," with a path from ",[253,19182,8881],{},", which skips the ",[253,19185,2117],{}," pipeline entirely. This is useful for pre-optimized SVGs or assets you manage elsewhere.",[653,19188,19190],{"id":19189},"how-much-do-builds-slow-down","How much do builds slow down?",[14,19192,19193,19194,19196],{},"The first build pays the processing cost; subsequent builds are much faster with ",[253,19195,3170],{}," cached. On a 400-image site the cached rebuild dropped from 95s to 12s. Limit Sharp concurrency if a runner is memory-constrained.",[653,19198,19200],{"id":19199},"should-i-use-image-or-picture","Should I use Image or Picture?",[14,19202,2360,19203,19205,19206,19208,19209,19211,19212,19214,19215,18527,19217,239],{},[253,19204,18510],{}," for a single optimized format and ",[253,19207,2162],{}," when you want to emit multiple formats (AVIF plus WebP) with a fallback. ",[253,19210,2162],{}," takes a ",[253,19213,18520],{}," array; ",[253,19216,18510],{},[253,19218,16126],{},[34,19220,684],{"id":683},[39,19222,19223,19230,19235,19240],{},[42,19224,19225,692,19227,19229],{},[229,19226,691],{},[23,19228,5501],{"href":5500}," — where images fit the LCP picture.",[42,19231,19232,19234],{},[23,19233,3852],{"href":3851}," — the framework-agnostic equivalent.",[42,19236,19237,19239],{},[23,19238,17744],{"href":18354}," — when to offload transforms to the edge.",[42,19241,19242,16848],{},[23,19243,14061],{"href":14060},[1346,19245,3395],{},{"title":712,"searchDepth":713,"depth":713,"links":19247},[19248,19249,19250,19251,19252,19253,19254,19255,19262],{"id":18480,"depth":713,"text":18481},{"id":18615,"depth":713,"text":18616},{"id":18696,"depth":713,"text":18697},{"id":18761,"depth":713,"text":18762},{"id":19031,"depth":713,"text":19032},{"id":2265,"depth":713,"text":2266},{"id":2320,"depth":713,"text":2321},{"id":650,"depth":713,"text":651,"children":19256},[19257,19258,19259,19260,19261],{"id":19156,"depth":730,"text":19157},{"id":19166,"depth":730,"text":19167},{"id":19173,"depth":730,"text":19174},{"id":19189,"depth":730,"text":19190},{"id":19199,"depth":730,"text":19200},{"id":683,"depth":713,"text":684},[19264,19265,19266],{"name":737,"item":738},{"name":5501,"item":5500},{"name":2190,"item":2189},"Optimize images at build time with Astro astro:assets — resized, modern-format images generated during the build with no runtime cost, plus CI checks and measurement.",[19269,19271,19272,19274,19276],{"q":19157,"a":19270},"Build time. Optimized files are written to dist\u002F with zero runtime cost, so there is no server-side resizing penalty when a visitor loads the page.",{"q":19167,"a":19170},{"q":19174,"a":19273},"Yes — use a plain img tag with a path from public\u002F, which skips the astro:assets pipeline entirely. This is useful for pre-optimized SVGs or assets you manage elsewhere.",{"q":19190,"a":19275},"The first build pays the processing cost; subsequent builds are much faster with node_modules\u002F.astro cached. On a 400-image site the cached rebuild dropped from 95s to 12s in our measurement. Limit Sharp concurrency if a runner is memory-constrained.",{"q":19200,"a":19277},"Use Image for a single optimized format and Picture when you want to emit multiple formats (AVIF plus WebP) with a fallback. Picture takes a formats array; Image takes a single format.",{},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fimage-optimization-pipelines-in-astro",{"title":2190,"description":19267},"performance-optimization-core-web-vitals-for-ssgs\u002Fimage-optimization-pipelines-in-astro\u002Findex","xgNvrSv8tjOXrfZk4___dazlSVLlMJuxDUWYlfXPQIA",{"id":19284,"title":3852,"body":19285,"breadcrumb":20053,"dateModified":743,"datePublished":2446,"description":20058,"extension":745,"faq":20059,"meta":20070,"navigation":752,"path":20071,"seo":20072,"slug":19289,"stem":20073,"type":756,"__hash__":20074},"content\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fimage-optimization-pipelines-in-astro\u002Foptimizing-webp-images-in-hugo-without-plugins\u002Findex.md",{"type":7,"value":19286,"toc":20032},[19287,19290,19302,19304,19359,19363,19392,19481,19485,19489,19506,19510,19526,19581,19594,19598,19601,19675,19679,19685,19729,19744,19746,19756,19801,19814,19816,19896,19898,19920,19922,19926,19937,19941,19957,19961,19976,19980,19989,19993,20003,20005,20029],[10,19288,3852],{"id":19289},"optimizing-webp-images-in-hugo-without-plugins",[14,19291,19292,19293,19296,19297,4582,19299,19301],{},"Hugo can generate WebP images during the build with nothing but its own image-processing functions — no Node tooling, no third-party modules, no system image libraries. The one real requirement is the ",[229,19294,19295],{},"extended"," edition of Hugo, which ships with a WebP encoder compiled in. For a content site that means modern-format images, smaller transfers, and a faster Largest Contentful Paint (LCP) without adding a single dependency to your toolchain. This is the Hugo counterpart to ",[23,19298,2190],{"href":2189},[23,19300,5501],{"href":5500},", where the image is almost always the LCP element.",[34,19303,37],{"id":36},[39,19305,19306,19325,19352],{},[42,19307,19308,19313,19314,19317,19318,19321,19322,239],{},[229,19309,19310],{},[253,19311,19312],{},"hugo-extended"," installed locally and in CI. Confirm with ",[253,19315,19316],{},"hugo version"," — the output must contain ",[253,19319,19320],{},"+extended",". The plain binary cannot encode WebP and will fail with ",[253,19323,19324],{},"image processing not available",[42,19326,19327,19328,19331,19332,738,19334,19337,19338,19340,19341,19344,19345,19348,19349,19351],{},"Source images placed where Hugo processes them: either ",[253,19329,19330],{},"assets\u002F"," (global, via ",[253,19333,3870],{},[253,19335,19336],{},"resources.GetMatch",") or a page bundle in ",[253,19339,2765],{}," (via ",[253,19342,19343],{},".Resources.Get","). Files under ",[253,19346,19347],{},"static\u002F"," are copied verbatim and ",[229,19350,3112],{}," processed.",[42,19353,19354,19355,19358],{},"A way to verify byte sizes after the build — ",[253,19356,19357],{},"ls -lh public\u002F"," or your browser's network panel against a deploy preview.",[34,19360,19362],{"id":19361},"how-hugos-image-pipeline-works","How Hugo's Image Pipeline Works",[14,19364,19365,19366,19368,19369,19371,19372,2204,19375,19378,19379,1850,19381,1850,19383,1850,19385,19388,19389,19391],{},"Hugo processes images at build time using its built-in Go image libraries — not an external dependency like libvips or Sharp. WebP encoding specifically is part of ",[253,19367,19312],{},", so the only system requirement is using that edition; you do ",[229,19370,3112],{}," install ",[253,19373,19374],{},"libwebp",[253,19376,19377],{},"libvips",". Each call to a processing method (",[253,19380,3699],{},[253,19382,3702],{},[253,19384,3705],{},[253,19386,19387],{},"Crop",") produces a new image resource, and Hugo writes the result once and caches it in ",[253,19390,3253],{},", keyed on the source bytes plus the options string. Later builds reuse the cached variant, so a clean first build pays the encoding cost and every rebuild is nearly free.",[55,19393,19394,19478],{},[58,19395,66,19400,66,19403,66,19406,66,19471],{"viewBox":19396,"role":61,"ariaLabelledBy":19397,"xmlns":65},"0 0 820 320",[19398,19399],"hugo-webp-title","hugo-webp-desc",[68,19401,19402],{"id":19398},"Hugo native image-processing flow to WebP",[72,19404,19405],{"id":19399},"A source JPEG in assets or a page bundle is read with resources.GetMatch, processed by Resize or Fill in hugo-extended, encoded to WebP and cached in resources slash underscore gen, then served inside a picture element with a JPEG fallback.",[95,19407,78,19408,78,19411,78,19413,78,19415,78,19419,78,19421,78,19423,78,19426,78,19429,78,19432,78,19434,78,19438,78,19441,78,19444,78,19447,78,19450,78,19453,78,19456,78,19468,66],{"style":813},[99,19409,19410],{"x":1415,"y":2521,"fill":103,"style":1416},"One source in, a cached WebP variant out — no plugins",[107,19412],{"x":5393,"y":159,"width":161,"height":1430,"rx":823,"fill":162,"opacity":877,"stroke":164,"style":116},[99,19414,18403],{"x":15952,"y":5379,"fill":103,"style":121},[99,19416,19418],{"x":15952,"y":19417,"fill":93,"style":126},"163","assets\u002F or bundle",[99,19420,11036],{"x":15952,"y":160,"fill":93,"style":126},[107,19422],{"x":3500,"y":4682,"width":194,"height":4682,"rx":823,"fill":114,"opacity":186,"stroke":114,"style":116},[99,19424,19312],{"x":19425,"y":2535,"fill":114,"style":121},"295",[99,19427,3522],{"x":19425,"y":19428,"fill":93,"style":126},"154",[99,19430,19431],{"x":19425,"y":10805,"fill":93,"style":126},"\"800x webp q80\"",[107,19433],{"x":5338,"y":4682,"width":194,"height":4682,"rx":823,"fill":185,"opacity":850,"stroke":187,"style":116},[99,19435,19437],{"x":19436,"y":2535,"fill":187,"style":121},"505","WebP variant",[99,19439,19440],{"x":19436,"y":19428,"fill":103,"style":126},"~96 KB",[99,19442,19443],{"x":19436,"y":10805,"fill":93,"style":126},"cached in resources\u002F_gen",[107,19445],{"x":19446,"y":4682,"width":194,"height":4682,"rx":823,"fill":824,"opacity":1432,"stroke":824,"style":116},"630",[99,19448,8172],{"x":19449,"y":2535,"fill":824,"style":121},"715",[99,19451,19452],{"x":19449,"y":19428,"fill":93,"style":126},"WebP source +",[99,19454,19455],{"x":19449,"y":10805,"fill":93,"style":126},"JPEG fallback",[95,19457,88,19458,88,19462,88,19465,78],{"stroke":93,"fill":205,"style":116},[90,19459],{"d":19460,"style":19461},"M170 150 L208 150","marker-end:url(#hugo-arrow)",[90,19463],{"d":19464,"style":19461},"M380 150 L418 150",[90,19466],{"d":19467,"style":19461},"M590 150 L628 150",[99,19469,19470],{"x":1415,"y":11924,"fill":93,"style":126},"First build encodes; every rebuild reuses the cached variant from resources\u002F_gen.",[76,19472,78,19473,66],{},[80,19474,88,19476,78],{"id":19475,"viewBox":83,"refX":84,"refY":85,"markerWidth":86,"markerHeight":86,"orient":87},"hugo-arrow",[90,19477],{"d":92,"fill":93},[218,19479,19480],{},"A 1.4 MB source is read from assets or a page bundle, resized and encoded to a ~96 KB WebP by hugo-extended, cached in resources\u002F_gen, and served inside a picture element with a JPEG fallback.",[34,19482,19484],{"id":19483},"the-recipe","The Recipe",[653,19486,19488],{"id":19487},"_1-confirm-the-extended-edition","1. Confirm the extended edition",[987,19490,19492],{"className":989,"code":19491,"language":991,"meta":712,"style":712},"hugo version\n# hugo v0.x.x+extended ... — the +extended suffix is required for WebP\n",[253,19493,19494,19501],{"__ignoreMap":712},[995,19495,19496,19498],{"class":997,"line":998},[995,19497,259],{"class":1007},[995,19499,19500],{"class":1023}," version\n",[995,19502,19503],{"class":997,"line":713},[995,19504,19505],{"class":1001},"# hugo v0.x.x+extended ... — the +extended suffix is required for WebP\n",[653,19507,19509],{"id":19508},"_2-generate-webp-in-a-template","2. Generate WebP in a template",[14,19511,19512,19513,19515,19516,19519,19520,19522,19523,19525],{},"Use a ",[253,19514,8172],{}," element with a WebP ",[253,19517,19518],{},"\u003Csource>"," and an original-format ",[253,19521,3847],{}," fallback. ",[253,19524,3699],{}," takes the dimensions and options — format and quality — in one space-separated string:",[987,19527,19529],{"className":3731,"code":19528,"language":3733,"meta":712,"style":712},"{{ $img := resources.GetMatch .Params.src }}\n{{ $webp := $img.Resize \"800x webp q80\" }}\n{{ $fallback := $img.Resize \"800x q80\" }}\n\u003Cpicture>\n  \u003Csource srcset=\"{{ $webp.RelPermalink }}\" type=\"image\u002Fwebp\">\n  \u003Cimg src=\"{{ $fallback.RelPermalink }}\"\n       alt=\"{{ .Params.alt }}\"\n       width=\"{{ $fallback.Width }}\" height=\"{{ $fallback.Height }}\"\n       loading=\"lazy\" decoding=\"async\">\n\u003C\u002Fpicture>\n",[253,19530,19531,19536,19541,19546,19551,19556,19561,19566,19571,19576],{"__ignoreMap":712},[995,19532,19533],{"class":997,"line":998},[995,19534,19535],{},"{{ $img := resources.GetMatch .Params.src }}\n",[995,19537,19538],{"class":997,"line":713},[995,19539,19540],{},"{{ $webp := $img.Resize \"800x webp q80\" }}\n",[995,19542,19543],{"class":997,"line":730},[995,19544,19545],{},"{{ $fallback := $img.Resize \"800x q80\" }}\n",[995,19547,19548],{"class":997,"line":1544},[995,19549,19550],{},"\u003Cpicture>\n",[995,19552,19553],{"class":997,"line":1550},[995,19554,19555],{},"  \u003Csource srcset=\"{{ $webp.RelPermalink }}\" type=\"image\u002Fwebp\">\n",[995,19557,19558],{"class":997,"line":1673},[995,19559,19560],{},"  \u003Cimg src=\"{{ $fallback.RelPermalink }}\"\n",[995,19562,19563],{"class":997,"line":1678},[995,19564,19565],{},"       alt=\"{{ .Params.alt }}\"\n",[995,19567,19568],{"class":997,"line":1693},[995,19569,19570],{},"       width=\"{{ $fallback.Width }}\" height=\"{{ $fallback.Height }}\"\n",[995,19572,19573],{"class":997,"line":1705},[995,19574,19575],{},"       loading=\"lazy\" decoding=\"async\">\n",[995,19577,19578],{"class":997,"line":1711},[995,19579,19580],{},"\u003C\u002Fpicture>\n",[14,19582,4324,19583,270,19585,19587,19588,19590,19591,19593],{},[253,19584,9286],{},[253,19586,18059],{}," from the processed resource reserves layout space and keeps Cumulative Layout Shift (CLS) at zero. Use ",[253,19589,4897],{}," for below-the-fold images — but ",[229,19592,3112],{}," for the LCP image, which should be eager (see Pitfalls).",[653,19595,19597],{"id":19596},"_3-emit-a-responsive-set-for-the-hero","3. Emit a responsive set for the hero",[14,19599,19600],{},"For an above-the-fold hero, generate several widths and let the browser choose, while keeping the WebP\u002Ffallback split:",[987,19602,19604],{"className":3731,"code":19603,"language":3733,"meta":712,"style":712},"{{ $img := resources.GetMatch .Params.src }}\n{{ $small := $img.Resize \"480x webp q80\" }}\n{{ $mid := $img.Resize \"800x webp q80\" }}\n{{ $large := $img.Resize \"1200x webp q80\" }}\n{{ $fallback := $img.Resize \"1200x q82\" }}\n\u003Cpicture>\n  \u003Csource\n    type=\"image\u002Fwebp\"\n    sizes=\"(max-width: 800px) 100vw, 1200px\"\n    srcset=\"{{ $small.RelPermalink }} 480w, {{ $mid.RelPermalink }} 800w, {{ $large.RelPermalink }} 1200w\">\n  \u003Cimg src=\"{{ $fallback.RelPermalink }}\"\n       alt=\"{{ .Params.alt }}\"\n       width=\"{{ $fallback.Width }}\" height=\"{{ $fallback.Height }}\"\n       fetchpriority=\"high\">\n\u003C\u002Fpicture>\n",[253,19605,19606,19610,19615,19620,19625,19630,19634,19639,19644,19649,19654,19658,19662,19666,19671],{"__ignoreMap":712},[995,19607,19608],{"class":997,"line":998},[995,19609,19535],{},[995,19611,19612],{"class":997,"line":713},[995,19613,19614],{},"{{ $small := $img.Resize \"480x webp q80\" }}\n",[995,19616,19617],{"class":997,"line":730},[995,19618,19619],{},"{{ $mid := $img.Resize \"800x webp q80\" }}\n",[995,19621,19622],{"class":997,"line":1544},[995,19623,19624],{},"{{ $large := $img.Resize \"1200x webp q80\" }}\n",[995,19626,19627],{"class":997,"line":1550},[995,19628,19629],{},"{{ $fallback := $img.Resize \"1200x q82\" }}\n",[995,19631,19632],{"class":997,"line":1673},[995,19633,19550],{},[995,19635,19636],{"class":997,"line":1678},[995,19637,19638],{},"  \u003Csource\n",[995,19640,19641],{"class":997,"line":1693},[995,19642,19643],{},"    type=\"image\u002Fwebp\"\n",[995,19645,19646],{"class":997,"line":1705},[995,19647,19648],{},"    sizes=\"(max-width: 800px) 100vw, 1200px\"\n",[995,19650,19651],{"class":997,"line":1711},[995,19652,19653],{},"    srcset=\"{{ $small.RelPermalink }} 480w, {{ $mid.RelPermalink }} 800w, {{ $large.RelPermalink }} 1200w\">\n",[995,19655,19656],{"class":997,"line":1717},[995,19657,19560],{},[995,19659,19660],{"class":997,"line":1726},[995,19661,19565],{},[995,19663,19664],{"class":997,"line":1732},[995,19665,19570],{},[995,19667,19668],{"class":997,"line":2967},[995,19669,19670],{},"       fetchpriority=\"high\">\n",[995,19672,19673],{"class":997,"line":2972},[995,19674,19580],{},[653,19676,19678],{"id":19677},"_4-set-global-quality-defaults","4. Set global quality defaults",[14,19680,19681,19682,19684],{},"Pin defaults in ",[253,19683,12901],{}," so output is consistent across templates:",[987,19686,19688],{"className":2792,"code":19687,"language":2794,"meta":712,"style":712},"[imaging]\n  quality = 80\n  resampleFilter = \"Lanczos\"\n  anchor = \"smart\"\n\n[imaging.exif]\n  disableDate = true\n  disableLatLong = true\n",[253,19689,19690,19695,19700,19705,19710,19714,19719,19724],{"__ignoreMap":712},[995,19691,19692],{"class":997,"line":998},[995,19693,19694],{},"[imaging]\n",[995,19696,19697],{"class":997,"line":713},[995,19698,19699],{},"  quality = 80\n",[995,19701,19702],{"class":997,"line":730},[995,19703,19704],{},"  resampleFilter = \"Lanczos\"\n",[995,19706,19707],{"class":997,"line":1544},[995,19708,19709],{},"  anchor = \"smart\"\n",[995,19711,19712],{"class":997,"line":1550},[995,19713,1541],{"emptyLinePlaceholder":752},[995,19715,19716],{"class":997,"line":1673},[995,19717,19718],{},"[imaging.exif]\n",[995,19720,19721],{"class":997,"line":1678},[995,19722,19723],{},"  disableDate = true\n",[995,19725,19726],{"class":997,"line":1693},[995,19727,19728],{},"  disableLatLong = true\n",[14,19730,19731,19733,19734,19737,19738,19740,19741,19743],{},[253,19732,9291],{}," accepts 1–100; ",[253,19735,19736],{},"anchor = \"smart\""," picks a focal point when you crop with ",[253,19739,3705],{},"; stripping EXIF date and GPS keeps output lean and avoids leaking metadata. This is the native equivalent of the build-time compression covered for Astro in ",[23,19742,17744],{"href":18354},", which is the route to reach for when you want transforms at the edge instead of at build time.",[34,19745,1166],{"id":1165},[14,19747,19748,19749,19752,19753,19755],{},"On a documentation page whose hero was a 1.4 MB JPEG at 1200px, switching to native Hugo WebP at ",[253,19750,19751],{},"q80"," produced the following, measured with ",[253,19754,19357],{}," for bytes and a throttled mid-tier mobile Lighthouse run for LCP:",[433,19757,19758,19769],{},[436,19759,19760],{},[439,19761,19762,19765,19767],{},[442,19763,19764],{},"Variant (1200px hero)",[442,19766,18725],{},[442,19768,10936],{},[457,19770,19771,19780,19791],{},[439,19772,19773,19775,19777],{},[462,19774,18734],{},[462,19776,11036],{},[462,19778,19779],{},"3.0 s",[439,19781,19782,19785,19788],{},[462,19783,19784],{},"Hugo JPEG q82",[462,19786,19787],{},"220 KB",[462,19789,19790],{},"2.0 s",[439,19792,19793,19796,19799],{},[462,19794,19795],{},"Hugo WebP q80",[462,19797,19798],{},"96 KB",[462,19800,1206],{},[14,19802,19803,19804,19806,19807,19809,19810,239],{},"WebP at ",[253,19805,19751],{}," is about 56% smaller than Hugo's own re-encoded JPEG and roughly 93% smaller than the original, cutting LCP from 3.0 s to 1.6 s. Build time on the page was unaffected after the first run because the variant is cached in ",[253,19808,3253],{},". The same priority-hint and hero-sizing logic that earns the last few hundred milliseconds is covered framework-agnostically in ",[23,19811,19813],{"href":19812},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Flargest-contentful-paint-optimization-for-static-sites\u002Foptimizing-lcp-on-astro-with-priority-hints\u002F","Optimizing LCP on Astro with Priority Hints",[34,19815,600],{"id":599},[39,19817,19818,19832,19852,19863,19875,19885],{},[42,19819,19820,19825,19826,19828,19829,19831],{},[229,19821,19822,19824],{},[253,19823,19336],{}," returns nil:"," wrong case, the image lives under ",[253,19827,19347],{},", or it is outside both ",[253,19830,19330],{}," and any page bundle. Move it into a processed scope and match the exact filename — Linux filesystems are case-sensitive.",[42,19833,19834,19838,19839,19841,19842,19844,19845,19847,19848,2204,19850,239],{},[229,19835,19836,931],{},[253,19837,19324],{}," you are on the standard ",[253,19840,259],{}," binary. Install ",[253,19843,19312],{},"; you do ",[229,19846,3112],{}," need system ",[253,19849,19374],{},[253,19851,19377],{},[42,19853,19854,692,19857,19859,19860,19862],{},[229,19855,19856],{},"Lazy-loading the LCP image:",[253,19858,4897],{}," on the hero delays it and worsens LCP. Use eager loading plus ",[253,19861,19090],{}," for the above-the-fold image.",[42,19864,19865,19868,19869,19871,19872,19874],{},[229,19866,19867],{},"Stale variants after editing a source:"," Hugo keys the cache on the source bytes. If you replace an image but keep the filename, run ",[253,19870,5159],{}," or delete ",[253,19873,3253],{}," to force regeneration.",[42,19876,19877,19880,19881,19884],{},[229,19878,19879],{},"Over-cutting quality:"," below ",[253,19882,19883],{},"q60"," you start to see banding on gradients. Reach for a smaller width before dropping quality further.",[42,19886,19887,19889,19890,19892,19893,19895],{},[229,19888,637],{}," the change is entirely in templates and ",[253,19891,12901],{},". Revert the commit and rebuild — ",[253,19894,3253],{}," regenerates the old variants on the next build, so there is no external state to undo.",[34,19897,642],{"id":641},[14,19899,19900,19901,19903,19904,19906,19907,19910,19911,19913,19914,19916,19917,239],{},"Native WebP in Hugo is three things: run ",[253,19902,19312],{},", keep sources in ",[253,19905,19330],{}," or a page bundle, and call ",[253,19908,19909],{},"Resize \"…x webp qNN\""," inside a ",[253,19912,8172],{}," with a fallback. No plugins, no Node, no system image libraries — ",[253,19915,3253],{}," keeps rebuilds fast, and a 1.4 MB hero drops to under 100 KB with LCP nearly halved. The same build-time discipline, expressed in each generator's own tooling, runs through the whole ",[23,19918,19919],{"href":2189},"image pipeline guide",[34,19921,651],{"id":650},[653,19923,19925],{"id":19924},"does-hugo-need-external-binaries-for-webp","Does Hugo need external binaries for WebP?",[14,19927,19928,19929,19931,19932,2204,19934,19936],{},"No system libraries. WebP encoding is compiled into the ",[253,19930,19312],{}," edition, so the only requirement is using that build of Hugo. You do not need to install ",[253,19933,19374],{},[253,19935,19377],{}," on the host or in your CI image.",[653,19938,19940],{"id":19939},"can-i-convert-existing-jpeg-and-png-sources-to-webp","Can I convert existing JPEG and PNG sources to WebP?",[14,19942,19943,19944,1850,19946,10335,19948,19950,19951,19954,19955,239],{},"Yes, at build time. Reference the source through ",[253,19945,3699],{},[253,19947,3702],{},[253,19949,3705],{}," with the ",[253,19952,19953],{},"webp"," format in the options string and Hugo emits a WebP variant. The original file is never modified; the variant is written to the output directory and cached in ",[253,19956,3253],{},[653,19958,19960],{"id":19959},"how-do-i-keep-legacy-browser-support","How do I keep legacy-browser support?",[14,19962,19963,19964,19966,19967,19969,19970,19519,19973,19975],{},"Wrap the WebP variant in a ",[253,19965,8172],{}," element with a ",[253,19968,19518],{}," of type ",[253,19971,19972],{},"image\u002Fwebp",[253,19974,3847],{}," fallback. Browsers that support WebP take the source; the rest load the fallback automatically, so no browser is left without an image.",[653,19977,19979],{"id":19978},"why-does-the-build-say-image-processing-not-available","Why does the build say image processing not available?",[14,19981,19982,19983,19985,19986,19988],{},"You are running the standard ",[253,19984,259],{}," binary, which has no WebP encoder. Switch to the ",[253,19987,19312],{}," edition. The error is specifically about the missing encoder, not about a missing system library.",[653,19990,19992],{"id":19991},"how-do-i-avoid-reprocessing-every-image-on-each-build","How do I avoid reprocessing every image on each build?",[14,19994,19995,19996,19998,19999,20002],{},"Hugo caches processed variants in ",[253,19997,3253],{},", keyed on the source and the processing options. As long as that directory persists between builds, unchanged images are reused. In CI, cache the ",[253,20000,20001],{},"resources"," directory so the cache survives across runs.",[34,20004,684],{"id":683},[39,20006,20007,20014,20019,20024],{},[42,20008,20009,692,20011,20013],{},[229,20010,691],{},[23,20012,2190],{"href":2189}," — the broader build-time image pipeline this Hugo recipe sits under.",[42,20015,20016,20018],{},[23,20017,17744],{"href":18354}," — when to move transforms to the edge instead of the build.",[42,20020,20021,20023],{},[23,20022,19813],{"href":19812}," — the priority-hint work that earns the last LCP milliseconds.",[42,20025,20026,20028],{},[23,20027,5501],{"href":5500}," — where image optimization fits the LCP picture.",[1346,20030,20031],{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":712,"searchDepth":713,"depth":713,"links":20033},[20034,20035,20036,20042,20043,20044,20045,20052],{"id":36,"depth":713,"text":37},{"id":19361,"depth":713,"text":19362},{"id":19483,"depth":713,"text":19484,"children":20037},[20038,20039,20040,20041],{"id":19487,"depth":730,"text":19488},{"id":19508,"depth":730,"text":19509},{"id":19596,"depth":730,"text":19597},{"id":19677,"depth":730,"text":19678},{"id":1165,"depth":713,"text":1166},{"id":599,"depth":713,"text":600},{"id":641,"depth":713,"text":642},{"id":650,"depth":713,"text":651,"children":20046},[20047,20048,20049,20050,20051],{"id":19924,"depth":730,"text":19925},{"id":19939,"depth":730,"text":19940},{"id":19959,"depth":730,"text":19960},{"id":19978,"depth":730,"text":19979},{"id":19991,"depth":730,"text":19992},{"id":683,"depth":713,"text":684},[20054,20055,20056,20057],{"name":737,"item":738},{"name":5501,"item":5500},{"name":2190,"item":2189},{"name":3852,"item":3851},"Generate WebP at build time using only Hugo's built-in image processing — no Node, no plugins, no system libraries. Requires the hugo-extended edition for WebP.",[20060,20062,20064,20066,20068],{"q":19925,"a":20061},"No system libraries. WebP encoding is compiled into the hugo-extended edition, so the only requirement is using that build of Hugo. You do not need to install libwebp or libvips on the host or in your CI image.",{"q":19940,"a":20063},"Yes, at build time. Reference the source through Resize, Fit, or Fill with the webp format in the options string and Hugo emits a WebP variant. The original file is never modified; the variant is written to the output directory and cached in resources\u002F_gen.",{"q":19960,"a":20065},"Wrap the WebP variant in a picture element with a source of type image\u002Fwebp and an original-format img fallback. Browsers that support WebP take the source; the rest load the fallback automatically, so no browser is left without an image.",{"q":19979,"a":20067},"You are running the standard hugo binary, which has no WebP encoder. Switch to the hugo-extended edition. The error is specifically about the missing encoder, not about a missing system library.",{"q":19992,"a":20069},"Hugo caches processed variants in resources\u002F_gen, keyed on the source and the processing options. As long as that directory persists between builds, unchanged images are reused. In CI, cache the resources directory so the cache survives across runs.",{},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fimage-optimization-pipelines-in-astro\u002Foptimizing-webp-images-in-hugo-without-plugins",{"title":3852,"description":20058},"performance-optimization-core-web-vitals-for-ssgs\u002Fimage-optimization-pipelines-in-astro\u002Foptimizing-webp-images-in-hugo-without-plugins\u002Findex","XsGTEYqtDRqa2EcfXgA9ROBbIQtjtEK8z2grNm0te3o",{"id":20076,"title":20077,"body":20078,"breadcrumb":20736,"dateModified":743,"datePublished":2446,"description":20739,"extension":745,"faq":20740,"meta":20747,"navigation":752,"path":20748,"seo":20749,"slug":20082,"stem":20750,"type":6089,"__hash__":20751},"content\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Findex.md","Core Web Vitals Optimization for SSGs",{"type":7,"value":20079,"toc":20716},[20080,20083,20086,20089,20192,20194,20197,20232,20236,20239,20245,20249,20252,20272,20275,20349,20353,20356,20388,20392,20398,20404,20476,20480,20491,20495,20505,20541,20546,20550,20553,20556,20580,20585,20587,20616,20618,20635,20637,20641,20644,20648,20651,20655,20667,20671,20674,20678,20681,20683,20713],[10,20081,5501],{"id":20082},"performance-optimization-core-web-vitals-for-ssgs",[14,20084,20085],{},"Static site generators give you a head start on Core Web Vitals — pre-rendered HTML means fast Largest Contentful Paint (LCP) and stable layout by default. But production performance isn't automatic. It comes from disciplined asset processing, correct cache headers at the edge, and keeping JavaScript off pages that don't need it. This guide is for engineers and documentation teams who already ship a static site and want to turn \"fast by default\" into \"fast under real-world load.\"",[14,20087,20088],{},"We cover the four levers in order of impact: build-time assets, edge delivery, hydration, and continuous measurement. Each one ties back to a specific metric and a specific tool you can use to measure it.",[55,20090,20091,20189],{},[58,20092,66,20097,66,20100,66,20103,66,20105,66,20182],{"viewBox":20093,"role":61,"ariaLabelledBy":20094,"xmlns":65},"0 0 800 320",[20095,20096],"cwv-flow-title","cwv-flow-desc",[68,20098,20099],{"id":20095},"From build to browser: where each Core Web Vital is won",[72,20101,20102],{"id":20096},"A four-stage pipeline showing build-time asset optimization, edge CDN delivery, client hydration, and continuous measurement, each annotated with the Core Web Vital it most affects.",[107,20104],{"x":2515,"y":2515,"width":8298,"height":1463,"fill":205},[95,20106,78,20108,78,20110,78,20112,78,20115,78,20118,78,20121,78,20124,78,20127,78,20130,78,20132,78,20135,78,20137,78,20141,78,20144,78,20147,78,20150,78,20152,78,20155,78,20158,78,20161,78,20164,78,20176,78,20179,66],{"style":20107},"font-family:system-ui, sans-serif;font-size:15px",[107,20109],{"x":5393,"y":849,"width":194,"height":1431,"rx":113,"fill":824,"opacity":825,"stroke":824,"style":116},[99,20111,5022],{"x":3484,"y":3484,"fill":824,"style":121},[99,20113,20114],{"x":3484,"y":2535,"fill":103,"style":829},"Assets, images,",[99,20116,20117],{"x":3484,"y":6153,"fill":103,"style":829},"bundles",[99,20119,20120],{"x":3484,"y":138,"fill":93,"style":859},"→ LCP ceiling",[107,20122],{"x":20123,"y":849,"width":194,"height":1431,"rx":113,"fill":185,"opacity":186,"stroke":185,"style":116},"215",[99,20125,20126],{"x":158,"y":3484,"fill":187,"style":121},"Edge",[99,20128,20129],{"x":158,"y":2535,"fill":103,"style":829},"CDN cache &",[99,20131,5691],{"x":158,"y":6153,"fill":103,"style":829},[99,20133,20134],{"x":158,"y":138,"fill":93,"style":859},"→ TTFB",[107,20136],{"x":1415,"y":849,"width":194,"height":1431,"rx":113,"fill":114,"opacity":186,"stroke":114,"style":116},[99,20138,20140],{"x":20139,"y":3484,"fill":114,"style":121},"495","Hydrate",[99,20142,20143],{"x":20139,"y":2535,"fill":103,"style":829},"Islands, defer",[99,20145,20146],{"x":20139,"y":6153,"fill":103,"style":829},"third-party JS",[99,20148,20149],{"x":20139,"y":138,"fill":93,"style":859},"→ INP",[107,20151],{"x":4647,"y":849,"width":194,"height":1431,"rx":113,"fill":2564,"opacity":825,"stroke":2564,"style":116},[99,20153,20154],{"x":3558,"y":3484,"fill":2565,"style":121},"Measure",[99,20156,20157],{"x":3558,"y":2535,"fill":103,"style":829},"Lab + field",[99,20159,20160],{"x":3558,"y":6153,"fill":103,"style":829},"RUM budgets",[99,20162,20163],{"x":3558,"y":138,"fill":93,"style":859},"→ CLS & drift",[95,20165,88,20166,88,20170,88,20173,78],{"fill":93},[90,20167],{"d":20168,"stroke":93,"style":20169},"M190 130 L213 130","stroke-width:2px;marker-end:url(#cwv-arrow)",[90,20171],{"d":20172,"stroke":93,"style":20169},"M385 130 L408 130",[90,20174],{"d":20175,"stroke":93,"style":20169},"M580 130 L603 130",[99,20177,20178],{"x":101,"y":3578,"fill":103,"style":1416},"Each stage owns a different Core Web Vital",[99,20180,20181],{"x":101,"y":184,"fill":93,"style":859},"A regression in any one stage shows up in production field data, not the lab.",[76,20183,78,20184,66],{},[80,20185,88,20187,78],{"id":20186,"viewBox":83,"refX":84,"refY":85,"markerWidth":86,"markerHeight":86,"orient":87},"cwv-arrow",[90,20188],{"d":92,"fill":93},[218,20190,20191],{},"The build sets your LCP ceiling, the edge sets TTFB, hydration governs INP, and measurement catches CLS and slow drift.",[34,20193,5447],{"id":5446},[14,20195,20196],{},"This guide is organized around four pillars of static-site performance, each with its own deep-dive section:",[39,20198,20199,20210,20218,20226],{},[42,20200,20201,20204,20205,20207,20208,239],{},[229,20202,20203],{},"Build-time asset optimization"," — compression, responsive images, and chunk splitting that set your LCP ceiling. The full image pipeline lives in ",[23,20206,2190],{"href":2189},", and font handling in ",[23,20209,14061],{"href":14060},[42,20211,20212,20215,20216,239],{},[229,20213,20214],{},"Edge delivery and caching"," — the two-tier cache policy that makes repeat visits nearly free, detailed in ",[23,20217,13849],{"href":14803},[42,20219,20220,20223,20224,239],{},[229,20221,20222],{},"Hydration and interactivity"," — shipping zero JavaScript by default and hydrating only real islands, covered in ",[23,20225,10896],{"href":10895},[42,20227,20228,20231],{},[229,20229,20230],{},"Continuous measurement"," — gating deploys on a performance budget so regressions fail the build instead of shipping.",[34,20233,20235],{"id":20234},"choosing-an-ssg-for-production-performance","Choosing an SSG for Production Performance",[14,20237,20238],{},"Every major generator pre-renders HTML, but they differ in how much JavaScript they ship and how they handle assets. Astro ships zero JS by default and hydrates islands on demand, which gives it the best out-of-the-box INP story. Eleventy is template-only — there is no client runtime at all unless you add one — so its baseline is even leaner, at the cost of doing interactivity yourself. Hugo is the fastest builder and emits plain HTML\u002FCSS, ideal for large content sites. A Next.js static export gives you React's component model with pre-rendered output, but you pay for the framework runtime on every interactive page.",[14,20240,20241,20242,20244],{},"For Core Web Vitals specifically, the ranking on a content-heavy site is roughly: Eleventy and Hugo (no runtime) ≈ Astro (islands) \u003C Next export (full React hydration). The framework comparison in ",[23,20243,31],{"href":30}," weighs these trade-offs against authoring experience and ecosystem.",[34,20246,20248],{"id":20247},"build-time-asset-optimization-compression","Build-Time Asset Optimization & Compression",[14,20250,20251],{},"What you ship at build time sets your LCP ceiling. Compress HTML, CSS, and JavaScript — Brotli is well supported by every major CDN and beats gzip by 15-20% on text assets — and split bundles so a visitor to one page doesn't download the whole site's JavaScript.",[14,20253,20254,20255,738,20257,20259,20260,20262,20263,20266,20267,2114,20269,20271],{},"Generate responsive images at build time rather than resizing in the browser. Each framework has a native path: Hugo's image ",[253,20256,3699],{},[253,20258,3705],{}," methods, Eleventy's ",[253,20261,2125],{},", Jekyll's ",[253,20264,20265],{},"jekyll_picture_tag",", and Astro's built-in ",[253,20268,2113],{},[253,20270,2117],{}," (no separate plugin — image handling moved into Astro core in v3). All emit optimized formats like WebP\u002FAVIF at build time, which typically cut image bytes by 25-50% versus JPEG at equivalent quality.",[14,20273,20274],{},"For JavaScript, the highest-leverage build setting is chunk splitting — isolate dependencies into a long-cacheable vendor chunk so app changes don't bust the whole bundle:",[987,20276,20278],{"className":1600,"code":20277,"language":1602,"meta":712,"style":712},"\u002F\u002F astro.config.mjs\nimport { defineConfig } from 'astro\u002Fconfig';\n\nexport default defineConfig({\n  build: {\n    rollupOptions: {\n      output: {\n        \u002F\u002F Put everything from node_modules in a stable \"vendor\" chunk.\n        manualChunks(id) {\n          if (id.includes('node_modules')) return 'vendor';\n        },\n      },\n    },\n  },\n});\n",[253,20279,20280,20284,20289,20293,20298,20302,20307,20312,20317,20322,20327,20332,20337,20341,20345],{"__ignoreMap":712},[995,20281,20282],{"class":997,"line":998},[995,20283,1609],{},[995,20285,20286],{"class":997,"line":713},[995,20287,20288],{},"import { defineConfig } from 'astro\u002Fconfig';\n",[995,20290,20291],{"class":997,"line":730},[995,20292,1541],{"emptyLinePlaceholder":752},[995,20294,20295],{"class":997,"line":1544},[995,20296,20297],{},"export default defineConfig({\n",[995,20299,20300],{"class":997,"line":1550},[995,20301,5617],{},[995,20303,20304],{"class":997,"line":1673},[995,20305,20306],{},"    rollupOptions: {\n",[995,20308,20309],{"class":997,"line":1678},[995,20310,20311],{},"      output: {\n",[995,20313,20314],{"class":997,"line":1693},[995,20315,20316],{},"        \u002F\u002F Put everything from node_modules in a stable \"vendor\" chunk.\n",[995,20318,20319],{"class":997,"line":1705},[995,20320,20321],{},"        manualChunks(id) {\n",[995,20323,20324],{"class":997,"line":1711},[995,20325,20326],{},"          if (id.includes('node_modules')) return 'vendor';\n",[995,20328,20329],{"class":997,"line":1717},[995,20330,20331],{},"        },\n",[995,20333,20334],{"class":997,"line":1726},[995,20335,20336],{},"      },\n",[995,20338,20339],{"class":997,"line":1732},[995,20340,2964],{},[995,20342,20343],{"class":997,"line":2967},[995,20344,1729],{},[995,20346,20347],{"class":997,"line":2972},[995,20348,1735],{},[34,20350,20352],{"id":20351},"core-web-vitals-lcp-cls-and-inp-for-static-sites","Core Web Vitals: LCP, CLS and INP for Static Sites",[14,20354,20355],{},"The three metrics fail for different reasons, so they need different fixes:",[39,20357,20358,20367,20383],{},[42,20359,20360,20363,20364,20366],{},[229,20361,20362],{},"LCP"," is usually a hero image or a web font blocking the largest text block. Preload the LCP image, serve it as AVIF\u002FWebP, and give it ",[253,20365,19090],{},". On a typical marketing hero this moves LCP from ~3.2s to ~1.8s on a mid-tier mobile connection.",[42,20368,20369,20371,20372,738,20374,256,20376,20378,20379,20382],{},[229,20370,16586],{}," comes from images and ads without reserved dimensions, and from web fonts swapping in late. Always set ",[253,20373,9286],{},[253,20375,18059],{},[253,20377,18229],{},") on media, and use ",[253,20380,20381],{},"font-display: optional"," or a metric-matched fallback to avoid reflow.",[42,20384,20385,20387],{},[229,20386,10947],{}," is driven by main-thread JavaScript during interaction. The fix is structural: ship less JS, hydrate fewer components, and move heavy work off the main thread.",[34,20389,20391],{"id":20390},"delivery-architecture-edge-caching","Delivery Architecture & Edge Caching",[14,20393,20394,20395,20397],{},"Serving pre-built HTML well is mostly about cache headers. Distribute through a CDN's points of presence to cut Time to First Byte (TTFB), and align your purge strategy with the ",[23,20396,13849],{"href":14803}," so a deploy doesn't stampede your origin.",[14,20399,20400,20401,20403],{},"The rule that matters most: fingerprinted (hashed) assets never change, so cache them for a year as ",[253,20402,11756],{}," — the browser then skips revalidation entirely. HTML is the opposite and needs a short TTL. A typical host config for hashed assets:",[987,20405,20407],{"className":14263,"code":20406,"language":14265,"meta":712,"style":712},"{\n  \"headers\": [\n    {\n      \"source\": \"\u002Fstatic\u002F:path*\",\n      \"headers\": [\n        { \"key\": \"Cache-Control\", \"value\": \"public, max-age=31536000, immutable\" }\n      ]\n    }\n  ]\n}\n",[253,20408,20409,20413,20419,20423,20434,20440,20460,20464,20468,20472],{"__ignoreMap":712},[995,20410,20411],{"class":997,"line":998},[995,20412,14272],{"class":1618},[995,20414,20415,20417],{"class":997,"line":713},[995,20416,14277],{"class":1010},[995,20418,14280],{"class":1618},[995,20420,20421],{"class":997,"line":730},[995,20422,14285],{"class":1618},[995,20424,20425,20427,20429,20432],{"class":997,"line":1544},[995,20426,14290],{"class":1010},[995,20428,1925],{"class":1618},[995,20430,20431],{"class":1023},"\"\u002Fstatic\u002F:path*\"",[995,20433,2885],{"class":1618},[995,20435,20436,20438],{"class":997,"line":1550},[995,20437,14302],{"class":1010},[995,20439,14280],{"class":1618},[995,20441,20442,20444,20446,20448,20450,20452,20454,20456,20458],{"class":997,"line":1673},[995,20443,14309],{"class":1618},[995,20445,14312],{"class":1010},[995,20447,1925],{"class":1618},[995,20449,14317],{"class":1023},[995,20451,1850],{"class":1618},[995,20453,14322],{"class":1010},[995,20455,1925],{"class":1618},[995,20457,14327],{"class":1023},[995,20459,7475],{"class":1618},[995,20461,20462],{"class":997,"line":1678},[995,20463,14334],{"class":1618},[995,20465,20466],{"class":997,"line":1693},[995,20467,14395],{"class":1618},[995,20469,20470],{"class":997,"line":1705},[995,20471,14400],{"class":1618},[995,20473,20474],{"class":997,"line":1711},[995,20475,9008],{"class":1618},[34,20477,20479],{"id":20478},"image-font-and-asset-optimization-pipelines","Image, Font and Asset Optimization Pipelines",[14,20481,20482,20483,20485,20486,270,20488,20490],{},"Images and fonts are the two assets most likely to wreck LCP and CLS. The discipline is the same for both: process them at build time, serve modern formats, and reserve their space in the layout before they load. For images, that means a build step that emits multiple widths and formats and a ",[253,20484,8172],{}," element that lets the browser pick. For fonts, it means subsetting to the characters you use, self-hosting to avoid a third-party connection, and preloading the one weight above the fold. The dedicated guides — ",[23,20487,2190],{"href":2189},[23,20489,14061],{"href":14060}," — walk through both end to end with before\u002Fafter numbers.",[34,20492,20494],{"id":20493},"client-side-hydration-interaction-metrics","Client-Side Hydration & Interaction Metrics",[14,20496,20497,20498,20501,20502,20504],{},"JavaScript on the main thread is what drives INP up. The static-site advantage is that you can ship ",[229,20499,20500],{},"zero"," JS by default and hydrate only the components that are actually interactive — islands architecture. Astro's ",[253,20503,10850],{}," directives make the trade-off explicit: load eagerly, on visibility, or on idle.",[987,20506,20508],{"className":16196,"code":20507,"language":16198,"meta":712,"style":712},"\u003C!-- Hydrate only when needed, not on every page load -->\n\u003CSearchBox client:visible \u002F>\n\u003CCartWidget client:idle \u002F>\n",[253,20509,20510,20515,20529],{"__ignoreMap":712},[995,20511,20512],{"class":997,"line":998},[995,20513,20514],{"class":1001},"\u003C!-- Hydrate only when needed, not on every page load -->\n",[995,20516,20517,20519,20523,20526],{"class":997,"line":713},[995,20518,16205],{"class":1618},[995,20520,20522],{"class":20521},"s7hpK","SearchBox",[995,20524,20525],{"class":1007}," client:visible",[995,20527,20528],{"class":1618}," \u002F>\n",[995,20530,20531,20533,20536,20539],{"class":997,"line":730},[995,20532,16205],{"class":1618},[995,20534,20535],{"class":20521},"CartWidget",[995,20537,20538],{"class":1007}," client:idle",[995,20540,20528],{"class":1618},[14,20542,8896,20543,20545],{},[23,20544,10896],{"href":10895}," approach covers when each directive is appropriate. Audit third-party scripts just as hard — analytics and chat widgets routinely wreck INP on otherwise-fast static pages, because they run on the same main thread your interactions need.",[34,20547,20549],{"id":20548},"cicd-performance-budgets-and-continuous-measurement","CI\u002FCD, Performance Budgets and Continuous Measurement",[14,20551,20552],{},"Lab scores (Lighthouse) and field data (Real User Monitoring) measure different things; track both. Lab catches regressions before deploy; RUM tells you what users actually experience across devices and networks.",[14,20554,20555],{},"Gate deploys on a Lighthouse budget so a regression fails the build instead of shipping:",[987,20557,20559],{"className":989,"code":20558,"language":991,"meta":712,"style":712},"lhci autorun \\\n  --collect.url=https:\u002F\u002Fdeploy-preview.example.com\u002F \\\n  --assert.preset=lighthouse:recommended\n",[253,20560,20561,20569,20576],{"__ignoreMap":712},[995,20562,20563,20565,20567],{"class":997,"line":998},[995,20564,16647],{"class":1007},[995,20566,16650],{"class":1023},[995,20568,3002],{"class":1010},[995,20570,20571,20574],{"class":997,"line":713},[995,20572,20573],{"class":1010},"  --collect.url=https:\u002F\u002Fdeploy-preview.example.com\u002F",[995,20575,3002],{"class":1010},[995,20577,20578],{"class":997,"line":730},[995,20579,19063],{"class":1010},[14,20581,20582,20583,239],{},"When a field metric drops, line it up against your deploy timeline — a sudden INP regression usually maps to a specific release or a new third-party tag, not gradual drift. Wiring this into the deploy flow is covered in ",[23,20584,5505],{"href":5504},[34,20586,2266],{"id":2265},[39,20588,20589,20595,20604,20610],{},[42,20590,20591,20594],{},[229,20592,20593],{},"Over-hydrating static content:"," attaching a client framework to purely presentational markup adds bundle weight and delays First Contentful Paint with no benefit.",[42,20596,20597,20600,20601,20603],{},[229,20598,20599],{},"Long TTLs on HTML:"," caching HTML aggressively serves stale content and breaks rollbacks. Keep HTML short-lived; reserve ",[253,20602,11756],{}," for hashed assets.",[42,20605,20606,20609],{},[229,20607,20608],{},"Ignoring third-party scripts:"," analytics, chat, and ad tags execute on the main thread and degrade INP and LCP regardless of how optimized your own output is.",[42,20611,20612,20615],{},[229,20613,20614],{},"Unsized media:"," images, embeds, and ads without explicit dimensions are the most common CLS source on otherwise-static pages.",[34,20617,2321],{"id":2320},[39,20619,20620,20623,20626,20629,20632],{},[42,20621,20622],{},"SSGs hand you good LCP and CLS for free; the real work is protecting INP and TTFB.",[42,20624,20625],{},"Optimize assets at build time — that step sets the ceiling everything else operates under.",[42,20627,20628],{},"Use the two-tier cache policy: immutable hashed assets, short-lived HTML.",[42,20630,20631],{},"Hydrate only genuine interactive islands, and budget third-party scripts as strictly as your own.",[42,20633,20634],{},"Gate every deploy on a performance budget so regressions never reach production silently.",[34,20636,651],{"id":650},[653,20638,20640],{"id":20639},"how-do-core-web-vitals-differ-for-ssgs-compared-to-spas","How do Core Web Vitals differ for SSGs compared to SPAs?",[14,20642,20643],{},"SSGs serve pre-rendered HTML, so LCP and CLS are strong by default. The remaining work is INP — you still have to control hydration so interactivity matches what a single-page app provides without shipping its full JavaScript cost.",[653,20645,20647],{"id":20646},"can-static-sites-achieve-top-lighthouse-scores-in-production","Can static sites achieve top Lighthouse scores in production?",[14,20649,20650],{},"Yes. With disciplined asset optimization, deferred third-party scripts, and edge caching, static sites routinely score 95-100 in the lab. Just remember lab scores can diverge from real-user data, so monitor both Lighthouse and field RUM.",[653,20652,20654],{"id":20653},"what-is-the-optimal-caching-strategy-for-ssg-html-files","What is the optimal caching strategy for SSG HTML files?",[14,20656,20657,20658,20660,20661,20663,20664,20666],{},"Use a short ",[253,20659,14636],{}," (0-300s) with ",[253,20662,14131],{}," for HTML, paired with one-year ",[253,20665,11756],{}," caching for fingerprinted CSS, JS, and images. This two-tier policy keeps content fresh while making repeat visits nearly free.",[653,20668,20670],{"id":20669},"how-does-partial-hydration-impact-seo-and-performance","How does partial hydration impact SEO and performance?",[14,20672,20673],{},"Search engines receive full crawlable HTML while JavaScript execution is deferred, which improves First Contentful Paint and INP and lowers the main-thread cost. Content is indexable regardless of whether an island has hydrated.",[653,20675,20677],{"id":20676},"which-core-web-vital-is-hardest-to-fix-on-a-static-site","Which Core Web Vital is hardest to fix on a static site?",[14,20679,20680],{},"INP. LCP and CLS are largely solved by pre-rendering and reserved space, but INP depends on how much JavaScript runs on the main thread during interaction — which is entirely under your control through hydration discipline and third-party script budgets.",[34,20682,684],{"id":683},[39,20684,20685,20693,20698,20703,20708],{},[42,20686,20687,692,20689,20692],{},[229,20688,691],{},[23,20690,20691],{"href":738},"Static Site Generators in Production"," — the home guide tying performance, framework choice, and deployment together.",[42,20694,20695,20697],{},[23,20696,2190],{"href":2189}," — build-time responsive images.",[42,20699,20700,20702],{},[23,20701,14061],{"href":14060}," — eliminate font-driven CLS.",[42,20704,20705,20707],{},[23,20706,13849],{"href":14803}," — the two-tier cache policy in depth.",[42,20709,20710,20712],{},[23,20711,10896],{"href":10895}," — control INP with islands.",[1346,20714,20715],{},"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 .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}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 pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .s7hpK, html code.shiki .s7hpK{--shiki-default:#B31D28;--shiki-default-font-style:italic;--shiki-dark:#FDAEB7;--shiki-dark-font-style:italic}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}",{"title":712,"searchDepth":713,"depth":713,"links":20717},[20718,20719,20720,20721,20722,20723,20724,20725,20726,20727,20728,20735],{"id":5446,"depth":713,"text":5447},{"id":20234,"depth":713,"text":20235},{"id":20247,"depth":713,"text":20248},{"id":20351,"depth":713,"text":20352},{"id":20390,"depth":713,"text":20391},{"id":20478,"depth":713,"text":20479},{"id":20493,"depth":713,"text":20494},{"id":20548,"depth":713,"text":20549},{"id":2265,"depth":713,"text":2266},{"id":2320,"depth":713,"text":2321},{"id":650,"depth":713,"text":651,"children":20729},[20730,20731,20732,20733,20734],{"id":20639,"depth":730,"text":20640},{"id":20646,"depth":730,"text":20647},{"id":20653,"depth":730,"text":20654},{"id":20669,"depth":730,"text":20670},{"id":20676,"depth":730,"text":20677},{"id":683,"depth":713,"text":684},[20737,20738],{"name":737,"item":738},{"name":5501,"item":5500},"Improve Core Web Vitals on static sites — LCP, CLS, INP — through disciplined asset processing, edge caching, and keeping JavaScript off pages that do not need it.",[20741,20742,20743,20745,20746],{"q":20640,"a":20643},{"q":20647,"a":20650},{"q":20654,"a":20744},"Use a short max-age (0-300s) with stale-while-revalidate for HTML, paired with one-year immutable caching for fingerprinted CSS, JS, and images. This two-tier policy keeps content fresh while making repeat visits nearly free.",{"q":20670,"a":20673},{"q":20677,"a":20680},{},"\u002Fperformance-optimization-core-web-vitals-for-ssgs",{"title":20077,"description":20739},"performance-optimization-core-web-vitals-for-ssgs\u002Findex","w2yfiQAcWZ0-Wa6C0NBrbNaf49wOuNue5_yFxMxhC6U",{"id":20753,"title":20754,"body":20755,"breadcrumb":21407,"dateModified":743,"datePublished":2446,"description":21413,"extension":745,"faq":21414,"meta":21420,"navigation":752,"path":21421,"seo":21422,"slug":20759,"stem":21423,"type":756,"__hash__":21424},"content\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fjavascript-hydration-partial-rendering\u002Fastro-islands-vs-full-hydration-performance\u002Findex.md","Astro Islands vs Full Hydration Performance",{"type":7,"value":20756,"toc":21390},[20757,20760,20768,20772,20786,20789,20867,20869,20891,20895,20898,20938,20944,20948,20958,20987,21002,21005,21085,21087,21090,21170,21176,21180,21191,21261,21272,21274,21318,21320,21326,21328,21332,21335,21339,21345,21349,21352,21356,21359,21361,21387],[10,20758,20754],{"id":20759},"astro-islands-vs-full-hydration-performance",[14,20761,20762,20763,20765,20766,239],{},"The difference between Astro's islands and a fully-hydrated single-page app is simple but decisive: full hydration boots the entire framework runtime on the client for every page, while islands ship zero JavaScript by default and hydrate only the components you mark interactive. On content-heavy pages — which is most of the web — that gap shows up directly in JavaScript bytes, Total Blocking Time, and Interaction to Next Paint (INP). This is the measured deep dive behind ",[23,20764,10896],{"href":10895},", part of ",[23,20767,5501],{"href":5500},[34,20769,20771],{"id":20770},"the-two-models","The Two Models",[39,20773,20774,20780],{},[42,20775,20776,20779],{},[229,20777,20778],{},"Full hydration:"," the client downloads the framework runtime, re-runs it over the whole page, and attaches event listeners everywhere. Interactivity is immediate everywhere, but you pay the runtime cost on every visit regardless of how little of the page is actually interactive. A typical React app shell is 130–180 KB of JavaScript before any of your own code.",[42,20781,20782,20785],{},[229,20783,20784],{},"Islands:"," static HTML ships with no JavaScript. Each interactive component is its own small bundle that hydrates on its own trigger. The static 90 percent of a page costs nothing on the main thread.",[14,20787,20788],{},"For a content page that is mostly prose with a search box and a chart, full hydration spends its largest cost on markup that will never be touched. Islands spend it only where a user can interact.",[55,20790,20791,20864],{},[58,20792,66,20797,66,20800,66,20803,66,20805,66,20857],{"viewBox":20793,"role":61,"ariaLabelledBy":20794,"xmlns":65},"0 0 800 340",[20795,20796],"aivf-title","aivf-desc",[68,20798,20799],{"id":20795},"Main-thread cost: full hydration versus islands",[72,20801,20802],{"id":20796},"Two timelines compare a fully hydrated page, where a long block of framework execution delays interactivity, against an islands page, where only two short island bundles execute and the rest of the main thread stays free.",[107,20804],{"x":2515,"y":2515,"width":8298,"height":6144,"fill":205},[95,20806,78,20807,78,20810,78,20813,78,20815,78,20819,78,20823,78,20826,78,20828,78,20832,78,20834,78,20838,78,20840,78,20843,78,20846,78,20849,78,20852,78,20854,66],{"style":813},[99,20808,20809],{"x":101,"y":2521,"fill":103,"style":1416},"Main-thread time after HTML arrives",[99,20811,20812],{"x":3578,"y":17814,"fill":2565,"style":2597},"Full hydration",[107,20814],{"x":3578,"y":6849,"width":10820,"height":5434,"rx":84,"fill":2564,"opacity":877,"stroke":2565,"style":116},[99,20816,20818],{"x":1463,"y":20817,"fill":103,"style":859},"121","runtime boot + hydrate entire page — 186 KB JS",[99,20820,20822],{"x":19446,"y":20817,"fill":2565,"style":20821},"font-size:13px;font-weight:700","TBT 410 ms",[99,20824,20825],{"x":3578,"y":4657,"fill":187,"style":2597},"Islands",[107,20827],{"x":3578,"y":12813,"width":159,"height":5434,"rx":84,"fill":824,"opacity":886,"stroke":824,"style":116},[99,20829,20831],{"x":15952,"y":20830,"fill":103,"style":126},"221","search 14 KB",[107,20833],{"x":194,"y":12813,"width":2563,"height":5434,"rx":84,"fill":114,"opacity":850,"stroke":114,"style":116},[99,20835,20837],{"x":20836,"y":20830,"fill":103,"style":126},"235","chart 22 KB",[107,20839],{"x":1463,"y":12813,"width":820,"height":5434,"rx":84,"fill":185,"opacity":186,"stroke":187,"style":116},[99,20841,20842],{"x":11991,"y":20830,"fill":187,"style":126},"main thread free — static HTML",[99,20844,20845],{"x":19446,"y":20830,"fill":187,"style":20821},"TBT 90 ms",[997,20847],{"x1":3578,"y1":5332,"x2":9750,"y2":5332,"stroke":93,"style":20848},"stroke-width:1.5px;marker-end:url(#aivf-arrow)",[99,20850,20851],{"x":3578,"y":6878,"fill":93,"style":2624},"page HTML painted",[99,20853,17036],{"x":9750,"y":6878,"fill":93,"style":12787},[99,20855,20856],{"x":101,"y":1463,"fill":93,"style":859},"Less main-thread work during early interaction means a lower INP at the 75th percentile.",[76,20858,78,20859,66],{},[80,20860,88,20862,78],{"id":20861,"viewBox":83,"refX":84,"refY":85,"markerWidth":86,"markerHeight":86,"orient":87},"aivf-arrow",[90,20863],{"d":92,"fill":93},[218,20865,20866],{},"Full hydration occupies the main thread with one long framework boot; islands run two short bundles and leave the rest of the thread free, which is what keeps INP low.",[34,20868,37],{"id":36},[39,20870,20871,20874,20884],{},[42,20872,20873],{},"An Astro project (v3 or later) with at least one interactive component you can render two ways.",[42,20875,20876,20877,1850,20880,20883],{},"A UI framework integration installed (",[253,20878,20879],{},"@astrojs\u002Freact",[253,20881,20882],{},"@astrojs\u002Fvue",", or similar) so you can build a full-hydration baseline to compare against.",[42,20885,20886,20887,20890],{},"Lighthouse available locally (",[253,20888,20889],{},"npx lighthouse",") and, ideally, field INP from Real User Monitoring on a deployed copy.",[34,20892,20894],{"id":20893},"building-both-versions-to-compare","Building Both Versions to Compare",[14,20896,20897],{},"Build the same page two ways and run Lighthouse against each so the numbers are apples to apples:",[987,20899,20901],{"className":989,"code":20900,"language":991,"meta":712,"style":712},"npx astro build\nnpx lighthouse \u003Curl> --only-categories=performance --output=json --output-path=.\u002Frun.json\n",[253,20902,20903,20912],{"__ignoreMap":712},[995,20904,20905,20907,20909],{"class":997,"line":998},[995,20906,1079],{"class":1007},[995,20908,3046],{"class":1023},[995,20910,20911],{"class":1023}," build\n",[995,20913,20914,20916,20919,20921,20924,20927,20930,20933,20935],{"class":997,"line":713},[995,20915,1079],{"class":1007},[995,20917,20918],{"class":1023}," lighthouse",[995,20920,9059],{"class":1614},[995,20922,20923],{"class":1023},"ur",[995,20925,20926],{"class":1618},"l",[995,20928,20929],{"class":1614},">",[995,20931,20932],{"class":1010}," --only-categories=performance",[995,20934,16043],{"class":1010},[995,20936,20937],{"class":1010}," --output-path=.\u002Frun.json\n",[14,20939,20940,20941,20943],{},"For the full-hydration baseline, mark every interactive component ",[253,20942,2211],{}," (and, in a framework like Next.js, render the page as a client component). For the islands version, mark only the genuinely interactive components and choose the lightest directive each can tolerate.",[34,20945,20947],{"id":20946},"choosing-a-directive","Choosing a Directive",[14,20949,20950,20951,20953,20954,20957],{},"Astro's ",[253,20952,10850],{}," directives are the boundary controls, and the directive decides ",[18,20955,20956],{},"when"," an island's JavaScript runs:",[39,20959,20960,20965,20970,20975,20981],{},[42,20961,20962,20964],{},[253,20963,2211],{}," — hydrate immediately. Above-the-fold interactivity only.",[42,20966,20967,20969],{},[253,20968,2203],{}," — hydrate when it scrolls into view. The default choice for most widgets.",[42,20971,20972,20974],{},[253,20973,2207],{}," — hydrate in an idle period. Good for non-urgent controls.",[42,20976,20977,20980],{},[253,20978,20979],{},"client:media"," — hydrate when a CSS media query matches, e.g. desktop-only UI.",[42,20982,20983,20986],{},[253,20984,20985],{},"client:only=\"react\""," — skip server rendering entirely for browser-only components (the framework name is required).",[987,20988,20990],{"className":10854,"code":20989,"language":10856,"meta":712,"style":712},"\u003CInteractiveChart client:visible data={chartData} \u002F>\n\u003CStaticTable data={rows} \u002F>   \u003C!-- no directive: pure HTML, zero JS -->\n",[253,20991,20992,20997],{"__ignoreMap":712},[995,20993,20994],{"class":997,"line":998},[995,20995,20996],{},"\u003CInteractiveChart client:visible data={chartData} \u002F>\n",[995,20998,20999],{"class":997,"line":713},[995,21000,21001],{},"\u003CStaticTable data={rows} \u002F>   \u003C!-- no directive: pure HTML, zero JS -->\n",[14,21003,21004],{},"If you load more than one framework across islands, dedupe shared dependencies so each runtime ships once:",[987,21006,21008],{"className":1600,"code":21007,"language":1602,"meta":712,"style":712},"\u002F\u002F astro.config.mjs\nimport { defineConfig } from 'astro\u002Fconfig';\n\nexport default defineConfig({\n  vite: {\n    build: {\n      rollupOptions: {\n        output: { manualChunks: { vendor: ['react', 'vue'] } },\n      },\n    },\n  },\n});\n",[253,21009,21010,21014,21026,21030,21040,21044,21048,21053,21069,21073,21077,21081],{"__ignoreMap":712},[995,21011,21012],{"class":997,"line":998},[995,21013,1609],{"class":1001},[995,21015,21016,21018,21020,21022,21024],{"class":997,"line":713},[995,21017,1615],{"class":1614},[995,21019,1619],{"class":1618},[995,21021,1622],{"class":1614},[995,21023,1625],{"class":1023},[995,21025,1628],{"class":1618},[995,21027,21028],{"class":997,"line":730},[995,21029,1541],{"emptyLinePlaceholder":752},[995,21031,21032,21034,21036,21038],{"class":997,"line":1544},[995,21033,1681],{"class":1614},[995,21035,1684],{"class":1614},[995,21037,1687],{"class":1007},[995,21039,1690],{"class":1618},[995,21041,21042],{"class":997,"line":1550},[995,21043,2916],{"class":1618},[995,21045,21046],{"class":997,"line":1673},[995,21047,2921],{"class":1618},[995,21049,21050],{"class":997,"line":1678},[995,21051,21052],{"class":1618},"      rollupOptions: {\n",[995,21054,21055,21058,21061,21063,21066],{"class":997,"line":1693},[995,21056,21057],{"class":1618},"        output: { manualChunks: { vendor: [",[995,21059,21060],{"class":1023},"'react'",[995,21062,1850],{"class":1618},[995,21064,21065],{"class":1023},"'vue'",[995,21067,21068],{"class":1618},"] } },\n",[995,21070,21071],{"class":997,"line":1705},[995,21072,20336],{"class":1618},[995,21074,21075],{"class":997,"line":1711},[995,21076,2964],{"class":1618},[995,21078,21079],{"class":997,"line":1717},[995,21080,1729],{"class":1618},[995,21082,21083],{"class":997,"line":1726},[995,21084,1735],{"class":1618},[34,21086,1166],{"id":1165},[14,21088,21089],{},"The same content page — prose, a search box, and a below-the-fold chart — built three ways and measured on a throttled mid-tier mobile profile (Lighthouse for lab metrics, field INP at the 75th percentile from Real User Monitoring):",[433,21091,21092,21109],{},[436,21093,21094],{},[439,21095,21096,21098,21101,21103,21106],{},[442,21097,2135],{},[442,21099,21100],{},"JS shipped",[442,21102,10958],{},[442,21104,21105],{},"INP (field p75)",[442,21107,21108],{},"Lighthouse perf",[457,21110,21111,21130,21147],{},[439,21112,21113,21119,21122,21125,21128],{},[462,21114,21115,21116,21118],{},"Full hydration (",[253,21117,2211],{}," everywhere)",[462,21120,21121],{},"186 KB",[462,21123,21124],{},"410 ms",[462,21126,21127],{},"290 ms",[462,21129,4629],{},[439,21131,21132,21137,21140,21143,21145],{},[462,21133,21134,21135],{},"Islands, all ",[253,21136,2211],{},[462,21138,21139],{},"92 KB",[462,21141,21142],{},"240 ms",[462,21144,10950],{},[462,21146,17814],{},[439,21148,21149,21161,21163,21165,21168],{},[462,21150,21151,21152,738,21155,738,21158],{},"Islands, mixed ",[253,21153,21154],{},"load",[253,21156,21157],{},"idle",[253,21159,21160],{},"visible",[462,21162,21139],{},[462,21164,10953],{},[462,21166,21167],{},"140 ms",[462,21169,833],{},[14,21171,21172,21173,21175],{},"Two separate wins are visible. Moving from full hydration to islands roughly halves the JavaScript, because the static markup stops shipping a runtime. Then, with the same 92 KB of island code, deferring the non-urgent islands off ",[253,21174,2211],{}," cuts Total Blocking Time from 240 ms to 90 ms and pulls field INP from 210 ms (needs-improvement) under the 200 ms \"good\" threshold to 140 ms. The directive choice matters as much as the architecture choice.",[34,21177,21179],{"id":21178},"analyzing-the-bundle","Analyzing the Bundle",[14,21181,21182,21183,21186,21187,21190],{},"Astro has no ",[253,21184,21185],{},"ASTRO_ANALYZE"," flag; visualize chunks by adding ",[253,21188,21189],{},"rollup-plugin-visualizer"," to the Vite config, which writes a treemap on build:",[987,21192,21194],{"className":1600,"code":21193,"language":1602,"meta":712,"style":712},"\u002F\u002F astro.config.mjs\nimport { defineConfig } from 'astro\u002Fconfig';\nimport { visualizer } from 'rollup-plugin-visualizer';\n\nexport default defineConfig({\n  vite: { plugins: [visualizer({ filename: 'stats.html' })] },\n});\n",[253,21195,21196,21200,21212,21226,21230,21240,21257],{"__ignoreMap":712},[995,21197,21198],{"class":997,"line":998},[995,21199,1609],{"class":1001},[995,21201,21202,21204,21206,21208,21210],{"class":997,"line":713},[995,21203,1615],{"class":1614},[995,21205,1619],{"class":1618},[995,21207,1622],{"class":1614},[995,21209,1625],{"class":1023},[995,21211,1628],{"class":1618},[995,21213,21214,21216,21219,21221,21224],{"class":997,"line":730},[995,21215,1615],{"class":1614},[995,21217,21218],{"class":1618}," { visualizer } ",[995,21220,1622],{"class":1614},[995,21222,21223],{"class":1023}," 'rollup-plugin-visualizer'",[995,21225,1628],{"class":1618},[995,21227,21228],{"class":997,"line":1544},[995,21229,1541],{"emptyLinePlaceholder":752},[995,21231,21232,21234,21236,21238],{"class":997,"line":1550},[995,21233,1681],{"class":1614},[995,21235,1684],{"class":1614},[995,21237,1687],{"class":1007},[995,21239,1690],{"class":1618},[995,21241,21242,21245,21248,21251,21254],{"class":997,"line":1673},[995,21243,21244],{"class":1618},"  vite: { plugins: [",[995,21246,21247],{"class":1007},"visualizer",[995,21249,21250],{"class":1618},"({ filename: ",[995,21252,21253],{"class":1023},"'stats.html'",[995,21255,21256],{"class":1618}," })] },\n",[995,21258,21259],{"class":997,"line":1678},[995,21260,1735],{"class":1618},[14,21262,21263,21264,21267,21268,21271],{},"Open ",[253,21265,21266],{},"stats.html"," to confirm each island is small and no runtime is duplicated, then preview locally (",[253,21269,21270],{},"npx astro preview",") to catch hydration warnings before deploy.",[34,21273,600],{"id":599},[39,21275,21276,21284,21290,21308],{},[42,21277,21278,21283],{},[229,21279,21280,21282],{},[253,21281,2211],{}," on static UI:"," forces hydration of non-interactive markup, raising Total Blocking Time and INP for no benefit. Drop the directive.",[42,21285,21286,21289],{},[229,21287,21288],{},"Hydration mismatch from dynamic server state:"," server-rendered timestamps or random IDs differ from the client render and trigger a mismatch and re-render. Keep server output deterministic.",[42,21291,21292,21298,21299,738,21302,21305,21306,239],{},[229,21293,16685,21294,21297],{},[253,21295,21296],{},"client:only"," for browser-only APIs:"," a component that touches ",[253,21300,21301],{},"window",[253,21303,21304],{},"document"," during server rendering produces empty or broken markup. Mark it ",[253,21307,21296],{},[42,21309,21310,21312,21313,18249,21315,21317],{},[229,21311,637],{}," the directive lives in the component markup, so reverting a too-aggressive ",[253,21314,2211],{},[253,21316,2203],{}," is a one-line change and a redeploy — there is no runtime state to migrate.",[34,21319,642],{"id":641},[14,21321,21322,21323,21325],{},"Islands win whenever most of a page is static — which is most content pages — because you stop paying for a framework runtime you don't use. The measured path is clear: full hydration to islands roughly halves the JavaScript, and then directive discipline (defaulting to no directive, reaching for ",[253,21324,2203],{},") takes Total Blocking Time and field INP down again. Default to no directive, dedupe shared frameworks, and verify with a bundle visualizer and field INP rather than trusting the lab number alone.",[34,21327,651],{"id":650},[653,21329,21331],{"id":21330},"does-islands-architecture-eliminate-all-javascript","Does islands architecture eliminate all JavaScript?",[14,21333,21334],{},"No. Islands ship zero JavaScript for the static parts of a page, but each component you mark interactive still ships the targeted bundle it needs to hydrate. The saving comes from not loading and executing a framework runtime across the parts of the page that never needed it.",[653,21336,21338],{"id":21337},"how-do-i-measure-the-hydration-impact-myself","How do I measure the hydration impact myself?",[14,21340,21341,21342,21344],{},"Run a Lighthouse build against each version and read Total Blocking Time, then add ",[253,21343,21189],{}," to confirm each island is a small independent chunk. Total Blocking Time is the lab proxy; cross-reference it with field INP from Real User Monitoring to see what users actually experience.",[653,21346,21348],{"id":21347},"can-i-mix-frameworks-in-one-astro-project","Can I mix frameworks in one Astro project?",[14,21350,21351],{},"Yes. React, Vue, Svelte, and Preact can coexist, but each adds its own runtime, so isolate them to the islands that truly need them and dedupe shared dependencies so a runtime ships only once.",[653,21353,21355],{"id":21354},"when-is-full-hydration-actually-the-right-choice","When is full hydration actually the right choice?",[14,21357,21358],{},"When most of the page is interactive — a dashboard, an editor, a heavily stateful app shell. If nearly every element responds to input, islands stop saving much, and a single coherent runtime can be simpler. For content-heavy pages, which are mostly static, islands win clearly.",[34,21360,684],{"id":683},[39,21362,21363,21370,21377,21382],{},[42,21364,21365,692,21367,21369],{},[229,21366,691],{},[23,21368,10896],{"href":10895}," — boundaries, directives, and CI budgets.",[42,21371,21372,21376],{},[23,21373,21375],{"href":21374},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fjavascript-hydration-partial-rendering\u002Fhow-to-reduce-bundle-size-in-eleventy-builds\u002F","How to Reduce Bundle Size in Eleventy Builds"," — trimming JavaScript on a template-only generator.",[42,21378,21379,21381],{},[23,21380,10981],{"href":10980}," — turning the field INP numbers above into a live signal.",[42,21383,21384,21386],{},[23,21385,5501],{"href":5500}," — where hydration fits the INP picture.",[1346,21388,21389],{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}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 .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 .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}",{"title":712,"searchDepth":713,"depth":713,"links":21391},[21392,21393,21394,21395,21396,21397,21398,21399,21400,21406],{"id":20770,"depth":713,"text":20771},{"id":36,"depth":713,"text":37},{"id":20893,"depth":713,"text":20894},{"id":20946,"depth":713,"text":20947},{"id":1165,"depth":713,"text":1166},{"id":21178,"depth":713,"text":21179},{"id":599,"depth":713,"text":600},{"id":641,"depth":713,"text":642},{"id":650,"depth":713,"text":651,"children":21401},[21402,21403,21404,21405],{"id":21330,"depth":730,"text":21331},{"id":21337,"depth":730,"text":21338},{"id":21347,"depth":730,"text":21348},{"id":21354,"depth":730,"text":21355},{"id":683,"depth":713,"text":684},[21408,21409,21410,21411],{"name":737,"item":738},{"name":5501,"item":5500},{"name":10896,"item":10895},{"name":20754,"item":21412},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fjavascript-hydration-partial-rendering\u002Fastro-islands-vs-full-hydration-performance\u002F","Measured comparison of Astro islands against full SPA hydration — JavaScript bytes, Total Blocking Time, and INP numbers, plus which directive to reach for.",[21415,21416,21418,21419],{"q":21331,"a":21334},{"q":21338,"a":21417},"Run a Lighthouse build against each version and read Total Blocking Time, then add rollup-plugin-visualizer to confirm each island is a small independent chunk. Total Blocking Time is the lab proxy; cross-reference it with field INP from Real User Monitoring to see what users actually experience.",{"q":21348,"a":21351},{"q":21355,"a":21358},{},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fjavascript-hydration-partial-rendering\u002Fastro-islands-vs-full-hydration-performance",{"title":20754,"description":21413},"performance-optimization-core-web-vitals-for-ssgs\u002Fjavascript-hydration-partial-rendering\u002Fastro-islands-vs-full-hydration-performance\u002Findex","Dna7TDDbtCC9Sfje5h4ER7P8q3Te7T98KdGavRLSgwo",{"id":21426,"title":21375,"body":21427,"breadcrumb":22189,"dateModified":743,"datePublished":2446,"description":22194,"extension":745,"faq":22195,"meta":22202,"navigation":752,"path":22203,"seo":22204,"slug":21431,"stem":22205,"type":756,"__hash__":22206},"content\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fjavascript-hydration-partial-rendering\u002Fhow-to-reduce-bundle-size-in-eleventy-builds\u002Findex.md",{"type":7,"value":21428,"toc":22172},[21429,21432,21444,21446,21467,21569,21573,21576,21631,21638,21642,21655,21837,21843,21847,21850,21897,21914,21918,21925,21948,21958,21960,21968,22039,22042,22044,22091,22093,22102,22104,22108,22111,22115,22125,22129,22136,22140,22143,22145,22169],[10,21430,21375],{"id":21431},"how-to-reduce-bundle-size-in-eleventy-builds",[14,21433,21434,21435,21437,21438,21440,21441,21443],{},"Eleventy doesn't bundle JavaScript for you — it's a templating tool — so bloat usually comes from scripts dropped into a base layout that every page then downloads, parses, and executes. The fixes are three: scope scripts to the pages that actually need them, run them through a real bundler that tree-shakes (esbuild), and defer or lazy-load third-party code. This applies the ",[23,21436,10896],{"href":10895}," patterns from ",[23,21439,5501],{"href":5500}," to Eleventy specifically, where there is no ",[253,21442,10850],{}," directive but the same opt-in principle applies.",[34,21445,37],{"id":36},[39,21447,21448,21451,21461],{},[42,21449,21450],{},"An Eleventy site (3.x recommended — the bundle plugin ships with core) that currently loads one or more scripts from a shared layout.",[42,21452,21453,21456,21457,21460],{},[253,21454,21455],{},"esbuild"," available (",[253,21458,21459],{},"npm i -D esbuild",") for compiling and tree-shaking real application JavaScript.",[42,21462,21463,21466],{},[253,21464,21465],{},"du"," and Lighthouse locally so you can measure the output directory and Total Blocking Time before and after.",[55,21468,21469,21566],{},[58,21470,66,21475,66,21478,66,21481,66,21483,66,21559],{"viewBox":21471,"role":61,"ariaLabelledBy":21472,"xmlns":65},"0 0 800 300",[21473,21474],"eb-flow-title","eb-flow-desc",[68,21476,21477],{"id":21473},"Trimming an Eleventy JavaScript bundle",[72,21479,21480],{"id":21474},"A 78 kilobyte source of scripts flows through three trimming stages — scoping per route, tree-shaking and minifying with esbuild, and deferring third-party code — producing an 11 kilobyte shipped bundle.",[107,21482],{"x":2515,"y":2515,"width":8298,"height":158,"fill":205},[95,21484,78,21485,78,21488,78,21490,78,21493,78,21496,78,21498,78,21501,78,21504,78,21507,78,21510,78,21512,78,21515,78,21518,78,21521,78,21524,78,21527,78,21530,78,21533,78,21537,78,21540,78,21555,66],{"style":97},[99,21486,21487],{"x":101,"y":109,"fill":103,"style":104},"From a 78 KB global script to an 11 KB per-route payload",[107,21489],{"x":5393,"y":159,"width":1431,"height":1430,"rx":823,"fill":2564,"opacity":186,"stroke":2565,"style":116},[99,21491,21492],{"x":1430,"y":5379,"fill":2565,"style":121},"Source JS",[99,21494,21495],{"x":1430,"y":171,"fill":93,"style":126},"78 KB global",[107,21497],{"x":853,"y":4682,"width":2563,"height":4682,"rx":823,"fill":824,"opacity":825,"stroke":824,"style":116},[99,21499,21500],{"x":112,"y":2535,"fill":824,"style":121},"Scope",[99,21502,21503],{"x":112,"y":19428,"fill":93,"style":126},"bundle plugin",[99,21505,21506],{"x":112,"y":10805,"fill":93,"style":126},"per route",[107,21508],{"x":21509,"y":4682,"width":2563,"height":4682,"rx":823,"fill":114,"opacity":186,"stroke":114,"style":116},"355",[99,21511,21455],{"x":5338,"y":2535,"fill":114,"style":121},[99,21513,21514],{"x":5338,"y":19428,"fill":93,"style":126},"tree-shake +",[99,21516,21517],{"x":5338,"y":10805,"fill":93,"style":126},"minify",[107,21519],{"x":21520,"y":4682,"width":2563,"height":4682,"rx":823,"fill":162,"opacity":877,"stroke":164,"style":116},"525",[99,21522,21523],{"x":7842,"y":2535,"fill":164,"style":121},"Defer",[99,21525,21526],{"x":7842,"y":19428,"fill":93,"style":126},"third-party",[99,21528,21529],{"x":7842,"y":10805,"fill":93,"style":126},"lazy-load",[107,21531],{"x":21532,"y":159,"width":18402,"height":1430,"rx":823,"fill":185,"opacity":850,"stroke":187,"style":116},"695",[99,21534,21536],{"x":21535,"y":5379,"fill":187,"style":121},"737","Shipped",[99,21538,21539],{"x":21535,"y":171,"fill":93,"style":126},"11 KB",[95,21541,88,21542,88,21546,88,21549,88,21552,78],{"stroke":93,"fill":205,"style":116},[90,21543],{"d":21544,"style":21545},"M140 150 L183 150","marker-end:url(#eb-arrow)",[90,21547],{"d":21548,"style":21545},"M315 150 L353 150",[90,21550],{"d":21551,"style":21545},"M485 150 L523 150",[90,21553],{"d":21554,"style":21545},"M655 150 L693 150",[99,21556,21558],{"x":101,"y":21557,"fill":93,"style":859},"252","Each stage removes code the page never needed; the browser only ever sees the trimmed result.",[76,21560,78,21561,66],{},[80,21562,88,21564,78],{"id":21563,"viewBox":83,"refX":84,"refY":85,"markerWidth":86,"markerHeight":86,"orient":87},"eb-arrow",[90,21565],{"d":92,"fill":93},[218,21567,21568],{},"Scoping removes code from pages that don't use it, esbuild drops unused exports and whitespace, and deferring keeps third-party code off the critical path — 78 KB down to 11 KB.",[34,21570,21572],{"id":21571},"find-the-bloat","Find the Bloat",[14,21574,21575],{},"Build, then measure the output directory to find the largest scripts:",[987,21577,21579],{"className":989,"code":21578,"language":991,"meta":712,"style":712},"npx @11ty\u002Feleventy\nfind _site -type f -name '*.js' -exec du -h {} + | sort -rh | head\n",[253,21580,21581,21587],{"__ignoreMap":712},[995,21582,21583,21585],{"class":997,"line":998},[995,21584,1079],{"class":1007},[995,21586,12128],{"class":1023},[995,21588,21589,21591,21594,21596,21598,21600,21603,21606,21609,21612,21615,21618,21620,21623,21626,21628],{"class":997,"line":713},[995,21590,18793],{"class":1007},[995,21592,21593],{"class":1023}," _site",[995,21595,18799],{"class":1010},[995,21597,18802],{"class":1023},[995,21599,18808],{"class":1010},[995,21601,21602],{"class":1023}," '*.js'",[995,21604,21605],{"class":1010}," -exec",[995,21607,21608],{"class":1023}," du",[995,21610,21611],{"class":1010}," -h",[995,21613,21614],{"class":1023}," {}",[995,21616,21617],{"class":1023}," +",[995,21619,14477],{"class":1614},[995,21621,21622],{"class":1007}," sort",[995,21624,21625],{"class":1010}," -rh",[995,21627,14477],{"class":1614},[995,21629,21630],{"class":1007}," head\n",[14,21632,21633,21634,21637],{},"Cross-reference with a Lighthouse run to spot duplicated polyfills and oversized vendor code. On the site used for the numbers below, a single ",[253,21635,21636],{},"app.js"," in the base layout was 78 KB minified and shipped on all 1,200 pages, even though only three pages used the chart code inside it.",[34,21639,21641],{"id":21640},"scope-assets-with-the-bundle-plugin","Scope Assets with the Bundle Plugin",[14,21643,21644,21645,738,21648,21651,21652,21654],{},"Eleventy's bundle plugin (bundled with Eleventy 3.x; install it explicitly on older versions) provides ",[253,21646,21647],{},"{% js %}",[253,21649,21650],{},"{% css %}"," shortcodes that collect only the scripts a page actually uses, instead of a global ",[253,21653,2373],{}," in the layout. Minification is applied through a transform, not an option flag:",[987,21656,21658],{"className":1600,"code":21657,"language":1602,"meta":712,"style":712},"\u002F\u002F eleventy.config.js\nconst { EleventyBundlePlugin } = require(\"@11ty\u002Feleventy\u002Fsrc\u002FPlugins\u002FBundlePlugin.js\");\nconst esbuild = require(\"esbuild\");\n\nmodule.exports = function (eleventyConfig) {\n  eleventyConfig.addBundle(\"js\", {\n    transforms: [\n      async function (content) {\n        if (process.env.ELEVENTY_ENV !== \"production\") return content;\n        const { code } = await esbuild.transform(content, { minify: true });\n        return code;\n      },\n    ],\n  });\n};\n",[253,21659,21660,21664,21686,21704,21708,21726,21741,21746,21759,21784,21812,21820,21824,21829,21833],{"__ignoreMap":712},[995,21661,21662],{"class":997,"line":998},[995,21663,6223],{"class":1001},[995,21665,21666,21668,21670,21673,21675,21677,21679,21681,21684],{"class":997,"line":713},[995,21667,6228],{"class":1614},[995,21669,10130],{"class":1618},[995,21671,21672],{"class":1010},"EleventyBundlePlugin",[995,21674,10140],{"class":1618},[995,21676,7317],{"class":1614},[995,21678,6236],{"class":1007},[995,21680,1799],{"class":1618},[995,21682,21683],{"class":1023},"\"@11ty\u002Feleventy\u002Fsrc\u002FPlugins\u002FBundlePlugin.js\"",[995,21685,5829],{"class":1618},[995,21687,21688,21690,21693,21695,21697,21699,21702],{"class":997,"line":730},[995,21689,6228],{"class":1614},[995,21691,21692],{"class":1010}," esbuild",[995,21694,1775],{"class":1614},[995,21696,6236],{"class":1007},[995,21698,1799],{"class":1618},[995,21700,21701],{"class":1023},"\"esbuild\"",[995,21703,5829],{"class":1618},[995,21705,21706],{"class":997,"line":1544},[995,21707,1541],{"emptyLinePlaceholder":752},[995,21709,21710,21712,21714,21716,21718,21720,21722,21724],{"class":997,"line":1550},[995,21711,1767],{"class":1010},[995,21713,239],{"class":1618},[995,21715,1772],{"class":1010},[995,21717,1775],{"class":1614},[995,21719,1778],{"class":1614},[995,21721,1781],{"class":1618},[995,21723,1785],{"class":1784},[995,21725,1788],{"class":1618},[995,21727,21728,21730,21733,21735,21738],{"class":997,"line":1673},[995,21729,1793],{"class":1618},[995,21731,21732],{"class":1007},"addBundle",[995,21734,1799],{"class":1618},[995,21736,21737],{"class":1023},"\"js\"",[995,21739,21740],{"class":1618},", {\n",[995,21742,21743],{"class":997,"line":1678},[995,21744,21745],{"class":1618},"    transforms: [\n",[995,21747,21748,21751,21753,21755,21757],{"class":997,"line":1693},[995,21749,21750],{"class":1614},"      async",[995,21752,1778],{"class":1614},[995,21754,1781],{"class":1618},[995,21756,10137],{"class":1784},[995,21758,1788],{"class":1618},[995,21760,21761,21764,21767,21770,21773,21776,21778,21781],{"class":997,"line":1705},[995,21762,21763],{"class":1614},"        if",[995,21765,21766],{"class":1618}," (process.env.",[995,21768,21769],{"class":1010},"ELEVENTY_ENV",[995,21771,21772],{"class":1614}," !==",[995,21774,21775],{"class":1023}," \"production\"",[995,21777,1811],{"class":1618},[995,21779,21780],{"class":1614},"return",[995,21782,21783],{"class":1618}," content;\n",[995,21785,21786,21789,21791,21793,21795,21797,21799,21802,21805,21808,21810],{"class":997,"line":1711},[995,21787,21788],{"class":1614},"        const",[995,21790,10130],{"class":1618},[995,21792,253],{"class":1010},[995,21794,10140],{"class":1618},[995,21796,7317],{"class":1614},[995,21798,8281],{"class":1614},[995,21800,21801],{"class":1618}," esbuild.",[995,21803,21804],{"class":1007},"transform",[995,21806,21807],{"class":1618},"(content, { minify: ",[995,21809,6283],{"class":1010},[995,21811,6500],{"class":1618},[995,21813,21814,21817],{"class":997,"line":1717},[995,21815,21816],{"class":1614},"        return",[995,21818,21819],{"class":1618}," code;\n",[995,21821,21822],{"class":997,"line":1726},[995,21823,20336],{"class":1618},[995,21825,21826],{"class":997,"line":1732},[995,21827,21828],{"class":1618},"    ],\n",[995,21830,21831],{"class":997,"line":2967},[995,21832,8371],{"class":1618},[995,21834,21835],{"class":997,"line":2972},[995,21836,1877],{"class":1618},[14,21838,21839,21840,21842],{},"In templates, wrap ",[253,21841,21647],{}," blocks so they only emit on pages that need them (drive it from front matter), keeping per-route payloads minimal. Moving the chart code out of the global layout and into a scoped block took it off 1,197 pages immediately.",[34,21844,21846],{"id":21845},"bundle-and-tree-shake-with-esbuild","Bundle and Tree-Shake with esbuild",[14,21848,21849],{},"For real application JavaScript, compile with esbuild — it tree-shakes and minifies in one pass:",[987,21851,21853],{"className":989,"code":21852,"language":991,"meta":712,"style":712},"npx esbuild src\u002Fscripts\u002Findex.js \\\n  --bundle --minify --tree-shaking=true \\\n  --target=es2020 \\\n  --metafile=meta.json \\\n  --outfile=_site\u002Fassets\u002Fbundle.js\n",[253,21854,21855,21866,21878,21885,21892],{"__ignoreMap":712},[995,21856,21857,21859,21861,21864],{"class":997,"line":998},[995,21858,1079],{"class":1007},[995,21860,21692],{"class":1023},[995,21862,21863],{"class":1023}," src\u002Fscripts\u002Findex.js",[995,21865,3002],{"class":1010},[995,21867,21868,21871,21873,21876],{"class":997,"line":713},[995,21869,21870],{"class":1010},"  --bundle",[995,21872,3642],{"class":1010},[995,21874,21875],{"class":1010}," --tree-shaking=true",[995,21877,3002],{"class":1010},[995,21879,21880,21883],{"class":997,"line":730},[995,21881,21882],{"class":1010},"  --target=es2020",[995,21884,3002],{"class":1010},[995,21886,21887,21890],{"class":997,"line":1544},[995,21888,21889],{"class":1010},"  --metafile=meta.json",[995,21891,3002],{"class":1010},[995,21893,21894],{"class":997,"line":1550},[995,21895,21896],{"class":1010},"  --outfile=_site\u002Fassets\u002Fbundle.js\n",[14,21898,21899,21902,21903,21906,21907,3725,21910,21913],{},[253,21900,21901],{},"--metafile=meta.json"," writes an analysis you can inspect to see what each module contributes — paste it into esbuild's online analyzer or read it directly. Reference the output with ",[253,21904,21905],{},"\u003Cscript src=\"\u002Fassets\u002Fbundle.js\" defer>\u003C\u002Fscript>"," and add the source to Eleventy's passthrough copy if needed. Setting ",[253,21908,21909],{},"\"sideEffects\": false",[253,21911,21912],{},"package.json"," where it is accurate lets esbuild drop even more dead code.",[34,21915,21917],{"id":21916},"defer-third-party-scripts","Defer Third-Party Scripts",[14,21919,21920,21921,21924],{},"Third-party code is often the largest, least-controlled payload, and it runs on the same main thread your interactions need. Add ",[253,21922,21923],{},"defer"," to analytics and widgets, self-host critical fonts to avoid an extra DNS lookup and render-blocking request, and load heavy widgets via an Intersection Observer only when they scroll into view. Gate optional scripts on a front-matter flag:",[987,21926,21928],{"className":1912,"code":21927,"language":1914,"meta":712,"style":712},"layout: default\nneedsChart: true\n",[253,21929,21930,21939],{"__ignoreMap":712},[995,21931,21932,21934,21936],{"class":997,"line":998},[995,21933,7485],{"class":1921},[995,21935,1925],{"class":1618},[995,21937,21938],{"class":1023},"default\n",[995,21940,21941,21944,21946],{"class":997,"line":713},[995,21942,21943],{"class":1921},"needsChart",[995,21945,1925],{"class":1618},[995,21947,6408],{"class":1010},[14,21949,21950,21951,21954,21955,21957],{},"Then ",[253,21952,21953],{},"{% if needsChart %}…{% endif %}"," around the relevant ",[253,21956,21647],{}," block so the chart code never ships on pages that don't use it.",[34,21959,1166],{"id":1165},[14,21961,21962,21963,14710,21965,21967],{},"The same documentation site, before and after applying all three steps, measured with ",[253,21964,21465],{},[253,21966,2245],{}," and Lighthouse on a throttled mid-tier mobile profile:",[433,21969,21970,21983],{},[436,21971,21972],{},[439,21973,21974,21976,21979,21981],{},[442,21975,16580],{},[442,21977,21978],{},"JS shipped per page",[442,21980,10958],{},[442,21982,21108],{},[457,21984,21985,22001,22013,22027],{},[439,21986,21987,21992,21995,21998],{},[462,21988,21989,21990,982],{},"Baseline (global ",[253,21991,21636],{},[462,21993,21994],{},"78 KB",[462,21996,21997],{},"320 ms",[462,21999,22000],{},"79",[439,22002,22003,22006,22009,22011],{},[462,22004,22005],{},"After scoping with bundle plugin",[462,22007,22008],{},"31 KB",[462,22010,14119],{},[462,22012,584],{},[439,22014,22015,22018,22021,22024],{},[462,22016,22017],{},"After esbuild tree-shake + minify",[462,22019,22020],{},"19 KB",[462,22022,22023],{},"120 ms",[462,22025,22026],{},"93",[439,22028,22029,22032,22034,22037],{},[462,22030,22031],{},"After deferring third-party",[462,22033,21539],{},[462,22035,22036],{},"60 ms",[462,22038,579],{},[14,22040,22041],{},"The largest single win was scoping — taking the chart code off the 1,197 pages that never used it more than halved the typical page's JavaScript on its own. Tree-shaking removed a duplicated date-formatting library and unused exports, and deferring the analytics tag took it off the critical path entirely.",[34,22043,600],{"id":599},[39,22045,22046,22058,22069,22079],{},[42,22047,22048,22054,22055,22057],{},[229,22049,22050,22051,22053],{},"Global ",[253,22052,2373],{}," in the layout:"," every route then downloads it. Use scoped ",[253,22056,21647],{}," blocks or front-matter conditionals.",[42,22059,22060,22063,22064,3725,22066,22068],{},[229,22061,22062],{},"No tree-shaking:"," unused exports inflate the bundle. esbuild tree-shakes by default; set ",[253,22065,21909],{},[253,22067,21912],{}," where accurate so bundlers can drop more.",[42,22070,22071,22074,22075,22078],{},[229,22072,22073],{},"Dev settings in production:"," unminified output and sourcemaps shipped live. Gate minification on ",[253,22076,22077],{},"ELEVENTY_ENV=production"," in CI.",[42,22080,22081,22083,22084,22087,22088,22090],{},[229,22082,637],{}," scoping and bundling live in ",[253,22085,22086],{},"eleventy.config.js"," and template front matter, all version-controlled, so reverting is a ",[253,22089,15276],{}," and a rebuild — there is no runtime state and no cache to untangle.",[34,22092,642],{"id":641},[14,22094,22095,22096,22098,22099,22101],{},"Treat Eleventy JavaScript as opt-in per page: scope it with the bundle plugin, compile and tree-shake real application code with esbuild, and defer or lazy-load third-party scripts. Measure ",[253,22097,2245],{}," before and after with ",[253,22100,21465],{}," and a Lighthouse run, and the initial payload stays small as the site grows — here, 78 KB to 11 KB and a Total Blocking Time five times lower.",[34,22103,651],{"id":650},[653,22105,22107],{"id":22106},"does-eleventy-bundle-javascript-natively","Does Eleventy bundle JavaScript natively?",[14,22109,22110],{},"No. Eleventy is a templating tool and does not process JavaScript on its own. Use the Eleventy bundle plugin to scope scripts per page and esbuild or Rollup to compile, tree-shake, and minify the application code those pages reference.",[653,22112,22114],{"id":22113},"how-do-i-verify-the-reduction","How do I verify the reduction?",[14,22116,22117,22118,22120,22121,22124],{},"Measure the output directory before and after with ",[253,22119,21465],{},", inspect esbuild's ",[253,22122,22123],{},"--metafile"," to see what each module contributes, and confirm the field gains with a Lighthouse run. Watching Total Blocking Time fall in Lighthouse is the quickest signal that the trimmed payload reached the browser.",[653,22126,22128],{"id":22127},"can-i-load-heavy-scripts-only-on-certain-pages","Can I load heavy scripts only on certain pages?",[14,22130,22131,22132,22135],{},"Yes. Set a front-matter flag such as ",[253,22133,22134],{},"needsChart: true"," and wrap the relevant bundle shortcode in a conditional so the script ships only on the pages that set the flag. Everything else stays script-free.",[653,22137,22139],{"id":22138},"why-is-a-single-global-script-in-the-layout-so-costly","Why is a single global script in the layout so costly?",[14,22141,22142],{},"Because every route that extends that layout downloads, parses, and executes it, even pages that never use the feature. On a large site that one script multiplies across thousands of pages and shows up as wasted main-thread time and a worse INP everywhere.",[34,22144,684],{"id":683},[39,22146,22147,22154,22159,22164],{},[42,22148,22149,692,22151,22153],{},[229,22150,691],{},[23,22152,10896],{"href":10895}," — the opt-in JavaScript principle this applies to Eleventy.",[42,22155,22156,22158],{},[23,22157,20754],{"href":21412}," — the same trade-off with Astro's directive system.",[42,22160,22161,22163],{},[23,22162,10981],{"href":10980}," — confirming the trimmed payload improves field INP.",[42,22165,22166,22168],{},[23,22167,5501],{"href":5500}," — where JavaScript budgets fit the INP picture.",[1346,22170,22171],{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}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 pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}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 .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 .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}",{"title":712,"searchDepth":713,"depth":713,"links":22173},[22174,22175,22176,22177,22178,22179,22180,22181,22182,22188],{"id":36,"depth":713,"text":37},{"id":21571,"depth":713,"text":21572},{"id":21640,"depth":713,"text":21641},{"id":21845,"depth":713,"text":21846},{"id":21916,"depth":713,"text":21917},{"id":1165,"depth":713,"text":1166},{"id":599,"depth":713,"text":600},{"id":641,"depth":713,"text":642},{"id":650,"depth":713,"text":651,"children":22183},[22184,22185,22186,22187],{"id":22106,"depth":730,"text":22107},{"id":22113,"depth":730,"text":22114},{"id":22127,"depth":730,"text":22128},{"id":22138,"depth":730,"text":22139},{"id":683,"depth":713,"text":684},[22190,22191,22192,22193],{"name":737,"item":738},{"name":5501,"item":5500},{"name":10896,"item":10895},{"name":21375,"item":21374},"Cut JavaScript in Eleventy builds — scope scripts per route, tree-shake and bundle with esbuild, and defer third-party code. Concrete before\u002Fafter KB numbers.",[22196,22197,22199,22201],{"q":22107,"a":22110},{"q":22114,"a":22198},"Measure the output directory before and after with du, inspect esbuild's metafile to see what each module contributes, and confirm the field gains with a Lighthouse run. Watching Total Blocking Time fall in Lighthouse is the quickest signal that the trimmed payload reached the browser.",{"q":22128,"a":22200},"Yes. Set a front-matter flag such as needsChart true and wrap the relevant bundle shortcode in a conditional so the script ships only on the pages that set the flag. Everything else stays script-free.",{"q":22139,"a":22142},{},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fjavascript-hydration-partial-rendering\u002Fhow-to-reduce-bundle-size-in-eleventy-builds",{"title":21375,"description":22194},"performance-optimization-core-web-vitals-for-ssgs\u002Fjavascript-hydration-partial-rendering\u002Fhow-to-reduce-bundle-size-in-eleventy-builds\u002Findex","T5CKVjWC_pIoHQqmFO9ISG33uFzZ-H181I6jVePaeYM",{"id":22208,"title":10896,"body":22209,"breadcrumb":23168,"dateModified":743,"datePublished":2446,"description":23172,"extension":745,"faq":23173,"meta":23182,"navigation":752,"path":23183,"seo":23184,"slug":22213,"stem":23185,"type":2460,"__hash__":23186},"content\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fjavascript-hydration-partial-rendering\u002Findex.md",{"type":7,"value":22210,"toc":23149},[22211,22214,22217,22223,22307,22309,22348,22352,22355,22364,22417,22425,22429,22435,22466,22486,22493,22552,22560,22564,22567,22694,22697,22823,22829,22833,22848,22851,22964,22970,22974,22983,22985,23029,23031,23059,23061,23065,23068,23072,23083,23085,23088,23092,23095,23099,23110,23114,23117,23119,23146],[10,22212,10896],{"id":22213},"javascript-hydration-partial-rendering",[14,22215,22216],{},"Partial hydration is the single biggest lever a static site generator gives you over Interaction to Next Paint (INP). A static site already wins Largest Contentful Paint and Cumulative Layout Shift for free, because the HTML is pre-rendered. INP is the one Core Web Vital you can still wreck after the fact, and you wreck it by running too much JavaScript on the main thread during interaction. The fix is structural: ship the page as static HTML, then hydrate only the components that are genuinely interactive instead of booting a framework runtime over the entire document.",[14,22218,22219,22220,22222],{},"This guide is for engineers and documentation teams who already ship a static site and want their INP to match their LCP. We cover how to draw hydration boundaries, how to pick the right client directive, how to split and budget the JavaScript that remains, how to keep server and client renders in sync, and how to prove the result with field data. It sits inside the broader ",[23,22221,5501],{"href":5500}," effort, where hydration is the stage that owns INP.",[55,22224,22225,22304],{},[58,22226,66,22230,66,22233,66,22236,66,22238],{"viewBox":3462,"role":61,"ariaLabelledBy":22227,"xmlns":65},[22228,22229],"hyd-islands-title","hyd-islands-desc",[68,22231,22232],{"id":22228},"Islands architecture and the INP cost of each hydration directive",[72,22234,22235],{"id":22229},"A static page contains three interactive islands. Each island is labeled with a client directive — load, idle, and visible — and an estimate of its main-thread cost and effect on Interaction to Next Paint, while the surrounding static HTML ships zero JavaScript.",[107,22237],{"x":2515,"y":2515,"width":2516,"height":3474,"fill":205},[95,22239,78,22240,78,22243,78,22246,78,22249,78,22251,78,22254,78,22256,78,22259,78,22262,78,22265,78,22267,78,22270,78,22272,78,22275,78,22278,78,22281,78,22283,78,22286,78,22288,78,22291,78,22294,78,22297,78,22301,66],{"style":813},[99,22241,22242],{"x":1415,"y":4630,"fill":103,"style":1416},"Static HTML page with three hydrated islands",[107,22244],{"x":109,"y":110,"width":2590,"height":111,"rx":22245,"fill":185,"opacity":6172,"stroke":187,"style":116},"16",[99,22247,22248],{"x":2595,"y":17814,"fill":187,"style":20821},"Static HTML — 0 KB JavaScript",[107,22250],{"x":110,"y":159,"width":142,"height":2563,"rx":823,"fill":2564,"opacity":825,"stroke":2565,"style":116},[99,22252,22253],{"x":7852,"y":837,"fill":2565,"style":121},"Header search",[99,22255,2211],{"x":7852,"y":18406,"fill":103,"style":859},[99,22257,22258],{"x":7852,"y":4657,"fill":93,"style":126},"above the fold",[99,22260,22261],{"x":7852,"y":6160,"fill":93,"style":126},"~14 KB · runs now",[99,22263,22264],{"x":7852,"y":2596,"fill":2565,"style":126},"highest INP risk",[107,22266],{"x":5361,"y":159,"width":142,"height":2563,"rx":823,"fill":162,"opacity":163,"stroke":164,"style":116},[99,22268,22269],{"x":1415,"y":837,"fill":164,"style":121},"Comment box",[99,22271,2207],{"x":1415,"y":18406,"fill":103,"style":859},[99,22273,22274],{"x":1415,"y":4657,"fill":93,"style":126},"non-urgent",[99,22276,22277],{"x":1415,"y":6160,"fill":93,"style":126},"~9 KB · when idle",[99,22279,22280],{"x":1415,"y":2596,"fill":164,"style":126},"low INP risk",[107,22282],{"x":10820,"y":159,"width":142,"height":2563,"rx":823,"fill":824,"opacity":825,"stroke":824,"style":116},[99,22284,22285],{"x":2562,"y":837,"fill":824,"style":121},"Chart widget",[99,22287,2203],{"x":2562,"y":18406,"fill":103,"style":859},[99,22289,22290],{"x":2562,"y":4657,"fill":93,"style":126},"below the fold",[99,22292,22293],{"x":2562,"y":6160,"fill":93,"style":126},"~22 KB · on scroll",[99,22295,22296],{"x":2562,"y":2596,"fill":824,"style":126},"deferred, no cost yet",[99,22298,22300],{"x":1415,"y":22299,"fill":93,"style":859},"312","Static markup costs nothing; only the marked islands ship JavaScript, on the trigger you choose.",[99,22302,22303],{"x":1415,"y":2623,"fill":93,"style":859},"Fewer eager islands & later triggers = a freer main thread = lower INP.",[218,22305,22306],{},"Each island carries its own bundle and its own hydration trigger. Deferring work with client:idle and client:visible keeps the main thread free during early interaction, which is exactly what INP measures.",[34,22308,5447],{"id":5446},[39,22310,22311,22317,22332,22340],{},[42,22312,22313,22316],{},[229,22314,22315],{},"Drawing hydration boundaries"," — classifying components as static or interactive so most of the page ships as plain HTML.",[42,22318,22319,22322,22323,1850,22325,3706,22327,22329,22330,239],{},[229,22320,22321],{},"Choosing a client directive"," — when ",[253,22324,2211],{},[253,22326,2207],{},[253,22328,2203],{}," are each correct, and what each costs INP. The full comparison against booting the whole framework lives in ",[23,22331,20754],{"href":21412},[42,22333,22334,22337,22338,239],{},[229,22335,22336],{},"Trimming the JavaScript that remains"," — splitting and tree-shaking bundles, covered for template-only generators in ",[23,22339,21375],{"href":21374},[42,22341,22342,22345,22346,239],{},[229,22343,22344],{},"Proving it in production"," — wiring field measurement so INP regressions surface, detailed in ",[23,22347,10981],{"href":10980},[34,22349,22351],{"id":22350},"setting-hydration-boundaries","Setting Hydration Boundaries",[14,22353,22354],{},"Start by classifying every component on a page as static or interactive. On a typical documentation or marketing page, the answer is \"static\" for the large majority — headings, prose, tables, images, navigation that is just links. Interactive means it responds to input on the client: a search box, a tabbed widget, a chart with tooltips, a copy-to-clipboard button.",[14,22356,22357,22358,22360,22361,22363],{},"In Astro, the boundary is explicit through the ",[253,22359,10850],{}," directives. A component with no directive renders to HTML at build time and ships ",[229,22362,20500],{}," JavaScript. A component with a directive becomes an island — its own small bundle that hydrates on its own trigger.",[987,22365,22367],{"className":10854,"code":22366,"language":10856,"meta":712,"style":712},"---\nimport InteractiveChart from '..\u002Fcomponents\u002FInteractiveChart.jsx';\nimport StaticTable from '..\u002Fcomponents\u002FStaticTable.astro';\n---\n\u003Cmain>\n  \u003Ch1>Static Dashboard\u003C\u002Fh1>\n  \u003Cp>Non-interactive content ships as plain HTML — zero JS.\u003C\u002Fp>\n  \u003CStaticTable data={rows} \u002F>            \u003C!-- no directive: pure HTML -->\n  \u003CInteractiveChart client:visible data={chartData} \u002F>\n\u003C\u002Fmain>\n",[253,22368,22369,22373,22378,22383,22387,22392,22397,22402,22407,22412],{"__ignoreMap":712},[995,22370,22371],{"class":997,"line":998},[995,22372,8106],{},[995,22374,22375],{"class":997,"line":713},[995,22376,22377],{},"import InteractiveChart from '..\u002Fcomponents\u002FInteractiveChart.jsx';\n",[995,22379,22380],{"class":997,"line":730},[995,22381,22382],{},"import StaticTable from '..\u002Fcomponents\u002FStaticTable.astro';\n",[995,22384,22385],{"class":997,"line":1544},[995,22386,8106],{},[995,22388,22389],{"class":997,"line":1550},[995,22390,22391],{},"\u003Cmain>\n",[995,22393,22394],{"class":997,"line":1673},[995,22395,22396],{},"  \u003Ch1>Static Dashboard\u003C\u002Fh1>\n",[995,22398,22399],{"class":997,"line":1678},[995,22400,22401],{},"  \u003Cp>Non-interactive content ships as plain HTML — zero JS.\u003C\u002Fp>\n",[995,22403,22404],{"class":997,"line":1693},[995,22405,22406],{},"  \u003CStaticTable data={rows} \u002F>            \u003C!-- no directive: pure HTML -->\n",[995,22408,22409],{"class":997,"line":1705},[995,22410,22411],{},"  \u003CInteractiveChart client:visible data={chartData} \u002F>\n",[995,22413,22414],{"class":997,"line":1711},[995,22415,22416],{},"\u003C\u002Fmain>\n",[14,22418,22419,22420,270,22422,22424],{},"In Eleventy, Hugo, and Jekyll there is no directive system, but the principle is identical: ship static HTML and attach a small Alpine.js or vanilla-JavaScript handler only on the elements that need behavior. There is no global framework runtime to boot, so the static parts cost nothing. Coordinate hydration with ",[23,22421,14061],{"href":14060},[23,22423,2190],{"href":2189}," so island JavaScript isn't competing with font and image loading for the same main thread during the first seconds of a page load.",[34,22426,22428],{"id":22427},"choosing-the-right-client-directive","Choosing the Right Client Directive",[14,22430,22431,22432,22434],{},"The directive you pick decides ",[18,22433,20956],{}," the island's JavaScript runs, and that timing is the lever on INP. The three you reach for most:",[39,22436,22437,22444,22455],{},[42,22438,22439,22443],{},[229,22440,22441],{},[253,22442,2211],{}," — hydrate immediately, in the page's initial load. Reserve it for above-the-fold controls that must respond the instant the page paints, like a header search box. It is the most expensive choice because the work lands during the same window the user is first trying to interact.",[42,22445,22446,22450,22451,22454],{},[229,22447,22448],{},[253,22449,2207],{}," — hydrate once the browser reports an idle period (via ",[253,22452,22453],{},"requestIdleCallback","). Right for non-urgent interactivity like a comment form or a newsletter widget that the user won't touch in the first moment.",[42,22456,22457,22461,22462,22465],{},[229,22458,22459],{},[253,22460,2203],{}," — hydrate when the component scrolls near the viewport (via ",[253,22463,22464],{},"IntersectionObserver","). This is the default choice for most widgets, because below-the-fold work costs the early interaction window nothing.",[987,22467,22469],{"className":10854,"code":22468,"language":10856,"meta":712,"style":712},"\u003CSearchBox client:load \u002F>        \u003C!-- above the fold, must be instant -->\n\u003CNewsletterForm client:idle \u002F>   \u003C!-- can wait for an idle moment -->\n\u003CDataChart client:visible \u002F>     \u003C!-- below the fold, defer to scroll -->\n",[253,22470,22471,22476,22481],{"__ignoreMap":712},[995,22472,22473],{"class":997,"line":998},[995,22474,22475],{},"\u003CSearchBox client:load \u002F>        \u003C!-- above the fold, must be instant -->\n",[995,22477,22478],{"class":997,"line":713},[995,22479,22480],{},"\u003CNewsletterForm client:idle \u002F>   \u003C!-- can wait for an idle moment -->\n",[995,22482,22483],{"class":997,"line":730},[995,22484,22485],{},"\u003CDataChart client:visible \u002F>     \u003C!-- below the fold, defer to scroll -->\n",[14,22487,22488,22489,22492],{},"The default should always be ",[18,22490,22491],{},"no directive",". Add the narrowest directive that still works only when a component is genuinely interactive. The measured difference between these choices and a full-framework boot is large:",[433,22494,22495,22509],{},[436,22496,22497],{},[439,22498,22499,22502,22505,22507],{},[442,22500,22501],{},"Hydration strategy",[442,22503,22504],{},"Client JS shipped",[442,22506,10958],{},[442,22508,21105],{},[457,22510,22511,22522,22535],{},[439,22512,22513,22516,22518,22520],{},[462,22514,22515],{},"Full hydration (whole page)",[462,22517,21121],{},[462,22519,21124],{},[462,22521,21127],{},[439,22523,22524,22529,22531,22533],{},[462,22525,22526,22527],{},"All islands ",[253,22528,2211],{},[462,22530,21139],{},[462,22532,21142],{},[462,22534,10950],{},[439,22536,22537,22546,22548,22550],{},[462,22538,22539,22540,738,22542,738,22544],{},"Mixed ",[253,22541,21154],{},[253,22543,21157],{},[253,22545,21160],{},[462,22547,21139],{},[462,22549,10953],{},[462,22551,21167],{},[14,22553,22554,22555,22557,22558,239],{},"Same islands, same code — moving the non-urgent ones off ",[253,22556,2211],{}," cut Total Blocking Time by more than half and pulled field INP under the 200 ms \"good\" threshold. The side-by-side against booting the entire framework is in ",[23,22559,20754],{"href":21412},[34,22561,22563],{"id":22562},"splitting-and-budgeting-the-javascript-that-remains","Splitting and Budgeting the JavaScript That Remains",[14,22565,22566],{},"Even with disciplined boundaries, the islands you do ship should be split so they load in parallel and cache independently. Isolate vendor code from your island code so a change to one island doesn't bust the whole cached bundle:",[987,22568,22570],{"className":1600,"code":22569,"language":1602,"meta":712,"style":712},"\u002F\u002F astro.config.mjs\nimport { defineConfig } from 'astro\u002Fconfig';\n\nexport default defineConfig({\n  build: {\n    rollupOptions: {\n      output: {\n        manualChunks(id) {\n          if (id.includes('node_modules')) return 'vendor';\n          if (id.includes('\u002Fislands\u002F')) return 'islands';\n        },\n      },\n    },\n  },\n});\n",[253,22571,22572,22576,22588,22592,22602,22606,22610,22614,22626,22652,22674,22678,22682,22686,22690],{"__ignoreMap":712},[995,22573,22574],{"class":997,"line":998},[995,22575,1609],{"class":1001},[995,22577,22578,22580,22582,22584,22586],{"class":997,"line":713},[995,22579,1615],{"class":1614},[995,22581,1619],{"class":1618},[995,22583,1622],{"class":1614},[995,22585,1625],{"class":1023},[995,22587,1628],{"class":1618},[995,22589,22590],{"class":997,"line":730},[995,22591,1541],{"emptyLinePlaceholder":752},[995,22593,22594,22596,22598,22600],{"class":997,"line":1544},[995,22595,1681],{"class":1614},[995,22597,1684],{"class":1614},[995,22599,1687],{"class":1007},[995,22601,1690],{"class":1618},[995,22603,22604],{"class":997,"line":1550},[995,22605,5617],{"class":1618},[995,22607,22608],{"class":997,"line":1673},[995,22609,20306],{"class":1618},[995,22611,22612],{"class":997,"line":1678},[995,22613,20311],{"class":1618},[995,22615,22616,22619,22621,22624],{"class":997,"line":1693},[995,22617,22618],{"class":1007},"        manualChunks",[995,22620,1799],{"class":1618},[995,22622,22623],{"class":1784},"id",[995,22625,1788],{"class":1618},[995,22627,22628,22631,22634,22637,22639,22642,22645,22647,22650],{"class":997,"line":1705},[995,22629,22630],{"class":1614},"          if",[995,22632,22633],{"class":1618}," (id.",[995,22635,22636],{"class":1007},"includes",[995,22638,1799],{"class":1618},[995,22640,22641],{"class":1023},"'node_modules'",[995,22643,22644],{"class":1618},")) ",[995,22646,21780],{"class":1614},[995,22648,22649],{"class":1023}," 'vendor'",[995,22651,1628],{"class":1618},[995,22653,22654,22656,22658,22660,22662,22665,22667,22669,22672],{"class":997,"line":1711},[995,22655,22630],{"class":1614},[995,22657,22633],{"class":1618},[995,22659,22636],{"class":1007},[995,22661,1799],{"class":1618},[995,22663,22664],{"class":1023},"'\u002Fislands\u002F'",[995,22666,22644],{"class":1618},[995,22668,21780],{"class":1614},[995,22670,22671],{"class":1023}," 'islands'",[995,22673,1628],{"class":1618},[995,22675,22676],{"class":997,"line":1717},[995,22677,20331],{"class":1618},[995,22679,22680],{"class":997,"line":1726},[995,22681,20336],{"class":1618},[995,22683,22684],{"class":997,"line":1732},[995,22685,2964],{"class":1618},[995,22687,22688],{"class":997,"line":2967},[995,22689,1729],{"class":1618},[995,22691,22692],{"class":997,"line":2972},[995,22693,1735],{"class":1618},[14,22695,22696],{},"Then make the JavaScript budget a build gate so a regression fails CI rather than reaching production:",[987,22698,22700],{"className":989,"code":22699,"language":991,"meta":712,"style":712},"#!\u002Fusr\u002Fbin\u002Fenv bash\nMAX_KB=100\nACTUAL_KB=$(du -k dist\u002Fassets\u002F*.js | awk '{ sum += $1 } END { print sum }')\nif [ \"${ACTUAL_KB:-0}\" -gt \"$MAX_KB\" ]; then\n  echo \"FAIL: client JS ${ACTUAL_KB}KB exceeds ${MAX_KB}KB budget\"; exit 1\nfi\necho \"PASS: client JS within budget (${ACTUAL_KB}KB)\"\n",[253,22701,22702,22706,22716,22749,22782,22806,22811],{"__ignoreMap":712},[995,22703,22704],{"class":997,"line":998},[995,22705,18775],{"class":1001},[995,22707,22708,22711,22713],{"class":997,"line":713},[995,22709,22710],{"class":1618},"MAX_KB",[995,22712,7317],{"class":1614},[995,22714,22715],{"class":1023},"100\n",[995,22717,22718,22721,22723,22725,22727,22730,22733,22736,22739,22741,22744,22747],{"class":997,"line":730},[995,22719,22720],{"class":1618},"ACTUAL_KB",[995,22722,7317],{"class":1614},[995,22724,18859],{"class":1618},[995,22726,21465],{"class":1007},[995,22728,22729],{"class":1010}," -k",[995,22731,22732],{"class":1023}," dist\u002Fassets\u002F",[995,22734,22735],{"class":1010},"*",[995,22737,22738],{"class":1023},".js",[995,22740,14477],{"class":1614},[995,22742,22743],{"class":1007}," awk",[995,22745,22746],{"class":1023}," '{ sum += $1 } END { print sum }'",[995,22748,1835],{"class":1618},[995,22750,22751,22754,22756,22759,22761,22764,22766,22769,22771,22773,22776,22778,22780],{"class":997,"line":1544},[995,22752,22753],{"class":1614},"if",[995,22755,18903],{"class":1618},[995,22757,22758],{"class":1023},"\"${",[995,22760,22720],{"class":1618},[995,22762,22763],{"class":1614},":-",[995,22765,2515],{"class":1618},[995,22767,22768],{"class":1023},"}\"",[995,22770,18913],{"class":1614},[995,22772,4983],{"class":1023},[995,22774,22775],{"class":1618},"$MAX_KB",[995,22777,18873],{"class":1023},[995,22779,18923],{"class":1618},[995,22781,18926],{"class":1614},[995,22783,22784,22787,22790,22792,22795,22797,22800,22802,22804],{"class":997,"line":1550},[995,22785,22786],{"class":1010},"  echo",[995,22788,22789],{"class":1023}," \"FAIL: client JS ${",[995,22791,22720],{"class":1618},[995,22793,22794],{"class":1023},"}KB exceeds ${",[995,22796,22710],{"class":1618},[995,22798,22799],{"class":1023},"}KB budget\"",[995,22801,18846],{"class":1618},[995,22803,18949],{"class":1010},[995,22805,18952],{"class":1010},[995,22807,22808],{"class":997,"line":1673},[995,22809,22810],{"class":1614},"fi\n",[995,22812,22813,22815,22818,22820],{"class":997,"line":1678},[995,22814,18967],{"class":1010},[995,22816,22817],{"class":1023}," \"PASS: client JS within budget (${",[995,22819,22720],{"class":1618},[995,22821,22822],{"class":1023},"}KB)\"\n",[14,22824,22825,22826,22828],{},"For template-only generators that don't bundle at all, the trimming work is different — you scope scripts per route and run them through esbuild. That is covered end to end in ",[23,22827,21375],{"href":21374},", where scoping a global script down to the three pages that needed it cut the sitewide payload from 78 KB to 11 KB.",[34,22830,22832],{"id":22831},"keeping-server-and-client-renders-in-sync","Keeping Server and Client Renders in Sync",[14,22834,22835,22836,1850,22839,22842,22843,270,22845,22847],{},"Hydration assumes the client can reuse the server's HTML. When the two renders disagree, the framework throws away the server markup and re-renders on the client — a hydration mismatch that costs main-thread time and often flashes a layout shift. The usual causes are non-deterministic values rendered during the build: ",[253,22837,22838],{},"Date.now()",[253,22840,22841],{},"Math.random()",", locale-dependent formatting, or browser-only APIs like ",[253,22844,21301],{},[253,22846,21304],{}," read during render.",[14,22849,22850],{},"Keep server output deterministic, and guard browser-only access so it runs after hydration rather than during render:",[987,22852,22854],{"className":8939,"code":22853,"language":8941,"meta":712,"style":712},"\u002F\u002F Read window only after mount, never during the render that the server also runs.\nimport { useEffect, useState } from 'react';\n\nexport default function Width() {\n  const [w, setW] = useState(null);\n  useEffect(() => setW(window.innerWidth), []);\n  return \u003Cspan>{w ?? '—'}\u003C\u002Fspan>;\n}\n",[253,22855,22856,22861,22875,22879,22892,22921,22937,22960],{"__ignoreMap":712},[995,22857,22858],{"class":997,"line":998},[995,22859,22860],{"class":1001},"\u002F\u002F Read window only after mount, never during the render that the server also runs.\n",[995,22862,22863,22865,22868,22870,22873],{"class":997,"line":713},[995,22864,1615],{"class":1614},[995,22866,22867],{"class":1618}," { useEffect, useState } ",[995,22869,1622],{"class":1614},[995,22871,22872],{"class":1023}," 'react'",[995,22874,1628],{"class":1618},[995,22876,22877],{"class":997,"line":730},[995,22878,1541],{"emptyLinePlaceholder":752},[995,22880,22881,22883,22885,22887,22890],{"class":997,"line":1544},[995,22882,1681],{"class":1614},[995,22884,1684],{"class":1614},[995,22886,1778],{"class":1614},[995,22888,22889],{"class":1007}," Width",[995,22891,8978],{"class":1618},[995,22893,22894,22896,22898,22901,22903,22906,22909,22911,22914,22916,22919],{"class":997,"line":1550},[995,22895,6270],{"class":1614},[995,22897,9305],{"class":1618},[995,22899,22900],{"class":1010},"w",[995,22902,1850],{"class":1618},[995,22904,22905],{"class":1010},"setW",[995,22907,22908],{"class":1618},"] ",[995,22910,7317],{"class":1614},[995,22912,22913],{"class":1007}," useState",[995,22915,1799],{"class":1618},[995,22917,22918],{"class":1010},"null",[995,22920,5829],{"class":1618},[995,22922,22923,22926,22929,22931,22934],{"class":997,"line":1673},[995,22924,22925],{"class":1007},"  useEffect",[995,22927,22928],{"class":1618},"(() ",[995,22930,1858],{"class":1614},[995,22932,22933],{"class":1007}," setW",[995,22935,22936],{"class":1618},"(window.innerWidth), []);\n",[995,22938,22939,22941,22943,22945,22948,22951,22954,22956,22958],{"class":997,"line":1678},[995,22940,5855],{"class":1614},[995,22942,9059],{"class":1618},[995,22944,995],{"class":1921},[995,22946,22947],{"class":1618},">{w ",[995,22949,22950],{"class":1614},"??",[995,22952,22953],{"class":1023}," '—'",[995,22955,10285],{"class":1618},[995,22957,995],{"class":1921},[995,22959,10290],{"class":1618},[995,22961,22962],{"class":997,"line":1693},[995,22963,9008],{"class":1618},[14,22965,22966,22967,22969],{},"For components that genuinely cannot render on the server because they depend on the DOM, skip server rendering entirely with ",[253,22968,20985],{}," (the framework name is required) rather than letting them produce broken server markup.",[34,22971,22973],{"id":22972},"production-monitoring","Production Monitoring",[14,22975,22976,22977,22980,22981,239],{},"Lab tools like Lighthouse give you Total Blocking Time, a useful proxy, but INP is a field metric — it is measured from real interactions across real devices. Deploy Real User Monitoring to track INP against when each hydration chunk loads, so a slow island shows up as a correlated INP spike. Provide a sensible non-interactive fallback for every island so a slow network doesn't leave a component dead while its bundle is still downloading. The full setup — from the ",[253,22978,22979],{},"web-vitals"," library to attributing INP to a specific interaction — is in ",[23,22982,10981],{"href":10980},[34,22984,2266],{"id":2265},[39,22986,22987,22995,23007,23018,23023],{},[42,22988,22989,22991,22992,22994],{},[229,22990,20593],{}," a ",[253,22993,10850],{}," directive on presentational markup ships JavaScript for nothing and raises INP. Default to no directive; add one only for genuine interactivity.",[42,22996,22997,23002,23003,2204,23005,239],{},[229,22998,22999,23000,931],{},"Everything on ",[253,23001,2211],{}," even correctly-marked islands hurt INP if they all hydrate eagerly. Move anything not strictly above the fold to ",[253,23004,2207],{},[253,23006,2203],{},[42,23008,23009,23012,23013,1850,23015,23017],{},[229,23010,23011],{},"Hydration mismatches:"," server HTML that differs from the client render (unguarded ",[253,23014,22838],{},[253,23016,21301],{},"-only code) causes a hydration abort and layout shift. Keep server output deterministic and guard browser-only APIs.",[42,23019,23020,23022],{},[229,23021,20608],{}," analytics, chat, and ad tags run on the same main thread your interactions need, and they routinely wreck INP on otherwise-fast pages. Budget them as strictly as your own code.",[42,23024,23025,23028],{},[229,23026,23027],{},"No CI budget:"," without a build gate, hydration creep is invisible until it shows up in field INP weeks later.",[34,23030,2321],{"id":2320},[39,23032,23033,23036,23042,23053,23056],{},[42,23034,23035],{},"INP is the one Core Web Vital a static site can still lose, and it is lost on the main thread during interaction.",[42,23037,23038,23039,23041],{},"Default to zero JavaScript; hydrate only genuine islands with the narrowest ",[253,23040,10850],{}," directive that works.",[42,23043,23044,23045,270,23047,23049,23050,23052],{},"Directive timing is the lever: ",[253,23046,2203],{},[253,23048,2207],{}," keep the early interaction window free, ",[253,23051,2211],{}," spends it.",[42,23054,23055],{},"Split and budget the JavaScript that remains, and fail the build when a route exceeds its budget.",[42,23057,23058],{},"Keep server and client renders deterministic to avoid mismatch cost, and confirm the result with field INP, not just lab Total Blocking Time.",[34,23060,651],{"id":650},[653,23062,23064],{"id":23063},"what-is-partial-hydration-and-why-does-it-matter-for-inp","What is partial hydration and why does it matter for INP?",[14,23066,23067],{},"Partial hydration means shipping a page as static HTML and attaching JavaScript only to the components that are genuinely interactive, instead of booting a framework over the whole page. Because Interaction to Next Paint is driven by main-thread work during interaction, hydrating fewer components leaves the main thread free and keeps INP low.",[653,23069,23071],{"id":23070},"when-should-i-use-clientload-versus-clientvisible-versus-clientidle","When should I use client:load versus client:visible versus client:idle?",[14,23073,2360,23074,23076,23077,23079,23080,23082],{},[253,23075,2211],{}," only for above-the-fold controls that must respond the instant the page appears, such as a header search box. Use ",[253,23078,2203],{}," for anything below the fold so its JavaScript is deferred until the component scrolls near the viewport. Use ",[253,23081,2207],{}," for non-urgent widgets that can wait until the main thread is quiet.",[653,23084,21331],{"id":21330},[14,23086,23087],{},"No. It makes JavaScript opt-in rather than default. The static parts of the page ship zero JavaScript, but each interactive island still ships the targeted bundle it needs to hydrate. The win is that you stop paying for a framework runtime on the 90 percent of the page that is static.",[653,23089,23091],{"id":23090},"how-do-i-stop-hydration-regressions-from-shipping","How do I stop hydration regressions from shipping?",[14,23093,23094],{},"Make the client JavaScript budget a build gate. Sum the size of the emitted client chunks in CI and fail the build when the total exceeds your per-route budget. Pair that with field Real User Monitoring so a regression that slips past the lab still surfaces in production INP data.",[653,23096,23098],{"id":23097},"what-causes-a-hydration-mismatch-and-how-do-i-avoid-it","What causes a hydration mismatch and how do I avoid it?",[14,23100,23101,23102,23104,23105,270,23107,23109],{},"A mismatch happens when the HTML rendered on the server differs from what the component renders on the client, usually because of non-deterministic values like ",[253,23103,22838],{}," or browser-only APIs read during render. Keep server output deterministic and guard ",[253,23106,21301],{},[253,23108,21304],{}," access so the client can reuse the server markup instead of throwing it away.",[653,23111,23113],{"id":23112},"do-non-astro-generators-support-partial-hydration","Do non-Astro generators support partial hydration?",[14,23115,23116],{},"Not as a built-in directive system, but you get the same effect by shipping static HTML from Eleventy, Hugo, or Jekyll and attaching a small Alpine.js or vanilla-JavaScript handler only where interactivity is needed. The principle is identical — no global framework runtime, just scoped behavior on specific elements.",[34,23118,684],{"id":683},[39,23120,23121,23127,23132,23136,23141],{},[42,23122,23123,692,23125,21386],{},[229,23124,691],{},[23,23126,5501],{"href":5500},[42,23128,23129,23131],{},[23,23130,20754],{"href":21412}," — the main-thread comparison with concrete numbers.",[42,23133,23134,21376],{},[23,23135,21375],{"href":21374},[42,23137,23138,23140],{},[23,23139,10981],{"href":10980}," — proving the result with field data.",[42,23142,23143,23145],{},[23,23144,13849],{"href":14803}," — the sibling effort that owns TTFB and repeat-visit cost.",[1346,23147,23148],{},"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 .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 .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}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 .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}",{"title":712,"searchDepth":713,"depth":713,"links":23150},[23151,23152,23153,23154,23155,23156,23157,23158,23159,23167],{"id":5446,"depth":713,"text":5447},{"id":22350,"depth":713,"text":22351},{"id":22427,"depth":713,"text":22428},{"id":22562,"depth":713,"text":22563},{"id":22831,"depth":713,"text":22832},{"id":22972,"depth":713,"text":22973},{"id":2265,"depth":713,"text":2266},{"id":2320,"depth":713,"text":2321},{"id":650,"depth":713,"text":651,"children":23160},[23161,23162,23163,23164,23165,23166],{"id":23063,"depth":730,"text":23064},{"id":23070,"depth":730,"text":23071},{"id":21330,"depth":730,"text":21331},{"id":23090,"depth":730,"text":23091},{"id":23097,"depth":730,"text":23098},{"id":23112,"depth":730,"text":23113},{"id":683,"depth":713,"text":684},[23169,23170,23171],{"name":737,"item":738},{"name":5501,"item":5500},{"name":10896,"item":10895},"Cut INP on static sites with partial hydration — ship static HTML, hydrate only real islands, choose the right client directive, and enforce a JS budget in CI.",[23174,23175,23177,23178,23179,23181],{"q":23064,"a":23067},{"q":23071,"a":23176},"Use client:load only for above-the-fold controls that must respond the instant the page appears, such as a header search box. Use client:visible for anything below the fold so its JavaScript is deferred until the component scrolls near the viewport. Use client:idle for non-urgent widgets that can wait until the main thread is quiet.",{"q":21331,"a":23087},{"q":23091,"a":23094},{"q":23098,"a":23180},"A mismatch happens when the HTML rendered on the server differs from what the component renders on the client, usually because of non-deterministic values like Date.now or browser-only APIs read during render. Keep server output deterministic and guard window and document access so the client can reuse the server markup instead of throwing it away.",{"q":23113,"a":23116},{},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fjavascript-hydration-partial-rendering",{"title":10896,"description":23172},"performance-optimization-core-web-vitals-for-ssgs\u002Fjavascript-hydration-partial-rendering\u002Findex","vDJ0R6WtLMDl1Ku74v52D2JhbN7NG_RyMv_295H7Tqg",{"id":23188,"title":23189,"body":23190,"breadcrumb":23940,"dateModified":743,"datePublished":743,"description":23945,"extension":745,"faq":23946,"meta":23952,"navigation":752,"path":23953,"seo":23954,"slug":23194,"stem":23955,"type":756,"__hash__":23956},"content\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fjavascript-hydration-partial-rendering\u002Fmeasuring-inp-on-static-sites-with-real-user-monitoring\u002Findex.md","Measuring INP on Static Sites with RUM",{"type":7,"value":23191,"toc":23922},[23192,23195,23206,23208,23221,23225,23232,23316,23320,23330,23344,23510,23519,23523,23526,23672,23679,23683,23690,23713,23716,23738,23740,23746,23796,23799,23801,23848,23850,23856,23858,23862,23865,23869,23872,23876,23879,23883,23886,23890,23893,23895,23919],[10,23193,10981],{"id":23194},"measuring-inp-on-static-sites-with-real-user-monitoring",[14,23196,23197,23198,23200,23201,23203,23204,17758],{},"Static sites are fast by default on LCP and CLS, but Interaction to Next Paint (INP) is the one Core Web Vital you cannot measure in a lab — it needs a real person to interact. The only way to know your true INP is to collect it in the field with real-user monitoring (RUM): the ",[253,23199,22979],{}," library reports the metric, attributes it to a specific interaction, and beacons it to a dashboard. This recipe covers wiring up that pipeline, attributing slow interactions, and reconciling lab versus field numbers. It belongs to ",[23,23202,10896],{"href":10895}," — where the cause of bad INP usually lives — inside the broader ",[23,23205,5501],{"href":5500},[34,23207,37],{"id":36},[39,23209,23210,23213,23216],{},[42,23211,23212],{},"A deployed static site (Astro, Eleventy, Hugo, or Jekyll) with some interactivity — search, menus, tabs, or hydrated components.",[42,23214,23215],{},"An endpoint to receive beacons: a serverless function, an analytics provider that ingests Web Vitals, or a logging endpoint that writes to a store you can query.",[42,23217,16939,23218,23220],{},[253,23219,16647],{}," for the lab comparison, plus the field data once it accumulates.",[34,23222,23224],{"id":23223},"why-the-lab-cannot-tell-you-inp","Why the Lab Cannot Tell You INP",[14,23226,23227,23228,23231],{},"Lighthouse runs a synthetic page load and reports ",[229,23229,23230],{},"Total Blocking Time (TBT)"," as a lab proxy because there is no sustained user interaction to measure. INP measures the latency from a real click, tap, or keypress to the next frame painted, across the whole page visit, reporting roughly the worst interaction. You can have a perfect TBT and still ship a 600 ms INP if a single hydrated handler blocks the main thread when a user actually clicks it. That gap is exactly why field measurement is mandatory.",[55,23233,23234,23313],{},[58,23235,66,23240,66,23243,66,23246,66,23306],{"viewBox":23236,"role":61,"ariaLabelledBy":23237,"xmlns":65},"0 0 780 300",[23238,23239],"inprum-flow-title","inprum-flow-desc",[68,23241,23242],{"id":23238},"Real-user monitoring data flow for INP",[72,23244,23245],{"id":23239},"A user interaction in the browser is measured by the web-vitals library, which sends a beacon to a collector endpoint that aggregates the 75th percentile INP onto a dashboard, with attribution to the slowest interaction target.",[95,23247,78,23248,78,23251,78,23253,78,23255,78,23258,78,23261,78,23263,78,23267,78,23270,78,23273,78,23276,78,23279,78,23282,78,23285,78,23287,78,23291,78,23294,66],{"style":97},[99,23249,23250],{"x":167,"y":102,"fill":103,"style":104},"Client interaction to field dashboard",[107,23252],{"x":109,"y":159,"width":119,"height":120,"rx":823,"fill":824,"opacity":186,"stroke":824,"style":116},[99,23254,14900],{"x":4682,"y":5379,"fill":824,"style":121},[99,23256,23257],{"x":4682,"y":841,"fill":93,"style":4658},"user clicks",[99,23259,23260],{"x":4682,"y":4651,"fill":93,"style":4658},"web-vitals onINP",[107,23262],{"x":184,"y":159,"width":161,"height":120,"rx":823,"fill":114,"opacity":186,"stroke":114,"style":116},[99,23264,23266],{"x":23265,"y":837,"fill":114,"style":121},"315","Beacon",[99,23268,23269],{"x":23265,"y":7852,"fill":93,"style":4658},"sendBeacon",[99,23271,23272],{"x":23265,"y":138,"fill":93,"style":4658},"value + target",[107,23274],{"x":11991,"y":159,"width":161,"height":120,"rx":823,"fill":162,"opacity":23275,"stroke":164,"style":116},"0.24",[99,23277,23278],{"x":15550,"y":837,"fill":103,"style":121},"Collector",[99,23280,23281],{"x":15550,"y":7852,"fill":93,"style":4658},"serverless fn",[99,23283,23284],{"x":15550,"y":138,"fill":93,"style":4658},"store",[107,23286],{"x":2562,"y":159,"width":4682,"height":120,"rx":823,"fill":185,"opacity":886,"stroke":187,"style":116},[99,23288,23290],{"x":23289,"y":3493,"fill":187,"style":121},"710","Dashboard",[99,23292,23293],{"x":23289,"y":11919,"fill":93,"style":4658},"p75 INP",[95,23295,88,23296,88,23300,88,23303,78],{"stroke":93,"fill":205,"style":116},[90,23297],{"d":23298,"style":23299},"M170 152 L238 152","marker-end:url(#inprum-arrow)",[90,23301],{"d":23302,"style":23299},"M390 152 L458 152",[90,23304],{"d":23305,"style":23299},"M610 152 L658 152",[76,23307,78,23308,66],{},[80,23309,88,23311,78],{"id":23310,"viewBox":83,"refX":84,"refY":85,"markerWidth":86,"markerHeight":86,"orient":87},"inprum-arrow",[90,23312],{"d":92,"fill":93},[218,23314,23315],{},"A real interaction is measured client-side by web-vitals, beaconed with its attribution to a collector, and aggregated to a 75th-percentile INP on a dashboard.",[34,23317,23319],{"id":23318},"step-1-collect-inp-with-the-attribution-build","Step 1: Collect INP with the Attribution Build",[14,23321,23322,23323,23325,23326,23329],{},"Install the ",[253,23324,22979],{}," library and import its ",[229,23327,23328],{},"attribution"," build, which adds the interaction target and a phase breakdown to each report:",[987,23331,23333],{"className":989,"code":23332,"language":991,"meta":712,"style":712},"npm i web-vitals\n",[253,23334,23335],{"__ignoreMap":712},[995,23336,23337,23339,23341],{"class":997,"line":998},[995,23338,1527],{"class":1007},[995,23340,18498],{"class":1023},[995,23342,23343],{"class":1023}," web-vitals\n",[987,23345,23347],{"className":1600,"code":23346,"language":1602,"meta":712,"style":712},"\u002F\u002F rum.js — loaded with defer or as a module\nimport { onINP } from 'web-vitals\u002Fattribution';\n\nfunction sendToCollector(metric) {\n  const body = JSON.stringify({\n    name: metric.name,                 \u002F\u002F \"INP\"\n    value: metric.value,               \u002F\u002F milliseconds\n    rating: metric.rating,             \u002F\u002F good | needs-improvement | poor\n    path: location.pathname,\n    \u002F\u002F attribution: which interaction was slowest, and why\n    target: metric.attribution.interactionTarget,   \u002F\u002F CSS selector\n    type: metric.attribution.interactionType,       \u002F\u002F pointer | keyboard\n    inputDelay: metric.attribution.inputDelay,\n    processing: metric.attribution.processingDuration,\n    presentation: metric.attribution.presentationDelay,\n  });\n  navigator.sendBeacon('\u002Fapi\u002Fvitals', body);\n}\n\nonINP(sendToCollector, { reportAllChanges: false });\n",[253,23348,23349,23354,23368,23372,23387,23406,23414,23422,23430,23435,23440,23448,23456,23461,23466,23471,23475,23490,23494,23498],{"__ignoreMap":712},[995,23350,23351],{"class":997,"line":998},[995,23352,23353],{"class":1001},"\u002F\u002F rum.js — loaded with defer or as a module\n",[995,23355,23356,23358,23361,23363,23366],{"class":997,"line":713},[995,23357,1615],{"class":1614},[995,23359,23360],{"class":1618}," { onINP } ",[995,23362,1622],{"class":1614},[995,23364,23365],{"class":1023}," 'web-vitals\u002Fattribution'",[995,23367,1628],{"class":1618},[995,23369,23370],{"class":997,"line":730},[995,23371,1541],{"emptyLinePlaceholder":752},[995,23373,23374,23377,23380,23382,23385],{"class":997,"line":1544},[995,23375,23376],{"class":1614},"function",[995,23378,23379],{"class":1007}," sendToCollector",[995,23381,1799],{"class":1618},[995,23383,23384],{"class":1784},"metric",[995,23386,1788],{"class":1618},[995,23388,23389,23391,23394,23396,23399,23401,23404],{"class":997,"line":1550},[995,23390,6270],{"class":1614},[995,23392,23393],{"class":1010}," body",[995,23395,1775],{"class":1614},[995,23397,23398],{"class":1010}," JSON",[995,23400,239],{"class":1618},[995,23402,23403],{"class":1007},"stringify",[995,23405,1690],{"class":1618},[995,23407,23408,23411],{"class":997,"line":1673},[995,23409,23410],{"class":1618},"    name: metric.name,                 ",[995,23412,23413],{"class":1001},"\u002F\u002F \"INP\"\n",[995,23415,23416,23419],{"class":997,"line":1678},[995,23417,23418],{"class":1618},"    value: metric.value,               ",[995,23420,23421],{"class":1001},"\u002F\u002F milliseconds\n",[995,23423,23424,23427],{"class":997,"line":1693},[995,23425,23426],{"class":1618},"    rating: metric.rating,             ",[995,23428,23429],{"class":1001},"\u002F\u002F good | needs-improvement | poor\n",[995,23431,23432],{"class":997,"line":1705},[995,23433,23434],{"class":1618},"    path: location.pathname,\n",[995,23436,23437],{"class":997,"line":1711},[995,23438,23439],{"class":1001},"    \u002F\u002F attribution: which interaction was slowest, and why\n",[995,23441,23442,23445],{"class":997,"line":1717},[995,23443,23444],{"class":1618},"    target: metric.attribution.interactionTarget,   ",[995,23446,23447],{"class":1001},"\u002F\u002F CSS selector\n",[995,23449,23450,23453],{"class":997,"line":1726},[995,23451,23452],{"class":1618},"    type: metric.attribution.interactionType,       ",[995,23454,23455],{"class":1001},"\u002F\u002F pointer | keyboard\n",[995,23457,23458],{"class":997,"line":1732},[995,23459,23460],{"class":1618},"    inputDelay: metric.attribution.inputDelay,\n",[995,23462,23463],{"class":997,"line":2967},[995,23464,23465],{"class":1618},"    processing: metric.attribution.processingDuration,\n",[995,23467,23468],{"class":997,"line":2972},[995,23469,23470],{"class":1618},"    presentation: metric.attribution.presentationDelay,\n",[995,23472,23473],{"class":997,"line":4147},[995,23474,8371],{"class":1618},[995,23476,23477,23480,23482,23484,23487],{"class":997,"line":4158},[995,23478,23479],{"class":1618},"  navigator.",[995,23481,23269],{"class":1007},[995,23483,1799],{"class":1618},[995,23485,23486],{"class":1023},"'\u002Fapi\u002Fvitals'",[995,23488,23489],{"class":1618},", body);\n",[995,23491,23492],{"class":997,"line":4168},[995,23493,9008],{"class":1618},[995,23495,23496],{"class":997,"line":4174},[995,23497,1541],{"emptyLinePlaceholder":752},[995,23499,23500,23503,23506,23508],{"class":997,"line":17372},[995,23501,23502],{"class":1007},"onINP",[995,23504,23505],{"class":1618},"(sendToCollector, { reportAllChanges: ",[995,23507,2929],{"class":1010},[995,23509,6500],{"class":1618},[14,23511,23512,23514,23515,23518],{},[253,23513,23502],{}," fires when the page is backgrounded or unloaded, reporting the worst interaction of the visit. ",[253,23516,23517],{},"navigator.sendBeacon"," queues the request without blocking unload. The whole module is around 2 to 3 KB compressed — small enough that it does not itself harm the interactivity you are measuring.",[34,23520,23522],{"id":23521},"step-2-receive-and-aggregate","Step 2: Receive and Aggregate",[14,23524,23525],{},"A minimal collector endpoint writes each beacon to a store you can query for percentiles. On Netlify or Cloudflare, a function works:",[987,23527,23529],{"className":1600,"code":23528,"language":1602,"meta":712,"style":712},"\u002F\u002F \u002Fapi\u002Fvitals — serverless function\nexport default async (request) => {\n  const m = await request.json();\n  if (m.name !== 'INP') return new Response(null, { status: 204 });\n  await store.append('inp', {\n    ts: Date.now(), path: m.path, value: m.value,\n    target: m.target, type: m.type,\n    inputDelay: m.inputDelay, processing: m.processing, presentation: m.presentation,\n  });\n  return new Response(null, { status: 204 });\n};\n",[253,23530,23531,23536,23555,23574,23607,23625,23636,23641,23646,23650,23668],{"__ignoreMap":712},[995,23532,23533],{"class":997,"line":998},[995,23534,23535],{"class":1001},"\u002F\u002F \u002Fapi\u002Fvitals — serverless function\n",[995,23537,23538,23540,23542,23544,23546,23549,23551,23553],{"class":997,"line":713},[995,23539,1681],{"class":1614},[995,23541,1684],{"class":1614},[995,23543,9021],{"class":1614},[995,23545,1781],{"class":1618},[995,23547,23548],{"class":1784},"request",[995,23550,1811],{"class":1618},[995,23552,1858],{"class":1614},[995,23554,8802],{"class":1618},[995,23556,23557,23559,23562,23564,23566,23569,23571],{"class":997,"line":730},[995,23558,6270],{"class":1614},[995,23560,23561],{"class":1010}," m",[995,23563,1775],{"class":1614},[995,23565,8281],{"class":1614},[995,23567,23568],{"class":1618}," request.",[995,23570,14265],{"class":1007},[995,23572,23573],{"class":1618},"();\n",[995,23575,23576,23578,23581,23584,23587,23589,23591,23593,23596,23598,23600,23603,23605],{"class":997,"line":1544},[995,23577,18900],{"class":1614},[995,23579,23580],{"class":1618}," (m.name ",[995,23582,23583],{"class":1614},"!==",[995,23585,23586],{"class":1023}," 'INP'",[995,23588,1811],{"class":1618},[995,23590,21780],{"class":1614},[995,23592,12078],{"class":1614},[995,23594,23595],{"class":1007}," Response",[995,23597,1799],{"class":1618},[995,23599,22918],{"class":1010},[995,23601,23602],{"class":1618},", { status: ",[995,23604,5402],{"class":1010},[995,23606,6500],{"class":1618},[995,23608,23609,23612,23615,23618,23620,23623],{"class":997,"line":1550},[995,23610,23611],{"class":1614},"  await",[995,23613,23614],{"class":1618}," store.",[995,23616,23617],{"class":1007},"append",[995,23619,1799],{"class":1618},[995,23621,23622],{"class":1023},"'inp'",[995,23624,21740],{"class":1618},[995,23626,23627,23630,23633],{"class":997,"line":1673},[995,23628,23629],{"class":1618},"    ts: Date.",[995,23631,23632],{"class":1007},"now",[995,23634,23635],{"class":1618},"(), path: m.path, value: m.value,\n",[995,23637,23638],{"class":997,"line":1678},[995,23639,23640],{"class":1618},"    target: m.target, type: m.type,\n",[995,23642,23643],{"class":997,"line":1693},[995,23644,23645],{"class":1618},"    inputDelay: m.inputDelay, processing: m.processing, presentation: m.presentation,\n",[995,23647,23648],{"class":997,"line":1705},[995,23649,8371],{"class":1618},[995,23651,23652,23654,23656,23658,23660,23662,23664,23666],{"class":997,"line":1711},[995,23653,5855],{"class":1614},[995,23655,12078],{"class":1614},[995,23657,23595],{"class":1007},[995,23659,1799],{"class":1618},[995,23661,22918],{"class":1010},[995,23663,23602],{"class":1618},[995,23665,5402],{"class":1010},[995,23667,6500],{"class":1618},[995,23669,23670],{"class":997,"line":1717},[995,23671,1877],{"class":1618},[14,23673,23674,23675,23678],{},"Then aggregate to the ",[229,23676,23677],{},"75th percentile per path"," — that is the threshold Google rates. Track p75, not the average, because the average hides the slow tail that actually hurts users. Many teams skip the self-built collector and point the same beacon at an analytics product that ingests Web Vitals; the client code is identical.",[34,23680,23682],{"id":23681},"step-3-attribute-and-reconcile-lab-vs-field","Step 3: Attribute and Reconcile Lab vs Field",[14,23684,23685,23686,23689],{},"The attribution fields turn a number into an action. Group your field INP by ",[253,23687,23688],{},"target"," to find the offending element, then read the phase breakdown:",[39,23691,23692,23701,23707],{},[42,23693,23694,23695,23698,23699,239],{},"High ",[253,23696,23697],{},"inputDelay",": the main thread was busy when the user interacted — usually hydration or a long task. This is the hydration-discipline fix from ",[23,23700,20754],{"href":21412},[42,23702,23694,23703,23706],{},[253,23704,23705],{},"processingDuration",": your event handler itself is slow — break up the work, debounce, or move it off the main thread.",[42,23708,23694,23709,23712],{},[253,23710,23711],{},"presentationDelay",": rendering the result is expensive — reduce DOM churn or large style recalcs.",[14,23714,23715],{},"Then reconcile with the lab. Run Lighthouse for the TBT proxy and compare it against the field p75 INP:",[987,23717,23719],{"className":989,"code":23718,"language":991,"meta":712,"style":712},"npx lhci autorun --collect.url=https:\u002F\u002Fyour-site.example.com \\\n  --assert.preset=lighthouse:recommended\n",[253,23720,23721,23734],{"__ignoreMap":712},[995,23722,23723,23725,23727,23729,23732],{"class":997,"line":998},[995,23724,1079],{"class":1007},[995,23726,19047],{"class":1023},[995,23728,16650],{"class":1023},[995,23730,23731],{"class":1010}," --collect.url=https:\u002F\u002Fyour-site.example.com",[995,23733,3002],{"class":1010},[995,23735,23736],{"class":997,"line":713},[995,23737,19063],{"class":1010},[34,23739,1166],{"id":1165},[14,23741,23742,23743,23745],{},"On a documentation site with a hydrated search box and a client-rendered filter, RUM revealed an INP problem the lab never showed. After attribution pointed at the search input's keydown handler (high ",[253,23744,23697],{}," from eager hydration), switching that island to hydrate on first interaction and debouncing the handler moved the numbers. Field values are p75 over ~4,000 page visits; lab is a Lighthouse mobile run:",[433,23747,23748,23763],{},[436,23749,23750],{},[439,23751,23752,23754,23757,23760],{},[442,23753,16580],{},[442,23755,23756],{},"Lab TBT (Lighthouse)",[442,23758,23759],{},"Field INP p75 (RUM)",[442,23761,23762],{},"Slowest target (attribution)",[457,23764,23765,23781],{},[439,23766,23767,23770,23772,23775],{},[462,23768,23769],{},"Before (eager hydration)",[462,23771,22023],{},[462,23773,23774],{},"480 ms",[462,23776,23777,23780],{},[253,23778,23779],{},"input.search"," keydown",[439,23782,23783,23786,23788,23790],{},[462,23784,23785],{},"After (hydrate on interaction + debounce)",[462,23787,10953],{},[462,23789,14119],{},[462,23791,23792,23795],{},[253,23793,23794],{},"button.filter"," pointer",[14,23797,23798],{},"The lab TBT barely moved (120 → 90 ms) and would never have flagged the problem — but field INP dropped from a poor 480 ms to a good 180 ms, crossing the 200 ms threshold. This is the entire argument for RUM: the lab said \"fine,\" the field said \"poor,\" and only the field number reflected what users felt. Pair this measurement with the hydration work in the parent guide so the fixes are durable.",[34,23800,600],{"id":599},[39,23802,23803,23809,23818,23828,23834,23840],{},[42,23804,23805,23808],{},[229,23806,23807],{},"Tracking the average instead of p75:"," the average masks the slow interactions. Always aggregate at the 75th percentile, the rating threshold.",[42,23810,23811,23817],{},[229,23812,23813,23814,931],{},"Using the base build instead of ",[253,23815,23816],{},"web-vitals\u002Fattribution"," without attribution you get a number but no target or phase breakdown, so you cannot tell what to fix.",[42,23819,23820,23823,23824,23827],{},[229,23821,23822],{},"Beaconing on every change:"," set ",[253,23825,23826],{},"reportAllChanges: false"," and report once on unload; flooding the collector adds cost and noise without better data.",[42,23829,23830,23833],{},[229,23831,23832],{},"Thin data:"," INP needs real traffic to stabilize. A handful of visits gives a noisy p75. Let data accumulate over days before acting, and segment by path so a slow page is not hidden in a site-wide aggregate.",[42,23835,23836,23839],{},[229,23837,23838],{},"Treating lab and field as interchangeable:"," they answer different questions. Keep both — lab for pre-deploy regression gates, field for the truth about real interactions.",[42,23841,23842,23844,23845,23847],{},[229,23843,637],{}," the RUM client is one small deferred module and a beacon endpoint. Removing it is deleting the import and the route; it carries no caching or build state. Because it is deferred and uses ",[253,23846,23269],{},", it does not affect the metrics it measures even while live.",[34,23849,642],{"id":641},[14,23851,23852,23853,23855],{},"INP is the Core Web Vital the lab cannot give you, so measure it in the field: the ",[253,23854,22979],{}," attribution build reports the metric and the slowest interaction, a small collector aggregates the 75th percentile per path, and the attribution breakdown tells you whether to fix hydration, the handler, or rendering. Reconcile that field p75 against Lighthouse's TBT proxy and trust the field number when they disagree. Once you can see which interaction is slow, the fixes live in hydration discipline — keep both lab gates and field monitoring running.",[34,23857,651],{"id":650},[653,23859,23861],{"id":23860},"why-cant-lighthouse-measure-inp","Why can't Lighthouse measure INP?",[14,23863,23864],{},"INP is an interaction metric — it needs a real user to click, tap, or type. Lighthouse runs a synthetic load with no sustained interaction, so it reports Total Blocking Time as a proxy, not INP. The only way to get a true INP value is from real users in the field, which is what real-user monitoring collects.",[653,23866,23868],{"id":23867},"how-does-the-web-vitals-library-attribute-inp-to-an-interaction","How does the web-vitals library attribute INP to an interaction?",[14,23870,23871],{},"The attribution build of the web-vitals library reports the interaction target element selector, the event type, and a breakdown of input delay, processing time, and presentation delay for the slowest interaction. That tells you which element and which phase to fix rather than just a number.",[653,23873,23875],{"id":23874},"how-much-javascript-does-rum-add-to-the-page","How much JavaScript does RUM add to the page?",[14,23877,23878],{},"The web-vitals attribution build is small, roughly 2 to 3 KB compressed, and it sends one beacon per page using navigator.sendBeacon, which does not block unload. The measurement cost is negligible compared to the interaction issues it helps you find.",[653,23880,23882],{"id":23881},"why-is-my-field-inp-worse-than-my-lab-score-suggested","Why is my field INP worse than my lab score suggested?",[14,23884,23885],{},"Lab tests run on a fixed device and network with no third-party variability. Real users hit slower devices, contended main threads, and late-loading third-party scripts that delay event handlers. Field INP captures the slow tail of real interactions that a single lab run never exercises.",[653,23887,23889],{"id":23888},"what-inp-value-should-i-target","What INP value should I target?",[14,23891,23892],{},"Google's good threshold is an INP at or below 200 milliseconds at the 75th percentile of page loads. Above 500 milliseconds is rated poor. Track the 75th percentile from your RUM data, not the average, because the average hides the slow interactions that hurt real users.",[34,23894,684],{"id":683},[39,23896,23897,23904,23909,23914],{},[42,23898,23899,692,23901,23903],{},[229,23900,691],{},[23,23902,10896],{"href":10895}," — where bad INP is usually caused and fixed.",[42,23905,23906,23908],{},[23,23907,20754],{"href":21412}," — the hydration strategy that drives input delay.",[42,23910,23911,23913],{},[23,23912,21375],{"href":21374}," — cutting the JavaScript that blocks the main thread.",[42,23915,23916,23918],{},[23,23917,5501],{"href":5500}," — where INP fits among the Core Web Vitals.",[1346,23920,23921],{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}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 .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 .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}",{"title":712,"searchDepth":713,"depth":713,"links":23923},[23924,23925,23926,23927,23928,23929,23930,23931,23932,23939],{"id":36,"depth":713,"text":37},{"id":23223,"depth":713,"text":23224},{"id":23318,"depth":713,"text":23319},{"id":23521,"depth":713,"text":23522},{"id":23681,"depth":713,"text":23682},{"id":1165,"depth":713,"text":1166},{"id":599,"depth":713,"text":600},{"id":641,"depth":713,"text":642},{"id":650,"depth":713,"text":651,"children":23933},[23934,23935,23936,23937,23938],{"id":23860,"depth":730,"text":23861},{"id":23867,"depth":730,"text":23868},{"id":23874,"depth":730,"text":23875},{"id":23881,"depth":730,"text":23882},{"id":23888,"depth":730,"text":23889},{"id":683,"depth":713,"text":684},[23941,23942,23943,23944],{"name":737,"item":738},{"name":5501,"item":5500},{"name":10896,"item":10895},{"name":23189,"item":10980},"Measure Interaction to Next Paint in the field with the web-vitals library and a RUM beacon, attribute INP to specific interactions, and reconcile lab versus field data.",[23947,23948,23949,23950,23951],{"q":23861,"a":23864},{"q":23868,"a":23871},{"q":23875,"a":23878},{"q":23882,"a":23885},{"q":23889,"a":23892},{},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Fjavascript-hydration-partial-rendering\u002Fmeasuring-inp-on-static-sites-with-real-user-monitoring",{"title":23189,"description":23945},"performance-optimization-core-web-vitals-for-ssgs\u002Fjavascript-hydration-partial-rendering\u002Fmeasuring-inp-on-static-sites-with-real-user-monitoring\u002Findex","IuHPDPaP7r8VbkIXeLk85D1q7XUcRYyayIGHNZK-zH0",{"id":23958,"title":23959,"body":23960,"breadcrumb":24644,"dateModified":743,"datePublished":743,"description":24650,"extension":745,"faq":24651,"meta":24656,"navigation":752,"path":24657,"seo":24658,"slug":23964,"stem":24659,"type":756,"__hash__":24660},"content\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Flargest-contentful-paint-optimization-for-static-sites\u002Feliminating-render-blocking-css-on-static-sites\u002Findex.md","Eliminating Render-Blocking CSS on Static Sites",{"type":7,"value":23961,"toc":24626},[23962,23965,23978,23980,24004,24085,24087,24091,24098,24235,24241,24245,24254,24335,24341,24345,24351,24421,24427,24429,24432,24488,24502,24504,24548,24550,24567,24569,24573,24576,24580,24583,24587,24590,24594,24597,24599,24623],[10,23963,23959],{"id":23964},"eliminating-render-blocking-css-on-static-sites",[14,23966,23967,23968,23971,23972,27,23976,32],{},"A static site can ship perfect HTML and a tiny hero image and still paint slowly, because a single external stylesheet in ",[253,23969,23970],{},"\u003Chead>"," blocks the first paint for an entire network round trip. The browser will not render anything — not the heading, not the hero — until that CSS has downloaded and parsed. This guide removes that block: inline the critical slice, load the rest non-blocking, and purge the rules you never use. It is the render-delay companion to ",[23,23973,23975],{"href":23974},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Flargest-contentful-paint-optimization-for-static-sites\u002F","Largest Contentful Paint Optimization for Static Sites",[23,23977,5501],{"href":5500},[34,23979,37],{"id":36},[39,23981,23982,23985,23999],{},[42,23983,23984],{},"A static site (Astro, Hugo, Eleventy, or Jekyll) that currently links one or more external stylesheets in the document head.",[42,23986,23987,23988,738,23991,23994,23995,23998],{},"A build step you can extend with a CSS tool (",[253,23989,23990],{},"critical",[253,23992,23993],{},"critters"," for inlining, ",[253,23996,23997],{},"PurgeCSS"," for pruning).",[42,24000,16939,24001,24003],{},[253,24002,16647],{}," and a deployed URL — the render-delay portion of LCP shows up clearly in the Lighthouse trace.",[55,24005,24006,24082],{},[58,24007,66,24011,66,24014,66,24017,66,24019,66,24075],{"viewBox":20093,"role":61,"ariaLabelledBy":24008,"xmlns":65},[24009,24010],"rbc-time-title","rbc-time-desc",[68,24012,24013],{"id":24009},"Render-blocking CSS versus inlined critical CSS",[72,24015,24016],{"id":24010},"Two timelines. The first shows HTML arriving, then a blocking stylesheet round trip, then first paint. The second shows HTML with inlined critical CSS painting immediately while the full stylesheet loads non-blocking.",[107,24018],{"x":2515,"y":2515,"width":8298,"height":1463,"fill":205},[95,24020,78,24021,78,24024,78,24028,78,24030,78,24032,78,24034,78,24037,78,24040,78,24043,78,24045,78,24048,78,24051,78,24054,78,24058,78,24061,78,24063,78,24066,78,24072,66],{"style":97},[99,24022,24023],{"x":101,"y":102,"fill":103,"style":104},"Where first paint happens on the timeline",[99,24025,24027],{"x":5393,"y":24026,"fill":2565,"style":2597},"75","Blocking",[107,24029],{"x":4682,"y":589,"width":1431,"height":16501,"rx":468,"fill":824,"opacity":4644},[99,24031,14007],{"x":7852,"y":828,"fill":103,"style":4658},[107,24033],{"x":111,"y":589,"width":142,"height":16501,"rx":468,"fill":2564,"opacity":4644},[99,24035,24036],{"x":1463,"y":828,"fill":103,"style":4658},"styles.css (blocks paint)",[997,24038],{"x1":5338,"y1":821,"x2":5338,"y2":833,"stroke":2565,"style":24039},"stroke-width:2px;stroke-dasharray:3 3",[99,24041,24042],{"x":5320,"y":828,"fill":2565,"style":11285},"FCP 2.1s",[997,24044],{"x1":4682,"y1":159,"x2":3571,"y2":159,"stroke":2592,"style":2602},[99,24046,24047],{"x":5393,"y":7852,"fill":187,"style":2597},"Critical inlined",[107,24049],{"x":4682,"y":24050,"width":1431,"height":16501,"rx":468,"fill":185,"opacity":4631},"147",[99,24052,24053],{"x":7852,"y":19417,"fill":103,"style":4658},"HTML + critical CSS",[997,24055],{"x1":111,"y1":24056,"x2":111,"y2":24057,"stroke":187,"style":24039},"137","181",[99,24059,24060],{"x":4634,"y":19417,"fill":187,"style":11285},"FCP 0.9s",[107,24062],{"x":184,"y":24050,"width":142,"height":16501,"rx":468,"fill":824,"opacity":5427},[99,24064,24065],{"x":6144,"y":19417,"fill":93,"style":4658},"styles.css (non-blocking)",[95,24067,88,24068,78],{"stroke":93,"fill":205,"style":116},[90,24069],{"d":24070,"style":24071},"M420 84 L220 137","marker-end:url(#rbc-arrow)",[99,24073,24074],{"x":101,"y":184,"fill":93,"style":126},"Inlining the critical slice lets the first paint happen on the HTML response, not after a separate CSS round trip.",[76,24076,78,24077,66],{},[80,24078,88,24080,78],{"id":24079,"viewBox":83,"refX":84,"refY":85,"markerWidth":86,"markerHeight":86,"orient":87},"rbc-arrow",[90,24081],{"d":92,"fill":93},[218,24083,24084],{},"A blocking stylesheet pushes first paint behind a full round trip; inlined critical CSS paints on the initial HTML while the full file loads in the background.",[34,24086,19484],{"id":19483},[653,24088,24090],{"id":24089},"_1-inline-the-critical-css-defer-the-rest","1. Inline the critical CSS, defer the rest",[14,24092,24093,24094,24097],{},"Extract the rules needed to render the first viewport, inline them in a ",[253,24095,24096],{},"\u003Cstyle>"," tag, and load the full stylesheet without blocking using the preload-swap pattern:",[987,24099,24101],{"className":16196,"code":24100,"language":16198,"meta":712,"style":712},"\u003Chead>\n  \u003Cstyle>\u002F* critical CSS for the first viewport: layout, hero, typography *\u002F\u003C\u002Fstyle>\n  \u003Clink rel=\"preload\" href=\"\u002Fstyles.css\" as=\"style\"\n        onload=\"this.onload=null;this.rel='stylesheet'\" \u002F>\n  \u003Cnoscript>\u003Clink rel=\"stylesheet\" href=\"\u002Fstyles.css\" \u002F>\u003C\u002Fnoscript>\n\u003C\u002Fhead>\n",[253,24102,24103,24112,24131,24157,24195,24227],{"__ignoreMap":712},[995,24104,24105,24107,24110],{"class":997,"line":998},[995,24106,16205],{"class":1618},[995,24108,24109],{"class":1921},"head",[995,24111,16246],{"class":1618},[995,24113,24114,24117,24119,24121,24124,24127,24129],{"class":997,"line":713},[995,24115,24116],{"class":1618},"  \u003C",[995,24118,1346],{"class":1921},[995,24120,20929],{"class":1618},[995,24122,24123],{"class":1001},"\u002F* critical CSS for the first viewport: layout, hero, typography *\u002F",[995,24125,24126],{"class":1618},"\u003C\u002F",[995,24128,1346],{"class":1921},[995,24130,16246],{"class":1618},[995,24132,24133,24135,24137,24139,24141,24143,24145,24147,24150,24152,24154],{"class":997,"line":730},[995,24134,24116],{"class":1618},[995,24136,16208],{"class":1921},[995,24138,16211],{"class":1007},[995,24140,7317],{"class":1618},[995,24142,16216],{"class":1023},[995,24144,16219],{"class":1007},[995,24146,7317],{"class":1618},[995,24148,24149],{"class":1023},"\"\u002Fstyles.css\"",[995,24151,16227],{"class":1007},[995,24153,7317],{"class":1618},[995,24155,24156],{"class":1023},"\"style\"\n",[995,24158,24159,24162,24164,24166,24169,24171,24174,24176,24178,24181,24183,24185,24188,24190,24193],{"class":997,"line":1544},[995,24160,24161],{"class":1007},"        onload",[995,24163,7317],{"class":1618},[995,24165,18873],{"class":1023},[995,24167,24168],{"class":1010},"this",[995,24170,239],{"class":1023},[995,24172,24173],{"class":1618},"onload",[995,24175,7317],{"class":1614},[995,24177,22918],{"class":1010},[995,24179,24180],{"class":1023},";",[995,24182,24168],{"class":1010},[995,24184,239],{"class":1023},[995,24186,24187],{"class":1618},"rel",[995,24189,7317],{"class":1614},[995,24191,24192],{"class":1023},"'stylesheet'\"",[995,24194,20528],{"class":1618},[995,24196,24197,24199,24202,24205,24207,24209,24211,24214,24216,24218,24220,24223,24225],{"class":997,"line":1550},[995,24198,24116],{"class":1618},[995,24200,24201],{"class":1921},"noscript",[995,24203,24204],{"class":1618},">\u003C",[995,24206,16208],{"class":1921},[995,24208,16211],{"class":1007},[995,24210,7317],{"class":1618},[995,24212,24213],{"class":1023},"\"stylesheet\"",[995,24215,16219],{"class":1007},[995,24217,7317],{"class":1618},[995,24219,24149],{"class":1023},[995,24221,24222],{"class":1618}," \u002F>\u003C\u002F",[995,24224,24201],{"class":1921},[995,24226,16246],{"class":1618},[995,24228,24229,24231,24233],{"class":997,"line":1673},[995,24230,24126],{"class":1618},[995,24232,24109],{"class":1921},[995,24234,16246],{"class":1618},[14,24236,8896,24237,24240],{},[253,24238,24239],{},"\u003Cnoscript>"," fallback keeps styles working when JavaScript is off. Keep the inlined slice under ~14 KB so it fits in the first round trip alongside the HTML.",[653,24242,24244],{"id":24243},"_2-generate-the-critical-slice-in-the-build-not-by-hand","2. Generate the critical slice in the build, not by hand",[14,24246,24247,24248,24250,24251,24253],{},"Hand-maintained critical CSS goes stale the moment the design changes. Generate it during the build so it always matches the current page. ",[253,24249,23993],{}," (Astro integrations bundle a version) and the standalone ",[253,24252,23990],{}," package both work:",[987,24255,24257],{"className":1600,"code":24256,"language":1602,"meta":712,"style":712},"\u002F\u002F astro.config.mjs\nimport { defineConfig } from 'astro\u002Fconfig';\nimport compress from 'astro-compress';\n\nexport default defineConfig({\n  integrations: [compress({ CSS: true })],\n  build: { inlineStylesheets: 'auto' }, \u002F\u002F Astro inlines small CSS automatically\n});\n",[253,24258,24259,24263,24275,24289,24293,24303,24318,24331],{"__ignoreMap":712},[995,24260,24261],{"class":997,"line":998},[995,24262,1609],{"class":1001},[995,24264,24265,24267,24269,24271,24273],{"class":997,"line":713},[995,24266,1615],{"class":1614},[995,24268,1619],{"class":1618},[995,24270,1622],{"class":1614},[995,24272,1625],{"class":1023},[995,24274,1628],{"class":1618},[995,24276,24277,24279,24282,24284,24287],{"class":997,"line":730},[995,24278,1615],{"class":1614},[995,24280,24281],{"class":1618}," compress ",[995,24283,1622],{"class":1614},[995,24285,24286],{"class":1023}," 'astro-compress'",[995,24288,1628],{"class":1618},[995,24290,24291],{"class":997,"line":1544},[995,24292,1541],{"emptyLinePlaceholder":752},[995,24294,24295,24297,24299,24301],{"class":997,"line":1550},[995,24296,1681],{"class":1614},[995,24298,1684],{"class":1614},[995,24300,1687],{"class":1007},[995,24302,1690],{"class":1618},[995,24304,24305,24307,24310,24313,24315],{"class":997,"line":1673},[995,24306,1696],{"class":1618},[995,24308,24309],{"class":1007},"compress",[995,24311,24312],{"class":1618},"({ CSS: ",[995,24314,6283],{"class":1010},[995,24316,24317],{"class":1618}," })],\n",[995,24319,24320,24323,24325,24328],{"class":997,"line":1678},[995,24321,24322],{"class":1618},"  build: { inlineStylesheets: ",[995,24324,5630],{"class":1023},[995,24326,24327],{"class":1618}," }, ",[995,24329,24330],{"class":1001},"\u002F\u002F Astro inlines small CSS automatically\n",[995,24332,24333],{"class":997,"line":1693},[995,24334,1735],{"class":1618},[14,24336,20950,24337,24340],{},[253,24338,24339],{},"inlineStylesheets: 'auto'"," inlines stylesheets below a size threshold automatically, which covers most content pages without a separate tool.",[653,24342,24344],{"id":24343},"_3-purge-unused-css","3. Purge unused CSS",[14,24346,24347,24348,24350],{},"Most stylesheets ship rules no page renders — utility frameworks are the worst offenders. ",[253,24349,23997],{}," scans your templates and content for the class names actually used and drops the rest, which both shrinks the deferred file and trims the critical slice:",[987,24352,24354],{"className":1600,"code":24353,"language":1602,"meta":712,"style":712},"\u002F\u002F purgecss.config.js\nmodule.exports = {\n  content: ['.\u002Fsrc\u002F**\u002F*.{astro,html,md,mdx}'],\n  safelist: [\u002F^is-\u002F, \u002F^has-\u002F], \u002F\u002F protect dynamically applied classes\n};\n",[253,24355,24356,24361,24373,24383,24417],{"__ignoreMap":712},[995,24357,24358],{"class":997,"line":998},[995,24359,24360],{"class":1001},"\u002F\u002F purgecss.config.js\n",[995,24362,24363,24365,24367,24369,24371],{"class":997,"line":713},[995,24364,1767],{"class":1010},[995,24366,239],{"class":1618},[995,24368,1772],{"class":1010},[995,24370,1775],{"class":1614},[995,24372,8802],{"class":1618},[995,24374,24375,24378,24381],{"class":997,"line":730},[995,24376,24377],{"class":1618},"  content: [",[995,24379,24380],{"class":1023},"'.\u002Fsrc\u002F**\u002F*.{astro,html,md,mdx}'",[995,24382,8306],{"class":1618},[995,24384,24385,24388,24390,24393,24396,24398,24401,24404,24406,24409,24411,24414],{"class":997,"line":1544},[995,24386,24387],{"class":1618},"  safelist: [",[995,24389,738],{"class":1023},[995,24391,24392],{"class":1614},"^",[995,24394,24395],{"class":10048},"is-",[995,24397,738],{"class":1023},[995,24399,24400],{"class":1618},",",[995,24402,24403],{"class":1023}," \u002F",[995,24405,24392],{"class":1614},[995,24407,24408],{"class":10048},"has-",[995,24410,738],{"class":1023},[995,24412,24413],{"class":1618},"], ",[995,24415,24416],{"class":1001},"\u002F\u002F protect dynamically applied classes\n",[995,24418,24419],{"class":997,"line":1550},[995,24420,1877],{"class":1618},[14,24422,19512,24423,24426],{},[253,24424,24425],{},"safelist"," for class names generated at runtime that the scanner cannot see in source, or purging will remove styles that are genuinely used.",[34,24428,1166],{"id":1165},[14,24430,24431],{},"Measured on a documentation landing page, throttled mobile profile (4x CPU, ~1.6 Mbps), median of five Lighthouse runs. The LCP element here was a heading, so render delay dominated:",[433,24433,24434,24449],{},[436,24435,24436],{},[439,24437,24438,24441,24444,24447],{},[442,24439,24440],{},"Change",[442,24442,24443],{},"CSS bytes (blocking)",[442,24445,24446],{},"FCP",[442,24448,20362],{},[457,24450,24451,24463,24476],{},[439,24452,24453,24456,24458,24460],{},[462,24454,24455],{},"Single 78 KB blocking stylesheet",[462,24457,21994],{},[462,24459,17521],{},[462,24461,24462],{},"2.4s",[439,24464,24465,24468,24471,24474],{},[462,24466,24467],{},"Inline critical + defer full file",[462,24469,24470],{},"9 KB inlined",[462,24472,24473],{},"0.9s",[462,24475,2169],{},[439,24477,24478,24481,24484,24486],{},[462,24479,24480],{},"+ PurgeCSS on the full file",[462,24482,24483],{},"9 KB inlined \u002F 22 KB deferred",[462,24485,24473],{},[462,24487,10939],{},[14,24489,24490,24491,24494,24495,24498,24499,24501],{},"Inlining a 9 KB critical slice and deferring the 78 KB file moved First Contentful Paint from ",[229,24492,24493],{},"2.1s to 0.9s"," and LCP from ",[229,24496,24497],{},"2.4s to 1.7s",". Purging the full stylesheet from 78 KB to 22 KB shaved the deferred load and brought LCP to ",[229,24500,10939],{},". The Lighthouse \"Eliminate render-blocking resources\" audit went from flagging 0.6s of potential savings to clean.",[34,24503,600],{"id":599},[39,24505,24506,24512,24518,24527,24536],{},[42,24507,24508,24511],{},[229,24509,24510],{},"Critical CSS too large:"," inlining the whole stylesheet defeats caching and bloats every HTML response. Keep it to the first viewport, under ~14 KB.",[42,24513,24514,24517],{},[229,24515,24516],{},"Stale hand-written critical CSS:"," it blocks paint with rules the page no longer needs. Always generate it in the build.",[42,24519,24520,24523,24524,24526],{},[229,24521,24522],{},"Over-aggressive purge:"," dropping classes applied at runtime breaks styling. Protect them with a ",[253,24525,24425],{}," and visually diff a few pages after purging.",[42,24528,24529,24535],{},[229,24530,24531,24532,24534],{},"Forgetting the ",[253,24533,24239],{}," fallback:"," without it, the deferred stylesheet never applies when JavaScript is disabled.",[42,24537,24538,24540,24541,24544,24545,24547],{},[229,24539,637],{}," revert to a single blocking ",[253,24542,24543],{},"\u003Clink rel=\"stylesheet\">"," and remove the inline ",[253,24546,24096],{}," and the build integration. The change is purely in generated HTML and the build config, so a git revert plus redeploy is enough.",[34,24549,642],{"id":641},[14,24551,24552,24553,24494,24555,24558,24559,24563,24564,24566],{},"Render-blocking CSS is invisible until you measure it, but it can cost half a second of blank screen on every page. Inline the critical slice, defer the full file, and purge what no page uses. On the example page these steps moved FCP from ",[229,24554,24493],{},[229,24556,24557],{},"2.4s to 1.6s"," without changing a line of content. Combine this with the hero work in ",[23,24560,24562],{"href":24561},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Flargest-contentful-paint-optimization-for-static-sites\u002Freducing-lcp-from-hero-images-on-static-sites\u002F","Reducing LCP from Hero Images on Static Sites"," and the priority hints in ",[23,24565,19813],{"href":19812}," for the complete LCP picture.",[34,24568,651],{"id":650},[653,24570,24572],{"id":24571},"what-is-render-blocking-css","What is render-blocking CSS?",[14,24574,24575],{},"An external stylesheet in the head that the browser must download and parse before it paints anything. Until that round trip finishes, the page stays blank, which delays First Contentful Paint and, when the LCP element is text, Largest Contentful Paint as well.",[653,24577,24579],{"id":24578},"how-big-should-the-inlined-critical-css-be","How big should the inlined critical CSS be?",[14,24581,24582],{},"Keep it to the rules needed to render the first viewport, typically under 14 KB so it fits in the first network round trip after the HTML. Bigger than that and you are inlining styles the first paint does not need, which bloats every HTML response.",[653,24584,24586],{"id":24585},"will-inlining-css-hurt-caching","Will inlining CSS hurt caching?",[14,24588,24589],{},"Inlined critical CSS is re-sent with every HTML document, so it is not cached separately. That is an acceptable trade for a small critical slice because HTML is short-lived anyway, while the full stylesheet still loads as a cacheable file with a long immutable lifetime.",[653,24591,24593],{"id":24592},"does-purging-unused-css-change-what-users-see","Does purging unused CSS change what users see?",[14,24595,24596],{},"It should not, if configured correctly. Purge tools scan your templates and content for the class names actually used and drop the rest. The risk is dynamically generated class names the scanner cannot see, which you protect with a safelist.",[34,24598,684],{"id":683},[39,24600,24601,24608,24613,24618],{},[42,24602,24603,692,24605,24607],{},[229,24604,691],{},[23,24606,23975],{"href":23974}," — the full LCP workflow.",[42,24609,24610,24612],{},[23,24611,24562],{"href":24561}," — fix the image side of LCP.",[42,24614,24615,24617],{},[23,24616,19813],{"href":19812}," — prioritize the LCP resource.",[42,24619,24620,24622],{},[23,24621,14061],{"href":14060}," — the other render-delay source on text-led pages.",[1346,24624,24625],{},"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 .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}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 pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}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 .sA_wV, html code.shiki .sA_wV{--shiki-default:#032F62;--shiki-dark:#DBEDFF}",{"title":712,"searchDepth":713,"depth":713,"links":24627},[24628,24629,24634,24635,24636,24637,24643],{"id":36,"depth":713,"text":37},{"id":19483,"depth":713,"text":19484,"children":24630},[24631,24632,24633],{"id":24089,"depth":730,"text":24090},{"id":24243,"depth":730,"text":24244},{"id":24343,"depth":730,"text":24344},{"id":1165,"depth":713,"text":1166},{"id":599,"depth":713,"text":600},{"id":641,"depth":713,"text":642},{"id":650,"depth":713,"text":651,"children":24638},[24639,24640,24641,24642],{"id":24571,"depth":730,"text":24572},{"id":24578,"depth":730,"text":24579},{"id":24585,"depth":730,"text":24586},{"id":24592,"depth":730,"text":24593},{"id":683,"depth":713,"text":684},[24645,24646,24647,24648],{"name":737,"item":738},{"name":5501,"item":5500},{"name":23975,"item":23974},{"name":23959,"item":24649},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Flargest-contentful-paint-optimization-for-static-sites\u002Feliminating-render-blocking-css-on-static-sites\u002F","Inline critical CSS, defer the rest, and purge unused rules to cut render delay on static sites — with measured LCP and FCP before\u002Fafter from Lighthouse.",[24652,24653,24654,24655],{"q":24572,"a":24575},{"q":24579,"a":24582},{"q":24586,"a":24589},{"q":24593,"a":24596},{},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Flargest-contentful-paint-optimization-for-static-sites\u002Feliminating-render-blocking-css-on-static-sites",{"title":23959,"description":24650},"performance-optimization-core-web-vitals-for-ssgs\u002Flargest-contentful-paint-optimization-for-static-sites\u002Feliminating-render-blocking-css-on-static-sites\u002Findex","Q5PzJFY3czKEnI2IlWaJ4ukfraiWSubI6KrMfn9BoP4",{"id":24662,"title":23975,"body":24663,"breadcrumb":25800,"dateModified":743,"datePublished":743,"description":25804,"extension":745,"faq":25805,"meta":25815,"navigation":752,"path":25816,"seo":25817,"slug":24667,"stem":25818,"type":2460,"__hash__":25819},"content\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Flargest-contentful-paint-optimization-for-static-sites\u002Findex.md",{"type":7,"value":24664,"toc":25782},[24665,24668,24674,24686,24797,24801,24804,24815,24921,24931,24987,24991,25001,25007,25058,25063,25101,25121,25125,25131,25156,25292,25332,25347,25351,25357,25479,25492,25496,25502,25546,25619,25625,25627,25662,25664,25684,25686,25690,25693,25697,25706,25710,25719,25723,25726,25730,25736,25740,25743,25745,25779],[10,24666,23975],{"id":24667},"largest-contentful-paint-optimization-for-static-sites",[14,24669,24670,24671,24673],{},"Largest Contentful Paint (LCP) measures how long it takes for the largest visible element in the viewport — usually a hero image, a heading, or a background image — to render. Static site generators hand you a head start because the HTML arrives pre-rendered in a single response, but the LCP element still has to be discovered, downloaded, and painted. This guide is the complete LCP workflow for static sites: find the LCP element, get it requested as early as possible, serve it in a fast format, and stop CSS and fonts from blocking the paint. It sits within the broader ",[23,24672,5501],{"href":5500}," effort, where LCP is the metric most directly tied to perceived load speed.",[14,24675,24676,24677,24679,24680,24683,24684,239],{},"Every step below ties to a measured number and the tool that produced it (Lighthouse, ",[253,24678,16647],{},", WebPageTest). The running example is a documentation landing page built with Astro, deployed to a CDN, and measured on a throttled mid-tier mobile profile (Moto G-class, 4x CPU slowdown, ~1.6 Mbps). The baseline LCP was ",[229,24681,24682],{},"3.4s","; the finished page lands at ",[229,24685,10939],{},[55,24687,24688,24794],{},[58,24689,66,24694,66,24697,66,24700,66,24702,66,24787],{"viewBox":24690,"role":61,"ariaLabelledBy":24691,"xmlns":65},"0 0 820 340",[24692,24693],"lcp-flow-title","lcp-flow-desc",[68,24695,24696],{"id":24692},"Where the LCP element's time goes",[72,24698,24699],{"id":24693},"A waterfall showing the four spans that make up Largest Contentful Paint on a static site: time to first byte, resource discovery delay, image download, and render delay, with the levers that shrink each span.",[107,24701],{"x":2515,"y":2515,"width":2516,"height":6144,"fill":205},[95,24703,78,24704,78,24707,78,24709,78,24714,78,24717,78,24719,78,24722,78,24725,78,24727,78,24730,78,24733,78,24735,78,24739,78,24742,78,24754,78,24757,78,24760,78,24763,78,24765,78,24769,78,24772,78,24775,78,24778,78,24781,78,24784,66],{"style":813},[99,24705,24706],{"x":1415,"y":2521,"fill":103,"style":1416},"LCP = TTFB + discovery + download + render delay",[107,24708],{"x":3578,"y":849,"width":161,"height":822,"rx":3579,"fill":824,"opacity":186,"stroke":824,"style":116},[99,24710,24713],{"x":24711,"y":24712,"fill":824,"style":121},"115","94","TTFB",[99,24715,24716],{"x":24711,"y":2531,"fill":93,"style":126},"edge cache",[107,24718],{"x":3500,"y":849,"width":194,"height":822,"rx":3579,"fill":162,"opacity":877,"stroke":164,"style":116},[99,24720,24721],{"x":19425,"y":24712,"fill":103,"style":121},"Discovery",[99,24723,24724],{"x":19425,"y":2531,"fill":93,"style":126},"preload & priority",[107,24726],{"x":101,"y":849,"width":194,"height":822,"rx":3579,"fill":185,"opacity":850,"stroke":187,"style":116},[99,24728,24729],{"x":18428,"y":24712,"fill":187,"style":121},"Download",[99,24731,24732],{"x":18428,"y":2531,"fill":93,"style":126},"AVIF\u002FWebP size",[107,24734],{"x":7842,"y":849,"width":175,"height":822,"rx":3579,"fill":2564,"opacity":825,"stroke":2565,"style":116},[99,24736,24738],{"x":24737,"y":24712,"fill":2565,"style":121},"685","Render delay",[99,24740,24741],{"x":24737,"y":2531,"fill":93,"style":126},"critical CSS & fonts",[95,24743,88,24744,88,24748,88,24751,78],{"stroke":93,"fill":205,"style":116},[90,24745],{"d":24746,"style":24747},"M190 98 L208 98","marker-end:url(#lcp-arrow)",[90,24749],{"d":24750,"style":24747},"M380 98 L398 98",[90,24752],{"d":24753,"style":24747},"M570 98 L588 98",[99,24755,24756],{"x":1415,"y":160,"fill":93,"style":859},"Same spans as a waterfall — shrink the widest first",[107,24758],{"x":3578,"y":142,"width":1431,"height":4630,"rx":876,"fill":824,"opacity":24759},"0.30",[99,24761,24762],{"x":4682,"y":146,"fill":103,"style":126},"0.2s",[107,24764],{"x":7852,"y":142,"width":194,"height":4630,"rx":876,"fill":162,"opacity":5411},[99,24766,24768],{"x":24767,"y":146,"fill":103,"style":126},"245","discovery 0.9s",[107,24770],{"x":874,"y":142,"width":184,"height":4630,"rx":876,"fill":185,"opacity":24771},"0.40",[99,24773,24774],{"x":863,"y":146,"fill":103,"style":126},"download 1.4s",[107,24776],{"x":3531,"y":142,"width":7852,"height":4630,"rx":876,"fill":2564,"opacity":24777},"0.35",[99,24779,24780],{"x":190,"y":146,"fill":103,"style":126},"render 0.9s",[99,24782,24783],{"x":3578,"y":5332,"fill":93,"style":2624},"Baseline waterfall — total LCP 3.4s. The download and discovery spans are the widest, so they get fixed first.",[99,24785,24786],{"x":3578,"y":5421,"fill":93,"style":2624},"After preload + AVIF + critical CSS, every span shrinks and total LCP falls to 1.6s.",[76,24788,78,24789,66],{},[80,24790,88,24792,78],{"id":24791,"viewBox":83,"refX":84,"refY":85,"markerWidth":86,"markerHeight":86,"orient":87},"lcp-arrow",[90,24793],{"d":92,"fill":93},[218,24795,24796],{},"LCP is the sum of four spans. Measure the waterfall, find the widest span, and apply the matching lever — discovery, download size, or render delay.",[34,24798,24800],{"id":24799},"step-1-identify-the-lcp-element","Step 1: Identify the LCP Element",[14,24802,24803],{},"You cannot optimize what you have not measured. Before touching markup, find the exact element the browser counts as the largest contentful paint, because the fix is different for an image, a heading, or a CSS background.",[14,24805,24806,24807,24810,24811,24814],{},"Lighthouse reports it directly. Run it against a deployed URL and open the ",[229,24808,24809],{},"Largest Contentful Paint element"," audit, which names the DOM node and the four sub-parts of LCP (TTFB, load delay, load time, render delay). For a programmatic check, drop a ",[253,24812,24813],{},"PerformanceObserver"," into the console:",[987,24816,24818],{"className":1600,"code":24817,"language":1602,"meta":712,"style":712},"new PerformanceObserver((list) => {\n  const entry = list.getEntries().at(-1);\n  console.log('LCP element:', entry.element, 'at', Math.round(entry.startTime), 'ms');\n}).observe({ type: 'largest-contentful-paint', buffered: true });\n",[253,24819,24820,24839,24867,24900],{"__ignoreMap":712},[995,24821,24822,24825,24828,24830,24833,24835,24837],{"class":997,"line":998},[995,24823,24824],{"class":1614},"new",[995,24826,24827],{"class":1007}," PerformanceObserver",[995,24829,1845],{"class":1618},[995,24831,24832],{"class":1784},"list",[995,24834,1811],{"class":1618},[995,24836,1858],{"class":1614},[995,24838,8802],{"class":1618},[995,24840,24841,24843,24846,24848,24851,24854,24856,24859,24861,24863,24865],{"class":997,"line":713},[995,24842,6270],{"class":1614},[995,24844,24845],{"class":1010}," entry",[995,24847,1775],{"class":1614},[995,24849,24850],{"class":1618}," list.",[995,24852,24853],{"class":1007},"getEntries",[995,24855,8988],{"class":1618},[995,24857,24858],{"class":1007},"at",[995,24860,1799],{"class":1618},[995,24862,1864],{"class":1614},[995,24864,11536],{"class":1010},[995,24866,5829],{"class":1618},[995,24868,24869,24872,24875,24877,24880,24883,24886,24889,24892,24895,24898],{"class":997,"line":730},[995,24870,24871],{"class":1618},"  console.",[995,24873,24874],{"class":1007},"log",[995,24876,1799],{"class":1618},[995,24878,24879],{"class":1023},"'LCP element:'",[995,24881,24882],{"class":1618},", entry.element, ",[995,24884,24885],{"class":1023},"'at'",[995,24887,24888],{"class":1618},", Math.",[995,24890,24891],{"class":1007},"round",[995,24893,24894],{"class":1618},"(entry.startTime), ",[995,24896,24897],{"class":1023},"'ms'",[995,24899,5829],{"class":1618},[995,24901,24902,24905,24908,24911,24914,24917,24919],{"class":997,"line":1544},[995,24903,24904],{"class":1618},"}).",[995,24906,24907],{"class":1007},"observe",[995,24909,24910],{"class":1618},"({ type: ",[995,24912,24913],{"class":1023},"'largest-contentful-paint'",[995,24915,24916],{"class":1618},", buffered: ",[995,24918,6283],{"class":1010},[995,24920,6500],{"class":1618},[14,24922,24923,24924,24926,24927,24930],{},"On our example page the LCP element was the hero ",[253,24925,3847],{},", paint timestamp ",[229,24928,24929],{},"3,400 ms",". WebPageTest confirmed it by marking the hero frame in the filmstrip. Knowing the element is an image — not text — told us the dominant cost was the download span, so that is where we started.",[433,24932,24933,24946],{},[436,24934,24935],{},[439,24936,24937,24940,24943],{},[442,24938,24939],{},"LCP element type",[442,24941,24942],{},"Most likely cost",[442,24944,24945],{},"Primary lever",[457,24947,24948,24965,24976],{},[439,24949,24950,24955,24958],{},[462,24951,24952,24953],{},"Hero ",[253,24954,3847],{},[462,24956,24957],{},"Download + late discovery",[462,24959,24960,24961,24964],{},"Preload, ",[253,24962,24963],{},"fetchpriority",", AVIF\u002FWebP",[439,24966,24967,24970,24973],{},[462,24968,24969],{},"CSS background image",[462,24971,24972],{},"Very late discovery",[462,24974,24975],{},"Preload (the parser can't see it in markup)",[439,24977,24978,24981,24984],{},[462,24979,24980],{},"Heading \u002F text block",[462,24982,24983],{},"Render delay from CSS + fonts",[462,24985,24986],{},"Inline critical CSS, preload font",[34,24988,24990],{"id":24989},"step-2-make-the-lcp-resource-discoverable-early","Step 2: Make the LCP Resource Discoverable Early",[14,24992,24993,24994,24996,24997,25000],{},"The single biggest waste in LCP is a late request. The browser's preload scanner finds resources by reading HTML, so an ",[253,24995,3847],{}," near the top is found quickly — but a CSS ",[253,24998,24999],{},"background-image",", an image injected by a script, or an image far down the DOM is discovered late, after the stylesheet that referenced it has already downloaded.",[14,25002,25003,25004,25006],{},"Two tools fix discovery. First, ",[253,25005,19090],{}," tells the browser this image matters more than the other images it found, so it gets a connection ahead of them:",[987,25008,25010],{"className":16196,"code":25009,"language":16198,"meta":712,"style":712},"\u003Cimg src=\"\u002Fhero.avif\" alt=\"Product dashboard\" width=\"1200\" height=\"600\"\n     fetchpriority=\"high\" \u002F>\n",[253,25011,25012,25046],{"__ignoreMap":712},[995,25013,25014,25016,25018,25020,25022,25025,25027,25029,25032,25035,25037,25039,25041,25043],{"class":997,"line":998},[995,25015,16205],{"class":1618},[995,25017,61],{"class":1921},[995,25019,17918],{"class":1007},[995,25021,7317],{"class":1618},[995,25023,25024],{"class":1023},"\"\u002Fhero.avif\"",[995,25026,17944],{"class":1007},[995,25028,7317],{"class":1618},[995,25030,25031],{"class":1023},"\"Product dashboard\"",[995,25033,25034],{"class":1007}," width",[995,25036,7317],{"class":1618},[995,25038,17933],{"class":1023},[995,25040,17936],{"class":1007},[995,25042,7317],{"class":1618},[995,25044,25045],{"class":1023},"\"600\"\n",[995,25047,25048,25051,25053,25056],{"class":997,"line":713},[995,25049,25050],{"class":1007},"     fetchpriority",[995,25052,7317],{"class":1618},[995,25054,25055],{"class":1023},"\"high\"",[995,25057,20528],{"class":1618},[14,25059,25060,25061,931],{},"Second, for resources the parser cannot see early — background images, or an image the framework outputs late — add an explicit preload in ",[253,25062,23970],{},[987,25064,25066],{"className":16196,"code":25065,"language":16198,"meta":712,"style":712},"\u003Clink rel=\"preload\" as=\"image\" href=\"\u002Fhero.avif\" fetchpriority=\"high\" \u002F>\n",[253,25067,25068],{"__ignoreMap":712},[995,25069,25070,25072,25074,25076,25078,25080,25082,25084,25086,25088,25090,25092,25095,25097,25099],{"class":997,"line":998},[995,25071,16205],{"class":1618},[995,25073,16208],{"class":1921},[995,25075,16211],{"class":1007},[995,25077,7317],{"class":1618},[995,25079,16216],{"class":1023},[995,25081,16227],{"class":1007},[995,25083,7317],{"class":1618},[995,25085,8250],{"class":1023},[995,25087,16219],{"class":1007},[995,25089,7317],{"class":1618},[995,25091,25024],{"class":1023},[995,25093,25094],{"class":1007}," fetchpriority",[995,25096,7317],{"class":1618},[995,25098,25055],{"class":1023},[995,25100,20528],{"class":1618},[14,25102,25103,25104,25106,25107,25110,25111,25114,25115,25118,25119,239],{},"In our measurement, adding ",[253,25105,19090],{}," plus a preload moved the hero request from ",[229,25108,25109],{},"910 ms to 120 ms"," after navigation start and cut LCP from ",[229,25112,25113],{},"3.4s to 2.7s"," — a 0.7s win with two lines of HTML. The Astro-specific mechanics (the ",[253,25116,25117],{},"priority"," prop, eager loading, and where preload belongs) are covered in ",[23,25120,19813],{"href":19812},[34,25122,25124],{"id":25123},"step-3-shrink-the-lcp-image-download","Step 3: Shrink the LCP Image Download",[14,25126,25127,25128,25130],{},"Discovery only helps if the file is small enough to arrive quickly. The download span is usually the widest part of LCP on an image-led page, and the fix is the same build-time work that drives the whole ",[23,25129,2190],{"href":2189}," approach: serve a modern format at the right size.",[39,25132,25133,25139,25148],{},[42,25134,25135,25138],{},[229,25136,25137],{},"Format:"," AVIF is typically 20-30% smaller than WebP at matched quality, and WebP is 25-50% smaller than JPEG. Emit both with a fallback.",[42,25140,25141,25144,25145,25147],{},[229,25142,25143],{},"Dimensions:"," never ship a 2400px source into a 1200px slot. Generate the widths the layout actually uses and let ",[253,25146,3720],{}," pick.",[42,25149,25150,692,25153,25155],{},[229,25151,25152],{},"Never lazy-load the hero:",[253,25154,4897],{}," on the LCP image defers it behind layout, which is the opposite of what you want.",[987,25157,25159],{"className":16196,"code":25158,"language":16198,"meta":712,"style":712},"\u003Cpicture>\n  \u003Csource type=\"image\u002Favif\" srcset=\"\u002Fhero-800.avif 800w, \u002Fhero-1200.avif 1200w\"\n          sizes=\"(max-width: 800px) 100vw, 1200px\" \u002F>\n  \u003Csource type=\"image\u002Fwebp\" srcset=\"\u002Fhero-800.webp 800w, \u002Fhero-1200.webp 1200w\"\n          sizes=\"(max-width: 800px) 100vw, 1200px\" \u002F>\n  \u003Cimg src=\"\u002Fhero-1200.jpg\" alt=\"Product dashboard\" width=\"1200\" height=\"600\"\n       fetchpriority=\"high\" decoding=\"async\" \u002F>\n\u003C\u002Fpicture>\n",[253,25160,25161,25170,25192,25204,25224,25234,25265,25284],{"__ignoreMap":712},[995,25162,25163,25165,25168],{"class":997,"line":998},[995,25164,16205],{"class":1618},[995,25166,25167],{"class":1921},"picture",[995,25169,16246],{"class":1618},[995,25171,25172,25174,25177,25179,25181,25184,25187,25189],{"class":997,"line":713},[995,25173,24116],{"class":1618},[995,25175,25176],{"class":1921},"source",[995,25178,16235],{"class":1007},[995,25180,7317],{"class":1618},[995,25182,25183],{"class":1023},"\"image\u002Favif\"",[995,25185,25186],{"class":1007}," srcset",[995,25188,7317],{"class":1618},[995,25190,25191],{"class":1023},"\"\u002Fhero-800.avif 800w, \u002Fhero-1200.avif 1200w\"\n",[995,25193,25194,25197,25199,25202],{"class":997,"line":730},[995,25195,25196],{"class":1007},"          sizes",[995,25198,7317],{"class":1618},[995,25200,25201],{"class":1023},"\"(max-width: 800px) 100vw, 1200px\"",[995,25203,20528],{"class":1618},[995,25205,25206,25208,25210,25212,25214,25217,25219,25221],{"class":997,"line":1544},[995,25207,24116],{"class":1618},[995,25209,25176],{"class":1921},[995,25211,16235],{"class":1007},[995,25213,7317],{"class":1618},[995,25215,25216],{"class":1023},"\"image\u002Fwebp\"",[995,25218,25186],{"class":1007},[995,25220,7317],{"class":1618},[995,25222,25223],{"class":1023},"\"\u002Fhero-800.webp 800w, \u002Fhero-1200.webp 1200w\"\n",[995,25225,25226,25228,25230,25232],{"class":997,"line":1550},[995,25227,25196],{"class":1007},[995,25229,7317],{"class":1618},[995,25231,25201],{"class":1023},[995,25233,20528],{"class":1618},[995,25235,25236,25238,25240,25242,25244,25247,25249,25251,25253,25255,25257,25259,25261,25263],{"class":997,"line":1673},[995,25237,24116],{"class":1618},[995,25239,61],{"class":1921},[995,25241,17918],{"class":1007},[995,25243,7317],{"class":1618},[995,25245,25246],{"class":1023},"\"\u002Fhero-1200.jpg\"",[995,25248,17944],{"class":1007},[995,25250,7317],{"class":1618},[995,25252,25031],{"class":1023},[995,25254,25034],{"class":1007},[995,25256,7317],{"class":1618},[995,25258,17933],{"class":1023},[995,25260,17936],{"class":1007},[995,25262,7317],{"class":1618},[995,25264,25045],{"class":1023},[995,25266,25267,25270,25272,25274,25277,25279,25282],{"class":997,"line":1678},[995,25268,25269],{"class":1007},"       fetchpriority",[995,25271,7317],{"class":1618},[995,25273,25055],{"class":1023},[995,25275,25276],{"class":1007}," decoding",[995,25278,7317],{"class":1618},[995,25280,25281],{"class":1023},"\"async\"",[995,25283,20528],{"class":1618},[995,25285,25286,25288,25290],{"class":997,"line":1693},[995,25287,24126],{"class":1618},[995,25289,25167],{"class":1921},[995,25291,16246],{"class":1618},[433,25293,25294,25305],{},[436,25295,25296],{},[439,25297,25298,25301,25303],{},[442,25299,25300],{},"Hero source (1200px)",[442,25302,18725],{},[442,25304,10936],{},[457,25306,25307,25316,25324],{},[439,25308,25309,25311,25313],{},[462,25310,18734],{},[462,25312,11036],{},[462,25314,25315],{},"2.7s",[439,25317,25318,25320,25322],{},[462,25319,18743],{},[462,25321,18746],{},[462,25323,18749],{},[439,25325,25326,25328,25330],{},[462,25327,18754],{},[462,25329,18155],{},[462,25331,2169],{},[14,25333,25334,25335,25338,25339,25341,25342,25344,25345,239],{},"Swapping the 1.4 MB JPEG for a 180 KB AVIF cut the download span from ",[229,25336,25337],{},"1.4s to 0.3s"," and brought LCP to ",[229,25340,2169],{},". The full hero recipe — sizing, ",[253,25343,3720],{},", and the lazy-loading trap — is in ",[23,25346,24562],{"href":24561},[34,25348,25350],{"id":25349},"step-4-remove-render-blocking-css","Step 4: Remove Render-Blocking CSS",[14,25352,25353,25354,25356],{},"Even a tiny LCP element cannot paint until the CSS that styles the page has loaded and parsed. A single external stylesheet in ",[253,25355,23970],{}," blocks the first paint for an entire network round trip. The fix is to inline the small slice of CSS needed to render the above-the-fold view and load the rest non-blocking:",[987,25358,25360],{"className":16196,"code":25359,"language":16198,"meta":712,"style":712},"\u003Chead>\n  \u003Cstyle>\u002F* critical CSS: layout, hero, typography for the first viewport *\u002F\u003C\u002Fstyle>\n  \u003Clink rel=\"preload\" href=\"\u002Fstyles.css\" as=\"style\"\n        onload=\"this.onload=null;this.rel='stylesheet'\" \u002F>\n  \u003Cnoscript>\u003Clink rel=\"stylesheet\" href=\"\u002Fstyles.css\" \u002F>\u003C\u002Fnoscript>\n\u003C\u002Fhead>\n",[253,25361,25362,25370,25387,25411,25443,25471],{"__ignoreMap":712},[995,25363,25364,25366,25368],{"class":997,"line":998},[995,25365,16205],{"class":1618},[995,25367,24109],{"class":1921},[995,25369,16246],{"class":1618},[995,25371,25372,25374,25376,25378,25381,25383,25385],{"class":997,"line":713},[995,25373,24116],{"class":1618},[995,25375,1346],{"class":1921},[995,25377,20929],{"class":1618},[995,25379,25380],{"class":1001},"\u002F* critical CSS: layout, hero, typography for the first viewport *\u002F",[995,25382,24126],{"class":1618},[995,25384,1346],{"class":1921},[995,25386,16246],{"class":1618},[995,25388,25389,25391,25393,25395,25397,25399,25401,25403,25405,25407,25409],{"class":997,"line":730},[995,25390,24116],{"class":1618},[995,25392,16208],{"class":1921},[995,25394,16211],{"class":1007},[995,25396,7317],{"class":1618},[995,25398,16216],{"class":1023},[995,25400,16219],{"class":1007},[995,25402,7317],{"class":1618},[995,25404,24149],{"class":1023},[995,25406,16227],{"class":1007},[995,25408,7317],{"class":1618},[995,25410,24156],{"class":1023},[995,25412,25413,25415,25417,25419,25421,25423,25425,25427,25429,25431,25433,25435,25437,25439,25441],{"class":997,"line":1544},[995,25414,24161],{"class":1007},[995,25416,7317],{"class":1618},[995,25418,18873],{"class":1023},[995,25420,24168],{"class":1010},[995,25422,239],{"class":1023},[995,25424,24173],{"class":1618},[995,25426,7317],{"class":1614},[995,25428,22918],{"class":1010},[995,25430,24180],{"class":1023},[995,25432,24168],{"class":1010},[995,25434,239],{"class":1023},[995,25436,24187],{"class":1618},[995,25438,7317],{"class":1614},[995,25440,24192],{"class":1023},[995,25442,20528],{"class":1618},[995,25444,25445,25447,25449,25451,25453,25455,25457,25459,25461,25463,25465,25467,25469],{"class":997,"line":1550},[995,25446,24116],{"class":1618},[995,25448,24201],{"class":1921},[995,25450,24204],{"class":1618},[995,25452,16208],{"class":1921},[995,25454,16211],{"class":1007},[995,25456,7317],{"class":1618},[995,25458,24213],{"class":1023},[995,25460,16219],{"class":1007},[995,25462,7317],{"class":1618},[995,25464,24149],{"class":1023},[995,25466,24222],{"class":1618},[995,25468,24201],{"class":1921},[995,25470,16246],{"class":1618},[995,25472,25473,25475,25477],{"class":997,"line":1673},[995,25474,24126],{"class":1618},[995,25476,24109],{"class":1921},[995,25478,16246],{"class":1618},[14,25480,25481,25482,25485,25486,25489,25490,239],{},"On our page the external stylesheet was 78 KB and blocked first paint for ",[229,25483,25484],{},"~0.6s",". Inlining a 9 KB critical slice and deferring the rest moved First Contentful Paint earlier and shaved the LCP render delay, contributing the final step from ",[229,25487,25488],{},"1.7s to 1.6s"," while improving FCP more dramatically (2.1s to 0.9s). The full critical-CSS and purge workflow — including how to generate the critical slice and prune unused rules — is in ",[23,25491,23959],{"href":24649},[34,25493,25495],{"id":25494},"step-5-keep-fonts-from-delaying-the-paint","Step 5: Keep Fonts From Delaying the Paint",[14,25497,25498,25499,25501],{},"When the LCP element is a text block, web fonts become the bottleneck. With ",[253,25500,16740],{}," the browser paints fallback text first and repaints when the web font arrives; if the LCP text uses the web font, its measured paint can move to that later repaint. Two disciplines keep text LCP early:",[987,25503,25505],{"className":16196,"code":25504,"language":16198,"meta":712,"style":712},"\u003Clink rel=\"preload\" as=\"font\" type=\"font\u002Fwoff2\"\n      href=\"\u002Ffonts\u002Finter-var.woff2\" crossorigin \u002F>\n",[253,25506,25507,25532],{"__ignoreMap":712},[995,25508,25509,25511,25513,25515,25517,25519,25521,25523,25525,25527,25529],{"class":997,"line":998},[995,25510,16205],{"class":1618},[995,25512,16208],{"class":1921},[995,25514,16211],{"class":1007},[995,25516,7317],{"class":1618},[995,25518,16216],{"class":1023},[995,25520,16227],{"class":1007},[995,25522,7317],{"class":1618},[995,25524,16232],{"class":1023},[995,25526,16235],{"class":1007},[995,25528,7317],{"class":1618},[995,25530,25531],{"class":1023},"\"font\u002Fwoff2\"\n",[995,25533,25534,25537,25539,25542,25544],{"class":997,"line":713},[995,25535,25536],{"class":1007},"      href",[995,25538,7317],{"class":1618},[995,25540,25541],{"class":1023},"\"\u002Ffonts\u002Finter-var.woff2\"",[995,25543,16243],{"class":1007},[995,25545,20528],{"class":1618},[987,25547,25549],{"className":16083,"code":25548,"language":16085,"meta":712,"style":712},"@font-face {\n  font-family: 'Inter';\n  src: url('\u002Ffonts\u002Finter-var.woff2') format('woff2');\n  font-display: swap;\n  size-adjust: 102%; \u002F* metric-matched fallback to avoid reflow *\u002F\n}\n",[253,25550,25551,25557,25567,25590,25600,25615],{"__ignoreMap":712},[995,25552,25553,25555],{"class":997,"line":998},[995,25554,16092],{"class":1614},[995,25556,8802],{"class":1618},[995,25558,25559,25561,25563,25565],{"class":997,"line":713},[995,25560,16099],{"class":1010},[995,25562,1925],{"class":1618},[995,25564,16104],{"class":1023},[995,25566,1628],{"class":1618},[995,25568,25569,25571,25573,25575,25577,25580,25582,25584,25586,25588],{"class":997,"line":730},[995,25570,16111],{"class":1010},[995,25572,1925],{"class":1618},[995,25574,16116],{"class":1010},[995,25576,1799],{"class":1618},[995,25578,25579],{"class":1023},"'\u002Ffonts\u002Finter-var.woff2'",[995,25581,1811],{"class":1618},[995,25583,16126],{"class":1010},[995,25585,1799],{"class":1618},[995,25587,16131],{"class":1023},[995,25589,5829],{"class":1618},[995,25591,25592,25594,25596,25598],{"class":997,"line":1544},[995,25593,16156],{"class":1010},[995,25595,1925],{"class":1618},[995,25597,16161],{"class":1010},[995,25599,1628],{"class":1618},[995,25601,25602,25604,25606,25608,25610,25612],{"class":997,"line":1550},[995,25603,16468],{"class":1010},[995,25605,1925],{"class":1618},[995,25607,3485],{"class":1010},[995,25609,16476],{"class":1614},[995,25611,18846],{"class":1618},[995,25613,25614],{"class":1001},"\u002F* metric-matched fallback to avoid reflow *\u002F\n",[995,25616,25617],{"class":997,"line":1673},[995,25618,9008],{"class":1618},[14,25620,25621,25622,25624],{},"Preload the one weight that appears above the fold, self-host it to avoid a third-party connection, and use a metric-matched fallback so the swap does not shift layout. The full font workflow lives in ",[23,25623,14061],{"href":14060},". On text-led pages, preloading the hero weight typically moves LCP earlier by 200-400 ms because the heading paints in its final font on the first paint.",[34,25626,2266],{"id":2265},[39,25628,25629,25635,25643,25650,25656],{},[42,25630,25631,25634],{},[229,25632,25633],{},"Preloading the wrong resource:"," a preload for an image the page does not actually use as its LCP wastes bandwidth and can delay the real LCP resource. Always re-run Lighthouse after adding a preload.",[42,25636,25637,25642],{},[229,25638,25639,25641],{},[253,25640,19090],{}," on many images:"," if everything is high priority, nothing is. Reserve it for the single LCP element.",[42,25644,25645,692,25647,25649],{},[229,25646,19081],{},[253,25648,4897],{}," on the above-the-fold image is the most common self-inflicted LCP regression on static sites.",[42,25651,25652,25655],{},[229,25653,25654],{},"Critical CSS drift:"," generated critical CSS goes stale when the design changes. Regenerate it in the build, not by hand, or it will block paint with rules the page no longer needs.",[42,25657,25658,25661],{},[229,25659,25660],{},"Counting lab numbers as field numbers:"," a great Lighthouse LCP can still be poor in the field if real users are on slower networks. Confirm with RUM at the 75th percentile.",[34,25663,2321],{"id":2320},[39,25665,25666,25669,25672,25678,25681],{},[42,25667,25668],{},"LCP is four spans — TTFB, discovery, download, render delay. Measure the waterfall and fix the widest first.",[42,25670,25671],{},"Identify the LCP element before optimizing; the fix differs for an image, a background, or text.",[42,25673,25674,25675,25677],{},"Make the LCP resource discoverable early with ",[253,25676,19090],{}," and a targeted preload.",[42,25679,25680],{},"Shrink the image with AVIF\u002FWebP at the right width, and never lazy-load the hero.",[42,25682,25683],{},"Inline critical CSS and preload the above-the-fold font so render delay does not gate the paint.",[34,25685,651],{"id":650},[653,25687,25689],{"id":25688},"what-is-a-good-lcp-target-for-a-static-site","What is a good LCP target for a static site?",[14,25691,25692],{},"Aim for 2.5 seconds or less at the 75th percentile of real users, which is the \"good\" threshold Google uses. On a pre-rendered static page with an optimized hero you can usually reach 1.5 to 2.0 seconds in the lab on a throttled mobile profile, which leaves headroom for slow real-world networks.",[653,25694,25696],{"id":25695},"how-do-i-find-which-element-is-the-lcp-element","How do I find which element is the LCP element?",[14,25698,25699,25700,25702,25703,25705],{},"Run Lighthouse and open the \"Largest Contentful Paint element\" audit, or use the ",[253,25701,24813],{}," API for ",[253,25704,16666],{}," entries in the browser console. WebPageTest also marks the LCP frame in its filmstrip. The element is most often a hero image, a heading, or a background image.",[653,25707,25709],{"id":25708},"does-preloading-the-lcp-image-always-help","Does preloading the LCP image always help?",[14,25711,25712,25713,25715,25716,25718],{},"It helps when the image is discovered late — for example a CSS background image or an image deep in the DOM. If the image is already an early ",[253,25714,3847],{}," tag with ",[253,25717,19090],{},", an extra preload adds little and can waste bandwidth on the wrong resource, so always measure before and after.",[653,25720,25722],{"id":25721},"why-does-css-affect-lcp-if-my-content-is-just-text","Why does CSS affect LCP if my content is just text?",[14,25724,25725],{},"Render-blocking CSS delays first paint entirely. The browser will not paint the LCP text block until the stylesheet that governs it has loaded and parsed. Inlining the critical CSS lets the first paint happen on the initial HTML response instead of waiting for a separate round trip.",[653,25727,25729],{"id":25728},"can-web-fonts-make-lcp-worse","Can web fonts make LCP worse?",[14,25731,25732,25733,25735],{},"Yes, when the LCP element is a text block. With ",[253,25734,16740],{}," the browser may delay painting that text or repaint it when the web font arrives, and the LCP timestamp moves to the later paint. Preloading the one above-the-fold weight and using a metric-matched fallback keeps the text paint early and stable.",[653,25737,25739],{"id":25738},"is-lcp-a-problem-on-static-sites-at-all-given-they-pre-render-html","Is LCP a problem on static sites at all, given they pre-render HTML?",[14,25741,25742],{},"Pre-rendering gives you a strong starting point because the markup arrives in one response, but the LCP resource itself — a hero image, a font, or a stylesheet — still has to download and render. Those are exactly the things this guide controls, which is why measured static sites still move from roughly 3 seconds to under 2 seconds after this work.",[34,25744,684],{"id":683},[39,25746,25747,25754,25764,25769,25774],{},[42,25748,25749,692,25751,25753],{},[229,25750,691],{},[23,25752,5501],{"href":5500}," — where LCP fits among the Core Web Vitals.",[42,25755,25756,7242,25758,25760,25761,25763],{},[23,25757,19813],{"href":19812},[253,25759,24963],{},", preload, and Astro's ",[253,25762,25117],{}," prop.",[42,25765,25766,25768],{},[23,25767,24562],{"href":24561}," — sizing, format, and the lazy-loading trap.",[42,25770,25771,25773],{},[23,25772,23959],{"href":24649}," — inline critical CSS and defer the rest.",[42,25775,25776,25778],{},[23,25777,2190],{"href":2189}," — the build-time image work behind the download span.",[1346,25780,25781],{},"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 .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}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}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}",{"title":712,"searchDepth":713,"depth":713,"links":25783},[25784,25785,25786,25787,25788,25789,25790,25791,25799],{"id":24799,"depth":713,"text":24800},{"id":24989,"depth":713,"text":24990},{"id":25123,"depth":713,"text":25124},{"id":25349,"depth":713,"text":25350},{"id":25494,"depth":713,"text":25495},{"id":2265,"depth":713,"text":2266},{"id":2320,"depth":713,"text":2321},{"id":650,"depth":713,"text":651,"children":25792},[25793,25794,25795,25796,25797,25798],{"id":25688,"depth":730,"text":25689},{"id":25695,"depth":730,"text":25696},{"id":25708,"depth":730,"text":25709},{"id":25721,"depth":730,"text":25722},{"id":25728,"depth":730,"text":25729},{"id":25738,"depth":730,"text":25739},{"id":683,"depth":713,"text":684},[25801,25802,25803],{"name":737,"item":738},{"name":5501,"item":5500},{"name":23975,"item":23974},"A complete approach to LCP on static sites — identify the LCP element, preload it, set fetchpriority, serve modern formats, and remove render-blocking CSS and fonts.",[25806,25807,25809,25811,25812,25814],{"q":25689,"a":25692},{"q":25696,"a":25808},"Run Lighthouse and open the \"Largest Contentful Paint element\" audit, or use the PerformanceObserver API for largest-contentful-paint entries in the browser console. WebPageTest also marks the LCP frame in its filmstrip. The element is most often a hero image, a heading, or a background image.",{"q":25709,"a":25810},"It helps when the image is discovered late — for example a CSS background image or an image deep in the DOM. If the image is already an early img tag with fetchpriority high, an extra preload adds little and can waste bandwidth on the wrong resource, so always measure before and after.",{"q":25722,"a":25725},{"q":25729,"a":25813},"Yes, when the LCP element is a text block. With font-display swap the browser may delay painting that text or repaint it when the web font arrives, and the LCP timestamp moves to the later paint. Preloading the one above-the-fold weight and using a metric-matched fallback keeps the text paint early and stable.",{"q":25739,"a":25742},{},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Flargest-contentful-paint-optimization-for-static-sites",{"title":23975,"description":25804},"performance-optimization-core-web-vitals-for-ssgs\u002Flargest-contentful-paint-optimization-for-static-sites\u002Findex","ZPilPF0QHV9cQrNbjfl7_xWT4f4CwIq7Q49ygcE9A4U",{"id":25821,"title":19813,"body":25822,"breadcrumb":26375,"dateModified":743,"datePublished":743,"description":26380,"extension":745,"faq":26381,"meta":26389,"navigation":752,"path":26390,"seo":26391,"slug":25826,"stem":26392,"type":756,"__hash__":26393},"content\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Flargest-contentful-paint-optimization-for-static-sites\u002Foptimizing-lcp-on-astro-with-priority-hints\u002Findex.md",{"type":7,"value":25823,"toc":26356},[25824,25827,25837,25839,25858,25942,25944,25951,25964,26023,26042,26046,26061,26099,26106,26110,26125,26127,26130,26192,26209,26211,26243,26245,26265,26267,26271,26293,26297,26303,26307,26316,26320,26323,26325,26354],[10,25825,19813],{"id":25826},"optimizing-lcp-on-astro-with-priority-hints",[14,25828,25829,25830,25832,25833,27,25835,32],{},"Astro pre-renders HTML, so the hero image is usually present in the markup from the first byte. What still goes wrong is ",[229,25831,25117],{},": the browser finds the hero alongside every other image and CSS request and gives it no special treatment, so it queues behind less important resources. Priority hints fix this by telling the browser which single resource matters most. This is the Astro-specific recipe for the discovery step in ",[23,25834,23975],{"href":23974},[23,25836,5501],{"href":5500},[34,25838,37],{"id":36},[39,25840,25841,25849,25852],{},[42,25842,25843,25844,25846,25847,239],{},"An Astro project (v3 or later) using ",[253,25845,2117],{}," for images. If you have not set that up, start with ",[23,25848,2190],{"href":2189},[42,25850,25851],{},"A deployed URL (a preview deploy is fine) so you can measure real headers and waterfalls — local dev does not reproduce edge timing.",[42,25853,25854,25855,25857],{},"Lighthouse (or ",[253,25856,16647],{},") and access to a WebPageTest run for the network waterfall.",[55,25859,25860,25939],{},[58,25861,66,25865,66,25868,66,25871,66,25873,66,25932],{"viewBox":20093,"role":61,"ariaLabelledBy":25862,"xmlns":65},[25863,25864],"ph-order-title","ph-order-desc",[68,25866,25867],{"id":25863},"Request order before and after priority hints",[72,25869,25870],{"id":25864},"Two stacked request waterfalls. Before, the hero image starts late behind CSS and other images. After, fetchpriority high and a preload move the hero request to the front so it finishes far sooner.",[107,25872],{"x":2515,"y":2515,"width":8298,"height":1463,"fill":205},[95,25874,78,25875,78,25878,78,25881,78,25883,78,25887,78,25889,78,25892,78,25894,78,25897,78,25900,78,25902,78,25906,78,25909,78,25912,78,25914,78,25917,78,25919,78,25921,78,25923,78,25929,66],{"style":97},[99,25876,25877],{"x":101,"y":102,"fill":103,"style":104},"Hero request order: default vs priority hint",[99,25879,25880],{"x":5393,"y":849,"fill":2565,"style":2597},"Before",[107,25882],{"x":873,"y":3559,"width":1431,"height":5393,"rx":468,"fill":824,"opacity":4644},[99,25884,25886],{"x":161,"y":25885,"fill":103,"style":4658},"73","styles.css",[107,25888],{"x":873,"y":120,"width":161,"height":5393,"rx":468,"fill":114,"opacity":24777},[99,25890,25891],{"x":16983,"y":10974,"fill":103,"style":4658},"logo + icons",[107,25893],{"x":112,"y":159,"width":142,"height":5393,"rx":468,"fill":2564,"opacity":5411},[99,25895,25896],{"x":215,"y":6848,"fill":103,"style":4658},"hero.avif (LCP)",[99,25898,25899],{"x":4696,"y":6848,"fill":93,"style":11285},"LCP 2.7s",[997,25901],{"x1":873,"y1":161,"x2":23289,"y2":161,"stroke":2592,"style":2602},[99,25903,25905],{"x":5393,"y":25904,"fill":187,"style":2597},"195","After",[107,25907],{"x":873,"y":25908,"width":142,"height":5393,"rx":468,"fill":185,"opacity":4631},"183",[99,25910,25911],{"x":175,"y":12813,"fill":103,"style":4658},"hero.avif (preload, high)",[99,25913,10806],{"x":158,"y":12813,"fill":93,"style":11285},[107,25915],{"x":873,"y":25916,"width":1431,"height":5393,"rx":468,"fill":824,"opacity":4644},"209",[99,25918,25886],{"x":161,"y":1437,"fill":103,"style":4658},[107,25920],{"x":873,"y":20836,"width":161,"height":5393,"rx":468,"fill":114,"opacity":24777},[99,25922,25891],{"x":16983,"y":112,"fill":103,"style":4658},[95,25924,88,25925,78],{"stroke":93,"fill":205,"style":116},[90,25926],{"d":25927,"style":25928},"M450 120 L470 150 L300 183","marker-end:url(#ph-arrow)",[99,25930,25931],{"x":101,"y":1462,"fill":93,"style":126},"Earlier in the waterfall means an earlier paint — the hero is no longer waiting behind low-value requests.",[76,25933,78,25934,66],{},[80,25935,88,25937,78],{"id":25936,"viewBox":83,"refX":84,"refY":85,"markerWidth":86,"markerHeight":86,"orient":87},"ph-arrow",[90,25938],{"d":92,"fill":93},[218,25940,25941],{},"The default order leaves the hero queued behind CSS and decorative images; a preload plus fetchpriority high moves it to the front of the waterfall.",[34,25943,19484],{"id":19483},[653,25945,25947,25948,25950],{"id":25946},"_1-mark-the-hero-with-the-priority-prop","1. Mark the hero with the ",[253,25949,25117],{}," prop",[14,25952,20950,25953,25955,25956,25958,25959,1850,25961,25963],{},[253,25954,18510],{}," component ships a ",[253,25957,25117],{}," shorthand. Setting it flips three things at once: ",[253,25960,19087],{},[253,25962,19090],{},", and no lazy loading. Use it on the hero only:",[987,25965,25967],{"className":10854,"code":25966,"language":10856,"meta":712,"style":712},"---\nimport { Image } from 'astro:assets';\nimport hero from '..\u002Fassets\u002Fhero.jpg';\n---\n\u003CImage\n  src={hero}\n  alt=\"Product dashboard overview\"\n  widths={[800, 1200]}\n  sizes=\"(max-width: 800px) 100vw, 1200px\"\n  format=\"avif\"\n  priority\n\u002F>\n",[253,25968,25969,25973,25978,25982,25986,25991,25995,26000,26005,26009,26014,26019],{"__ignoreMap":712},[995,25970,25971],{"class":997,"line":998},[995,25972,8106],{},[995,25974,25975],{"class":997,"line":713},[995,25976,25977],{},"import { Image } from 'astro:assets';\n",[995,25979,25980],{"class":997,"line":730},[995,25981,18548],{},[995,25983,25984],{"class":997,"line":1544},[995,25985,8106],{},[995,25987,25988],{"class":997,"line":1550},[995,25989,25990],{},"\u003CImage\n",[995,25992,25993],{"class":997,"line":1673},[995,25994,18562],{},[995,25996,25997],{"class":997,"line":1678},[995,25998,25999],{},"  alt=\"Product dashboard overview\"\n",[995,26001,26002],{"class":997,"line":1693},[995,26003,26004],{},"  widths={[800, 1200]}\n",[995,26006,26007],{"class":997,"line":1705},[995,26008,18577],{},[995,26010,26011],{"class":997,"line":1711},[995,26012,26013],{},"  format=\"avif\"\n",[995,26015,26016],{"class":997,"line":1717},[995,26017,26018],{},"  priority\n",[995,26020,26021],{"class":997,"line":1726},[995,26022,18592],{},[14,26024,26025,26026,26028,26029,270,26031,26033,26034,26036,26037,738,26039,26041],{},"The rendered ",[253,26027,3847],{}," carries ",[253,26030,19090],{},[253,26032,19087],{},", so the browser fetches it ahead of the default-priority images it discovers later. Because ",[253,26035,18510],{}," also emits explicit ",[253,26038,9286],{},[253,26040,18059],{},", it reserves layout space and avoids the shift that would otherwise hurt CLS.",[653,26043,26045],{"id":26044},"_2-add-a-preload-only-when-discovery-is-late","2. Add a preload only when discovery is late",[14,26047,8896,26048,26050,26051,26053,26054,26057,26058,26060],{},[253,26049,25117],{}," prop does ",[229,26052,3112],{}," emit a ",[253,26055,26056],{},"\u003Clink rel=\"preload\">",". If your hero is a CSS ",[253,26059,24999],{},", or rendered by a component that the preload scanner reaches late, add the preload by hand in the document head:",[987,26062,26064],{"className":10854,"code":26063,"language":10856,"meta":712,"style":712},"---\n\u002F\u002F src\u002Flayouts\u002FBase.astro\n---\n\u003Chead>\n  \u003Clink rel=\"preload\" as=\"image\" href=\"\u002F_astro\u002Fhero.HASH.avif\"\n        fetchpriority=\"high\" \u002F>\n\u003C\u002Fhead>\n",[253,26065,26066,26070,26075,26079,26084,26089,26094],{"__ignoreMap":712},[995,26067,26068],{"class":997,"line":998},[995,26069,8106],{},[995,26071,26072],{"class":997,"line":713},[995,26073,26074],{},"\u002F\u002F src\u002Flayouts\u002FBase.astro\n",[995,26076,26077],{"class":997,"line":730},[995,26078,8106],{},[995,26080,26081],{"class":997,"line":1544},[995,26082,26083],{},"\u003Chead>\n",[995,26085,26086],{"class":997,"line":1550},[995,26087,26088],{},"  \u003Clink rel=\"preload\" as=\"image\" href=\"\u002F_astro\u002Fhero.HASH.avif\"\n",[995,26090,26091],{"class":997,"line":1673},[995,26092,26093],{},"        fetchpriority=\"high\" \u002F>\n",[995,26095,26096],{"class":997,"line":1678},[995,26097,26098],{},"\u003C\u002Fhead>\n",[14,26100,26101,26102,26105],{},"For a normal ",[253,26103,26104],{},"\u003CImage priority>"," that the parser already finds at the top of the body, skip the preload — it would duplicate a request the browser is already prioritizing. Reach for it only when the network panel shows the hero starting late.",[653,26107,26109],{"id":26108},"_3-keep-priority-exclusive","3. Keep priority exclusive",[14,26111,26112,26114,26115,26118,26119,26121,26122,26124],{},[253,26113,19090],{}," is relative. If the logo, three feature thumbnails, and the hero are all ",[253,26116,26117],{},"high",", the browser cannot tell which one to fetch first and the hint does nothing. Leave every non-LCP image at default (or ",[253,26120,4897],{}," if below the fold) so the hero is the only ",[253,26123,26117],{}," request on the page.",[34,26126,1166],{"id":1165},[14,26128,26129],{},"Measured on a documentation landing page deployed to a CDN, throttled mobile profile (4x CPU, ~1.6 Mbps), median of five Lighthouse runs:",[433,26131,26132,26145],{},[436,26133,26134],{},[439,26135,26136,26138,26141,26143],{},[442,26137,24440],{},[442,26139,26140],{},"Hero request start",[442,26142,20362],{},[442,26144,24446],{},[457,26146,26147,26162,26178],{},[439,26148,26149,26155,26158,26160],{},[462,26150,26151,26152,26154],{},"Baseline (",[253,26153,18510],{},", no hints)",[462,26156,26157],{},"910 ms",[462,26159,25315],{},[462,26161,18749],{},[439,26163,26164,26172,26174,26176],{},[462,26165,26166,26169,26170,982],{},[253,26167,26168],{},"+ priority"," (eager + ",[253,26171,24963],{},[462,26173,21142],{},[462,26175,17538],{},[462,26177,2183],{},[439,26179,26180,26186,26188,26190],{},[462,26181,26182,26185],{},[253,26183,26184],{},"+ rel=preload"," (late-discovery hero)",[462,26187,22023],{},[462,26189,10939],{},[462,26191,2183],{},[14,26193,26194,26195,26198,26199,26202,26203,8912,26206,26208],{},"The hint moved the hero request from ",[229,26196,26197],{},"910 ms to 240 ms"," after navigation start, and LCP fell from ",[229,26200,26201],{},"2.7s to 2.0s",". Adding the preload (this page's hero was set via CSS, so the scanner found it late) shaved another ",[229,26204,26205],{},"0.4s",[229,26207,10939],{},". WebPageTest confirmed the hero moved to the front of the waterfall in both filmstrip and request log.",[34,26210,600],{"id":599},[39,26212,26213,26221,26229,26235],{},[42,26214,26215,26220],{},[229,26216,26217,26219],{},[253,26218,25117],{}," on multiple images:"," the most common mistake. It dilutes the signal; one hero only.",[42,26222,26223,26226,26227,239],{},[229,26224,26225],{},"Preload pointing at a stale hash:"," Astro fingerprints asset filenames, so a hard-coded preload URL breaks on the next build. Generate the URL from the imported asset or omit the preload and rely on ",[253,26228,25117],{},[42,26230,26231,26234],{},[229,26232,26233],{},"Preloading an off-screen image:"," preloading a resource that is not the LCP element wastes bandwidth and can delay the real LCP. Confirm with the Lighthouse LCP element audit.",[42,26236,26237,26239,26240,26242],{},[229,26238,637],{}," removing the ",[253,26241,25117],{}," prop and any manual preload line reverts the behavior completely — there is no cache or build state to clear. Re-run Lighthouse to confirm you are back to baseline.",[34,26244,642],{"id":641},[14,26246,26247,26248,26250,26251,26254,26255,26258,26259,26261,26262,26264],{},"Priority hints are the cheapest LCP win available to an Astro site: one ",[253,26249,25117],{}," prop on the hero, and a ",[253,26252,26253],{},"rel=preload"," only when the hero is discovered late. On the example page they cut LCP from ",[229,26256,26257],{},"2.7s to 1.6s"," without changing a single byte of the image. Pair them with format and sizing work in ",[23,26260,24562],{"href":24561}," and the render-delay fixes in ",[23,26263,23959],{"href":24649}," for the full LCP picture.",[34,26266,651],{"id":650},[653,26268,26270],{"id":26269},"what-does-the-astro-image-priority-prop-actually-do","What does the Astro Image priority prop actually do?",[14,26272,8896,26273,26275,26276,26278,26279,8912,26282,270,26285,8912,26287,26289,26290,26292],{},[253,26274,25117],{}," prop on Astro's ",[253,26277,18510],{}," component sets ",[253,26280,26281],{},"loading",[253,26283,26284],{},"eager",[253,26286,24963],{},[253,26288,26117],{},", and skips lazy loading, in one shorthand. It does not emit a preload link, so for a CSS background or a late-discovered image you still add a manual ",[253,26291,26253],{}," in the head.",[653,26294,26296],{"id":26295},"should-i-use-fetchpriority-high-on-more-than-one-image","Should I use fetchpriority high on more than one image?",[14,26298,26299,26300,26302],{},"No. Priority is relative, so marking several images high tells the browser they are all equally important and the benefit disappears. Reserve ",[253,26301,19090],{}," for the single LCP element and let everything else load at default or lazy priority.",[653,26304,26306],{"id":26305},"do-i-need-both-a-preload-link-and-fetchpriority-high","Do I need both a preload link and fetchpriority high?",[14,26308,26309,26310,26312,26313,26315],{},"Not always. If the hero is a normal ",[253,26311,3847],{}," the preload scanner finds early, ",[253,26314,19090],{}," alone is enough. Add a preload only when the resource is discovered late, such as a CSS background image, and then measure that it actually helped rather than wasted bandwidth.",[653,26317,26319],{"id":26318},"how-do-i-confirm-the-priority-hint-worked","How do I confirm the priority hint worked?",[14,26321,26322],{},"Run Lighthouse and check the LCP value and the \"Largest Contentful Paint element\" audit, and look at the network panel or a WebPageTest waterfall to confirm the hero request now starts near the top of the waterfall instead of behind other images.",[34,26324,684],{"id":683},[39,26326,26327,26334,26339,26344],{},[42,26328,26329,692,26331,26333],{},[229,26330,691],{},[23,26332,23975],{"href":23974}," — the full LCP workflow this fits into.",[42,26335,26336,26338],{},[23,26337,24562],{"href":24561}," — sizing and format for the hero.",[42,26340,26341,26343],{},[23,26342,23959],{"href":24649}," — cut the render-delay span.",[42,26345,26346,26348,26349,26351,26352,239],{},[23,26347,2190],{"href":2189}," — the ",[253,26350,2117],{}," setup behind ",[253,26353,18510],{},[1346,26355,11159],{},{"title":712,"searchDepth":713,"depth":713,"links":26357},[26358,26359,26365,26366,26367,26368,26374],{"id":36,"depth":713,"text":37},{"id":19483,"depth":713,"text":19484,"children":26360},[26361,26363,26364],{"id":25946,"depth":730,"text":26362},"1. Mark the hero with the priority prop",{"id":26044,"depth":730,"text":26045},{"id":26108,"depth":730,"text":26109},{"id":1165,"depth":713,"text":1166},{"id":599,"depth":713,"text":600},{"id":641,"depth":713,"text":642},{"id":650,"depth":713,"text":651,"children":26369},[26370,26371,26372,26373],{"id":26269,"depth":730,"text":26270},{"id":26295,"depth":730,"text":26296},{"id":26305,"depth":730,"text":26306},{"id":26318,"depth":730,"text":26319},{"id":683,"depth":713,"text":684},[26376,26377,26378,26379],{"name":737,"item":738},{"name":5501,"item":5500},{"name":23975,"item":23974},{"name":19813,"item":19812},"Use fetchpriority=\"high\", rel=preload, and Astro Image priority to load the hero first and cut LCP — with measured before\u002Fafter numbers from Lighthouse and WebPageTest.",[26382,26384,26386,26388],{"q":26270,"a":26383},"The priority prop on Astro's Image component sets loading to eager and fetchpriority to high, and skips lazy loading, in one shorthand. It does not emit a preload link, so for a CSS background or a late-discovered image you still add a manual rel=preload in the head.",{"q":26296,"a":26385},"No. Priority is relative, so marking several images high tells the browser they are all equally important and the benefit disappears. Reserve fetchpriority high for the single LCP element and let everything else load at default or lazy priority.",{"q":26306,"a":26387},"Not always. If the hero is a normal img the preload scanner finds early, fetchpriority high alone is enough. Add a preload only when the resource is discovered late, such as a CSS background image, and then measure that it actually helped rather than wasted bandwidth.",{"q":26319,"a":26322},{},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Flargest-contentful-paint-optimization-for-static-sites\u002Foptimizing-lcp-on-astro-with-priority-hints",{"title":19813,"description":26380},"performance-optimization-core-web-vitals-for-ssgs\u002Flargest-contentful-paint-optimization-for-static-sites\u002Foptimizing-lcp-on-astro-with-priority-hints\u002Findex","kjP6qwGQ798gf35XLAOM7NEHNuUYfwMEXMCrYsuY-SE",{"id":26395,"title":24562,"body":26396,"breadcrumb":26954,"dateModified":743,"datePublished":743,"description":26959,"extension":745,"faq":26960,"meta":26968,"navigation":752,"path":26969,"seo":26970,"slug":26400,"stem":26971,"type":756,"__hash__":26972},"content\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Flargest-contentful-paint-optimization-for-static-sites\u002Freducing-lcp-from-hero-images-on-static-sites\u002Findex.md",{"type":7,"value":26397,"toc":26935},[26398,26401,26409,26411,26425,26513,26515,26519,26527,26671,26676,26680,26687,26691,26704,26708,26724,26726,26729,26781,26801,26803,26849,26851,26863,26865,26869,26872,26876,26885,26889,26897,26901,26907,26909,26932],[10,26399,24562],{"id":26400},"reducing-lcp-from-hero-images-on-static-sites",[14,26402,26403,26404,26406,26407,17758],{},"On most marketing and documentation landing pages the hero image is the Largest Contentful Paint (LCP) element — it is the biggest thing in the first viewport, so the browser counts its paint time as your LCP. If that image is a 1.4 MB JPEG loaded at default priority, your LCP is whatever time it takes that file to arrive and render. This guide is the hero-image recipe: right size, right format, preloaded, and never lazy-loaded. It is the image-focused companion to ",[23,26405,23975],{"href":23974},", inside the larger ",[23,26408,5501],{"href":5500},[34,26410,37],{"id":36},[39,26412,26413,26416,26422],{},[42,26414,26415],{},"A static site (Astro, Hugo, Eleventy, or Jekyll) with a hero image in the first viewport.",[42,26417,26418,26419,26421],{},"A build-time image step that can emit multiple widths and formats — see ",[23,26420,2190],{"href":2189}," for the Astro path or the Hugo equivalent.",[42,26423,26424],{},"Lighthouse or WebPageTest and a deployed URL to measure against.",[55,26426,26427,26510],{},[58,26428,66,26432,66,26435,66,26438,66,26440,66,26503],{"viewBox":20093,"role":61,"ariaLabelledBy":26429,"xmlns":65},[26430,26431],"hero-flow-title","hero-flow-desc",[68,26433,26434],{"id":26430},"Hero image optimization flow",[72,26436,26437],{"id":26431},"A large source image passes through four checkpoints: generate responsive widths, encode to AVIF and WebP, set explicit dimensions, and preload with eager loading, producing a small fast hero that paints quickly.",[107,26439],{"x":2515,"y":2515,"width":8298,"height":1463,"fill":205},[95,26441,78,26442,78,26445,78,26447,78,26449,78,26451,78,26453,78,26455,78,26458,78,26460,78,26464,78,26467,78,26469,78,26472,78,26475,78,26478,78,26480,78,26483,78,26485,78,26500,66],{"style":97},[99,26443,26444],{"x":101,"y":109,"fill":103,"style":104},"From 1.4 MB source to a fast hero paint",[107,26446],{"x":5393,"y":1431,"width":1431,"height":849,"rx":823,"fill":162,"opacity":877,"stroke":164,"style":116},[99,26448,18403],{"x":1430,"y":161,"fill":103,"style":121},[99,26450,11036],{"x":1430,"y":194,"fill":93,"style":4658},[107,26452],{"x":194,"y":1431,"width":2563,"height":849,"rx":823,"fill":824,"opacity":186,"stroke":824,"style":116},[99,26454,3699],{"x":20836,"y":12791,"fill":824,"style":121},[99,26456,26457],{"x":20836,"y":11919,"fill":93,"style":4658},"800 \u002F 1200 \u002F 2400",[107,26459],{"x":874,"y":1431,"width":2563,"height":849,"rx":823,"fill":185,"opacity":850,"stroke":187,"style":116},[99,26461,26463],{"x":26462,"y":12791,"fill":187,"style":121},"395","Encode",[99,26465,26466],{"x":26462,"y":11919,"fill":93,"style":4658},"AVIF + WebP",[107,26468],{"x":4699,"y":1431,"width":119,"height":849,"rx":823,"fill":114,"opacity":186,"stroke":114,"style":116},[99,26470,26471],{"x":10820,"y":5379,"fill":114,"style":121},"Dimensions",[99,26473,26474],{"x":10820,"y":18406,"fill":93,"style":4658},"width\u002Fheight",[99,26476,26477],{"x":10820,"y":160,"fill":93,"style":4658},"eager + preload",[107,26479],{"x":2562,"y":1431,"width":1431,"height":849,"rx":823,"fill":2564,"opacity":825,"stroke":2565,"style":116},[99,26481,26482],{"x":11910,"y":12791,"fill":2565,"style":121},"Paint",[99,26484,10806],{"x":11910,"y":11919,"fill":93,"style":4658},[95,26486,88,26487,88,26491,88,26494,88,26497,78],{"stroke":93,"fill":205,"style":116},[90,26488],{"d":26489,"style":26490},"M140 155 L168 155","marker-end:url(#hero-arrow)",[90,26492],{"d":26493,"style":26490},"M300 155 L328 155",[90,26495],{"d":26496,"style":26490},"M460 155 L488 155",[90,26498],{"d":26499,"style":26490},"M630 155 L658 155",[99,26501,26502],{"x":101,"y":112,"fill":93,"style":126},"Each checkpoint cuts bytes or moves the request earlier; the lazy-load trap is the one to avoid in between.",[76,26504,78,26505,66],{},[80,26506,88,26508,78],{"id":26507,"viewBox":83,"refX":84,"refY":85,"markerWidth":86,"markerHeight":86,"orient":87},"hero-arrow",[90,26509],{"d":92,"fill":93},[218,26511,26512],{},"Four checkpoints turn a heavy source into a fast hero: resize to real widths, encode to AVIF\u002FWebP, set explicit dimensions, then load eagerly and preload.",[34,26514,19484],{"id":19483},[653,26516,26518],{"id":26517},"_1-generate-the-widths-the-layout-actually-uses","1. Generate the widths the layout actually uses",[14,26520,26521,26522,738,26524,931],{},"A hero rendered at 1200px CSS pixels needs an 800px width for phones and a 2400px variant for 2x screens — not a single 2400px file served to everyone. Generate the real widths and let the browser pick with ",[253,26523,3720],{},[253,26525,26526],{},"sizes",[987,26528,26530],{"className":16196,"code":26529,"language":16198,"meta":712,"style":712},"\u003Cpicture>\n  \u003Csource type=\"image\u002Favif\"\n          srcset=\"\u002Fhero-800.avif 800w, \u002Fhero-1200.avif 1200w, \u002Fhero-2400.avif 2400w\"\n          sizes=\"(max-width: 800px) 100vw, 1200px\" \u002F>\n  \u003Csource type=\"image\u002Fwebp\"\n          srcset=\"\u002Fhero-800.webp 800w, \u002Fhero-1200.webp 1200w, \u002Fhero-2400.webp 2400w\"\n          sizes=\"(max-width: 800px) 100vw, 1200px\" \u002F>\n  \u003Cimg src=\"\u002Fhero-1200.jpg\" alt=\"Analytics dashboard\"\n       width=\"1200\" height=\"600\"\n       fetchpriority=\"high\" loading=\"eager\" decoding=\"async\" \u002F>\n\u003C\u002Fpicture>\n",[253,26531,26532,26540,26553,26563,26573,26586,26595,26605,26624,26639,26663],{"__ignoreMap":712},[995,26533,26534,26536,26538],{"class":997,"line":998},[995,26535,16205],{"class":1618},[995,26537,25167],{"class":1921},[995,26539,16246],{"class":1618},[995,26541,26542,26544,26546,26548,26550],{"class":997,"line":713},[995,26543,24116],{"class":1618},[995,26545,25176],{"class":1921},[995,26547,16235],{"class":1007},[995,26549,7317],{"class":1618},[995,26551,26552],{"class":1023},"\"image\u002Favif\"\n",[995,26554,26555,26558,26560],{"class":997,"line":730},[995,26556,26557],{"class":1007},"          srcset",[995,26559,7317],{"class":1618},[995,26561,26562],{"class":1023},"\"\u002Fhero-800.avif 800w, \u002Fhero-1200.avif 1200w, \u002Fhero-2400.avif 2400w\"\n",[995,26564,26565,26567,26569,26571],{"class":997,"line":1544},[995,26566,25196],{"class":1007},[995,26568,7317],{"class":1618},[995,26570,25201],{"class":1023},[995,26572,20528],{"class":1618},[995,26574,26575,26577,26579,26581,26583],{"class":997,"line":1550},[995,26576,24116],{"class":1618},[995,26578,25176],{"class":1921},[995,26580,16235],{"class":1007},[995,26582,7317],{"class":1618},[995,26584,26585],{"class":1023},"\"image\u002Fwebp\"\n",[995,26587,26588,26590,26592],{"class":997,"line":1673},[995,26589,26557],{"class":1007},[995,26591,7317],{"class":1618},[995,26593,26594],{"class":1023},"\"\u002Fhero-800.webp 800w, \u002Fhero-1200.webp 1200w, \u002Fhero-2400.webp 2400w\"\n",[995,26596,26597,26599,26601,26603],{"class":997,"line":1678},[995,26598,25196],{"class":1007},[995,26600,7317],{"class":1618},[995,26602,25201],{"class":1023},[995,26604,20528],{"class":1618},[995,26606,26607,26609,26611,26613,26615,26617,26619,26621],{"class":997,"line":1693},[995,26608,24116],{"class":1618},[995,26610,61],{"class":1921},[995,26612,17918],{"class":1007},[995,26614,7317],{"class":1618},[995,26616,25246],{"class":1023},[995,26618,17944],{"class":1007},[995,26620,7317],{"class":1618},[995,26622,26623],{"class":1023},"\"Analytics dashboard\"\n",[995,26625,26626,26629,26631,26633,26635,26637],{"class":997,"line":1705},[995,26627,26628],{"class":1007},"       width",[995,26630,7317],{"class":1618},[995,26632,17933],{"class":1023},[995,26634,17936],{"class":1007},[995,26636,7317],{"class":1618},[995,26638,25045],{"class":1023},[995,26640,26641,26643,26645,26647,26650,26652,26655,26657,26659,26661],{"class":997,"line":1711},[995,26642,25269],{"class":1007},[995,26644,7317],{"class":1618},[995,26646,25055],{"class":1023},[995,26648,26649],{"class":1007}," loading",[995,26651,7317],{"class":1618},[995,26653,26654],{"class":1023},"\"eager\"",[995,26656,25276],{"class":1007},[995,26658,7317],{"class":1618},[995,26660,25281],{"class":1023},[995,26662,20528],{"class":1618},[995,26664,26665,26667,26669],{"class":997,"line":1717},[995,26666,24126],{"class":1618},[995,26668,25167],{"class":1921},[995,26670,16246],{"class":1618},[14,26672,8896,26673,26675],{},[253,26674,26526],{}," attribute tells the browser the rendered width before layout, so it downloads the right candidate on the first try instead of guessing.",[653,26677,26679],{"id":26678},"_2-encode-to-a-modern-format","2. Encode to a modern format",[14,26681,26682,26683,26686],{},"AVIF is typically 20-30% smaller than WebP at matched quality, and WebP is 25-50% smaller than JPEG. Emit both with the original as a final fallback. ",[253,26684,26685],{},"quality=80"," is the sweet spot for photographic heroes; below 60 you start to see banding on gradients.",[653,26688,26690],{"id":26689},"_3-set-explicit-dimensions","3. Set explicit dimensions",[14,26692,26693,26694,270,26696,256,26698,26700,26701,26703],{},"Always include ",[253,26695,9286],{},[253,26697,18059],{},[253,26699,18229],{},"). This reserves layout space so the hero does not push content down when it arrives, keeping Cumulative Layout Shift near zero. Astro's ",[253,26702,18510],{}," requires dimensions for exactly this reason.",[653,26705,26707],{"id":26706},"_4-load-it-eagerly-and-preload-it","4. Load it eagerly and preload it",[14,26709,26710,26711,26713,26714,270,26716,26718,26719,26721,26722,239],{},"Never put ",[253,26712,4897],{}," on the hero — that defers the LCP element on purpose. Use ",[253,26715,19087],{},[253,26717,19090],{},". If the hero is a CSS background or discovered late, add a preload; the Astro shorthand for all of this is the ",[253,26720,25117],{}," prop, covered in ",[23,26723,19813],{"href":19812},[34,26725,1166],{"id":1165},[14,26727,26728],{},"Measured on a marketing landing page, throttled mobile profile (4x CPU, ~1.6 Mbps), median of five Lighthouse runs:",[433,26730,26731,26745],{},[436,26732,26733],{},[439,26734,26735,26738,26741,26743],{},[442,26736,26737],{},"Hero variant",[442,26739,26740],{},"Delivered bytes (phone)",[442,26742,20362],{},[442,26744,16586],{},[457,26746,26747,26758,26769],{},[439,26748,26749,26752,26754,26756],{},[462,26750,26751],{},"1.4 MB JPEG, lazy-loaded",[462,26753,11036],{},[462,26755,24682],{},[462,26757,6172],{},[439,26759,26760,26763,26765,26767],{},[462,26761,26762],{},"1.4 MB JPEG, eager + preload",[462,26764,11036],{},[462,26766,17507],{},[462,26768,16628],{},[439,26770,26771,26774,26777,26779],{},[462,26772,26773],{},"Responsive AVIF, eager + preload",[462,26775,26776],{},"96 KB (800w)",[462,26778,10939],{},[462,26780,16628],{},[14,26782,26783,26784,26786,26787,26790,26791,25338,26794,26796,26797,26800],{},"Removing ",[253,26785,4897],{}," and adding the preload alone cut LCP from ",[229,26788,26789],{},"3.4s to 2.6s",". Switching to a responsive AVIF dropped the phone download from ",[229,26792,26793],{},"1.4 MB to 96 KB",[229,26795,10939],{},". Setting explicit dimensions took CLS from ",[229,26798,26799],{},"0.08 to 0.00",". WebPageTest confirmed the hero now paints in the first frame after first byte.",[34,26802,600],{"id":599},[39,26804,26805,26810,26819,26829,26838],{},[42,26806,26807,26809],{},[229,26808,19081],{}," the single most common LCP regression. Keep lazy loading for below-the-fold images only.",[42,26811,26812,26815,26816,26818],{},[229,26813,26814],{},"One giant width for all devices:"," a 2400px file on a phone wastes bandwidth and slows LCP where it hurts most. Always provide smaller candidates and a ",[253,26817,26526],{}," attribute.",[42,26820,26821,26823,26824,738,26826,26828],{},[229,26822,18221],{}," no ",[253,26825,9286],{},[253,26827,18059],{}," means the browser cannot reserve space, so CLS spikes when the hero loads.",[42,26830,26831,19119,26834,26837],{},[229,26832,26833],{},"Quality too low to save bytes:",[253,26835,26836],{},"quality=60"," produces visible banding. Reach for a smaller width instead of crushing quality.",[42,26839,26840,26842,26843,26845,26846,26848],{},[229,26841,637],{}," revert the ",[253,26844,8172],{}," block to the original ",[253,26847,3847],{}," and redeploy. Because the optimized variants are build artifacts, no cache state needs clearing — the next build regenerates or removes them.",[34,26850,642],{"id":641},[14,26852,26853,26854,26857,26858,26860,26861,239],{},"The hero is usually your LCP element, so it deserves the most attention: generate real widths, encode to AVIF\u002FWebP, set explicit dimensions, and load it eagerly with a preload. On the example page these steps moved LCP from ",[229,26855,26856],{},"3.4s to 1.6s"," and CLS from 0.08 to zero. Combine this with the priority hints in ",[23,26859,19813],{"href":19812}," and the render-delay work in ",[23,26862,23959],{"href":24649},[34,26864,651],{"id":650},[653,26866,26868],{"id":26867},"why-is-the-hero-image-so-often-the-lcp-element","Why is the hero image so often the LCP element?",[14,26870,26871],{},"The hero is usually the largest visible element in the first viewport, which is exactly what Largest Contentful Paint measures. If it is big and downloads slowly, your LCP is its paint time, so shrinking and prioritizing it has the most direct effect on the metric.",[653,26873,26875],{"id":26874},"should-i-ever-lazy-load-a-hero-image","Should I ever lazy-load a hero image?",[14,26877,26878,26879,26881,26882,26884],{},"No. ",[253,26880,4897],{}," tells the browser to defer the image until layout determines it is near the viewport, which delays the LCP element on purpose. Use ",[253,26883,19087],{}," for anything above the fold and reserve lazy loading for images below it.",[653,26886,26888],{"id":26887},"what-size-should-i-generate-the-hero-at","What size should I generate the hero at?",[14,26890,26891,26892,270,26894,26896],{},"Generate the widths your layout actually renders, plus a 2x variant for high-density screens, and let ",[253,26893,3720],{},[253,26895,26526],{}," pick. Shipping a 2400px source into a 1200px slot doubles the download for no visible gain. Match the largest width to the largest rendered size.",[653,26898,26900],{"id":26899},"does-a-responsive-srcset-help-lcp-or-just-bandwidth","Does a responsive srcset help LCP or just bandwidth?",[14,26902,26903,26904,26906],{},"Both. By serving a phone a smaller file than a desktop, ",[253,26905,3720],{}," cuts the download span of LCP on mobile, where networks are slowest and LCP problems are worst. It also avoids wasting bandwidth on pixels the device cannot show.",[34,26908,684],{"id":683},[39,26910,26911,26917,26922,26927],{},[42,26912,26913,692,26915,24607],{},[229,26914,691],{},[23,26916,23975],{"href":23974},[42,26918,26919,26921],{},[23,26920,19813],{"href":19812}," — prioritize the hero request.",[42,26923,26924,26926],{},[23,26925,2190],{"href":2189}," — generate the widths and formats this recipe needs.",[42,26928,26929,26931],{},[23,26930,23959],{"href":24649}," — remove the other LCP bottleneck.",[1346,26933,26934],{},"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 .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}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);}",{"title":712,"searchDepth":713,"depth":713,"links":26936},[26937,26938,26944,26945,26946,26947,26953],{"id":36,"depth":713,"text":37},{"id":19483,"depth":713,"text":19484,"children":26939},[26940,26941,26942,26943],{"id":26517,"depth":730,"text":26518},{"id":26678,"depth":730,"text":26679},{"id":26689,"depth":730,"text":26690},{"id":26706,"depth":730,"text":26707},{"id":1165,"depth":713,"text":1166},{"id":599,"depth":713,"text":600},{"id":641,"depth":713,"text":642},{"id":650,"depth":713,"text":651,"children":26948},[26949,26950,26951,26952],{"id":26867,"depth":730,"text":26868},{"id":26874,"depth":730,"text":26875},{"id":26887,"depth":730,"text":26888},{"id":26899,"depth":730,"text":26900},{"id":683,"depth":713,"text":684},[26955,26956,26957,26958],{"name":737,"item":738},{"name":5501,"item":5500},{"name":23975,"item":23974},{"name":24562,"item":24561},"Size, format, and preload hero images to cut LCP on static sites — responsive srcset, AVIF\u002FWebP, explicit dimensions, and why you must never lazy-load the hero.",[26961,26962,26964,26966],{"q":26868,"a":26871},{"q":26875,"a":26963},"No. loading=\"lazy\" tells the browser to defer the image until layout determines it is near the viewport, which delays the LCP element on purpose. Use loading=\"eager\" for anything above the fold and reserve lazy loading for images below it.",{"q":26888,"a":26965},"Generate the widths your layout actually renders, plus a 2x variant for high-density screens, and let srcset and sizes pick. Shipping a 2400px source into a 1200px slot doubles the download for no visible gain. Match the largest width to the largest rendered size.",{"q":26900,"a":26967},"Both. By serving a phone a smaller file than a desktop, srcset cuts the download span of LCP on mobile, where networks are slowest and LCP problems are worst. It also avoids wasting bandwidth on pixels the device cannot show.",{},"\u002Fperformance-optimization-core-web-vitals-for-ssgs\u002Flargest-contentful-paint-optimization-for-static-sites\u002Freducing-lcp-from-hero-images-on-static-sites",{"title":24562,"description":26959},"performance-optimization-core-web-vitals-for-ssgs\u002Flargest-contentful-paint-optimization-for-static-sites\u002Freducing-lcp-from-hero-images-on-static-sites\u002Findex","i5_w-k1k7o9EkqIj54AtpZEI9fy9jgs9rI7wTzXTY7U",{"id":26974,"title":26975,"body":26976,"breadcrumb":27631,"dateModified":743,"datePublished":2446,"description":27637,"extension":745,"faq":27638,"meta":27647,"navigation":752,"path":27648,"seo":27649,"slug":26980,"stem":27650,"type":756,"__hash__":27651},"content\u002Fproduction-ready-deployment-cicd-workflows\u002Fcloudflare-pages-edge-caching-setup\u002Fautomating-eleventy-deployments-with-cloudflare-pages\u002Findex.md","Automating Eleventy Deployments on Cloudflare Pages",{"type":7,"value":26977,"toc":27614},[26978,26982,26989,26991,27009,27096,27100,27107,27125,27138,27143,27146,27182,27186,27200,27259,27270,27274,27283,27308,27311,27317,27328,27330,27336,27402,27411,27413,27491,27493,27514,27516,27523,27540,27544,27551,27555,27565,27569,27578,27580,27611],[10,26979,26981],{"id":26980},"automating-eleventy-deployments-with-cloudflare-pages","Automating Eleventy Deployments with Cloudflare Pages",[14,26983,26984,26985,239],{},"Cloudflare Pages can build and deploy an Eleventy site straight from Git: connect the repository once, set the build command and output directory, and every push ships to the global edge. This recipe covers that setup end to end — runtime pinning, environment variables, cache headers, and the actual build and deploy times you should expect. It is the concrete, Eleventy-specific application of ",[23,26986,26988],{"href":26987},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fcloudflare-pages-edge-caching-setup\u002F","Cloudflare Pages Edge Caching Setup",[34,26990,37],{"id":36},[39,26992,26993,26999,27005],{},[42,26994,26995,26996,239],{},"An Eleventy 3 site in a GitHub or GitLab repository, with a committed ",[253,26997,26998],{},"package-lock.json",[42,27000,27001,27002,239],{},"A Cloudflare account with access to ",[229,27003,27004],{},"Workers & Pages",[42,27006,27007,15509],{},[253,27008,14076],{},[55,27010,27011,27093],{},[58,27012,66,27017,66,27020,66,27023,66,27025,66,27086],{"viewBox":27013,"role":61,"ariaLabelledBy":27014,"xmlns":65},"0 0 780 290",[27015,27016],"11ty-cf-title","11ty-cf-desc",[68,27018,27019],{"id":27015},"Eleventy to Cloudflare Pages continuous deploy flow",[72,27021,27022],{"id":27016},"A push to the repository triggers Cloudflare Pages to run npm ci then the Eleventy build, producing the _site output with a _headers file, which deploys atomically to the edge.",[107,27024],{"x":2515,"y":2515,"width":5370,"height":1462,"fill":205},[95,27026,78,27027,78,27030,78,27032,78,27035,78,27038,78,27040,78,27042,78,27045,78,27048,78,27050,78,27053,78,27056,78,27059,78,27061,78,27065,78,27068,78,27071,78,27083,66],{"style":813},[99,27028,27029],{"x":167,"y":109,"fill":103,"style":104},"Git push is the deploy pipeline",[107,27031],{"x":5393,"y":1430,"width":119,"height":873,"rx":823,"fill":114,"opacity":186,"stroke":114,"style":116},[99,27033,27034],{"x":873,"y":13894,"fill":114,"style":121},"git push",[99,27036,27037],{"x":873,"y":119,"fill":93,"style":126},"to main \u002F PR",[107,27039],{"x":25904,"y":1430,"width":161,"height":873,"rx":823,"fill":824,"opacity":186,"stroke":824,"style":116},[99,27041,2072],{"x":5332,"y":125,"fill":824,"style":121},[99,27043,27044],{"x":5332,"y":130,"fill":93,"style":126},"from lockfile",[99,27046,27047],{"x":5332,"y":6153,"fill":93,"style":4658},"Node 22 pinned",[107,27049],{"x":816,"y":1430,"width":7852,"height":873,"rx":823,"fill":162,"opacity":877,"stroke":164,"style":116},[99,27051,27052],{"x":11991,"y":125,"fill":103,"style":121},"eleventy build",[99,27054,27055],{"x":11991,"y":130,"fill":93,"style":126},"_site\u002F output",[99,27057,27058],{"x":11991,"y":6153,"fill":93,"style":4658},"+ _headers",[107,27060],{"x":866,"y":1430,"width":160,"height":873,"rx":823,"fill":185,"opacity":850,"stroke":187,"style":116},[99,27062,27064],{"x":27063,"y":125,"fill":187,"style":121},"665","Atomic deploy",[99,27066,27067],{"x":27063,"y":130,"fill":93,"style":126},"to global edge",[99,27069,27070],{"x":27063,"y":6153,"fill":93,"style":4658},"preview or prod",[95,27072,88,27073,88,27077,88,27080,78],{"stroke":93,"fill":205,"style":116},[90,27074],{"d":27075,"style":27076},"M160 125 L193 125","marker-end:url(#11ty-cf-arrow)",[90,27078],{"d":27079,"style":27076},"M345 125 L378 125",[90,27081],{"d":27082,"style":27076},"M540 125 L573 125",[99,27084,27085],{"x":167,"y":14947,"fill":93,"style":126},"No CLI, no artifact upload — Pages owns build and deploy from the connected repo.",[76,27087,78,27088,66],{},[80,27089,88,27091,78],{"id":27090,"viewBox":83,"refX":84,"refY":85,"markerWidth":86,"markerHeight":86,"orient":87},"11ty-cf-arrow",[90,27092],{"d":92,"fill":93},[218,27094,27095],{},"A push triggers `npm ci`, the Eleventy build into `_site\u002F`, and an atomic deploy to the edge — production on `main`, an isolated preview URL on every other branch.",[34,27097,27099],{"id":27098},"connect-the-repo-configure-the-build","Connect the Repo & Configure the Build",[14,27101,27102,27103,27106],{},"In the Cloudflare dashboard, go to ",[229,27104,27105],{},"Workers & Pages → Create → Pages → Connect to Git"," and pick your Eleventy repository. Set two values:",[39,27108,27109,27117],{},[42,27110,27111,692,27114],{},[229,27112,27113],{},"Build command:",[253,27115,27116],{},"npm run build",[42,27118,27119,692,27122,27124],{},[229,27120,27121],{},"Output directory:",[253,27123,2245],{}," (Eleventy's default)",[14,27126,27127,27128,27130,27131,27133,27134,27137],{},"Pages detects ",[253,27129,21912],{}," and runs ",[253,27132,2072],{}," before your build. Eleventy 3 requires Node 18 or newer, so pin the runtime — the simplest way is a ",[253,27135,27136],{},".nvmrc"," file at the repo root, which Pages reads automatically:",[987,27139,27141],{"className":27140,"code":2011,"language":99,"meta":712},[11603],[253,27142,2011],{"__ignoreMap":712},[14,27144,27145],{},"Keep the build production-flagged and quiet so the logs stay short and the output stays optimized:",[987,27147,27149],{"className":14263,"code":27148,"language":14265,"meta":712,"style":712},"{\n  \"scripts\": {\n    \"build\": \"ELEVENTY_ENV=production npx @11ty\u002Feleventy --quiet\"\n  }\n}\n",[253,27150,27151,27155,27163,27173,27178],{"__ignoreMap":712},[995,27152,27153],{"class":997,"line":998},[995,27154,14272],{"class":1618},[995,27156,27157,27160],{"class":997,"line":713},[995,27158,27159],{"class":1010},"  \"scripts\"",[995,27161,27162],{"class":1618},": {\n",[995,27164,27165,27168,27170],{"class":997,"line":730},[995,27166,27167],{"class":1010},"    \"build\"",[995,27169,1925],{"class":1618},[995,27171,27172],{"class":1023},"\"ELEVENTY_ENV=production npx @11ty\u002Feleventy --quiet\"\n",[995,27174,27175],{"class":997,"line":1544},[995,27176,27177],{"class":1618},"  }\n",[995,27179,27180],{"class":997,"line":1550},[995,27181,9008],{"class":1618},[34,27183,27185],{"id":27184},"environment-variables","Environment Variables",[14,27187,27188,27189,27192,27193,27195,27196,27199],{},"Add variables under ",[229,27190,27191],{},"Settings → Environment variables",", scoped to production or preview as needed. Pages injects them into the Node build process, so read them in ",[253,27194,22086],{}," or your data files via ",[253,27197,27198],{},"process.env.YOUR_VAR"," — Eleventy does not pass them to templates automatically. To expose a value to templates, surface it through global data:",[987,27201,27203],{"className":1600,"code":27202,"language":1602,"meta":712,"style":712},"\u002F\u002F eleventy.config.js\nmodule.exports = function (eleventyConfig) {\n  eleventyConfig.addGlobalData(\"siteEnv\", () => process.env.ELEVENTY_ENV || \"development\");\n};\n",[253,27204,27205,27209,27227,27255],{"__ignoreMap":712},[995,27206,27207],{"class":997,"line":998},[995,27208,6223],{"class":1001},[995,27210,27211,27213,27215,27217,27219,27221,27223,27225],{"class":997,"line":713},[995,27212,1767],{"class":1010},[995,27214,239],{"class":1618},[995,27216,1772],{"class":1010},[995,27218,1775],{"class":1614},[995,27220,1778],{"class":1614},[995,27222,1781],{"class":1618},[995,27224,1785],{"class":1784},[995,27226,1788],{"class":1618},[995,27228,27229,27231,27233,27235,27238,27241,27243,27246,27248,27250,27253],{"class":997,"line":730},[995,27230,1793],{"class":1618},[995,27232,5816],{"class":1007},[995,27234,1799],{"class":1618},[995,27236,27237],{"class":1023},"\"siteEnv\"",[995,27239,27240],{"class":1618},", () ",[995,27242,1858],{"class":1614},[995,27244,27245],{"class":1618}," process.env.",[995,27247,21769],{"class":1010},[995,27249,9333],{"class":1614},[995,27251,27252],{"class":1023}," \"development\"",[995,27254,5829],{"class":1618},[995,27256,27257],{"class":997,"line":1544},[995,27258,1877],{"class":1618},[14,27260,27261,27262,27265,27266,27269],{},"Never commit a real ",[253,27263,27264],{},".env"," file — keep it in ",[253,27267,27268],{},".gitignore"," and define the values in the dashboard.",[34,27271,27273],{"id":27272},"cache-headers","Cache Headers",[14,27275,27276,27277,27279,27280,27282],{},"Ship a ",[253,27278,14036],{}," file in ",[253,27281,7303],{},". Author it in your input directory and let Eleventy passthrough-copy it into the output:",[987,27284,27286],{"className":1600,"code":27285,"language":1602,"meta":712,"style":712},"\u002F\u002F eleventy.config.js — copy _headers verbatim into _site\u002F\neleventyConfig.addPassthroughCopy(\"_headers\");\n",[253,27287,27288,27293],{"__ignoreMap":712},[995,27289,27290],{"class":997,"line":998},[995,27291,27292],{"class":1001},"\u002F\u002F eleventy.config.js — copy _headers verbatim into _site\u002F\n",[995,27294,27295,27298,27301,27303,27306],{"class":997,"line":713},[995,27296,27297],{"class":1618},"eleventyConfig.",[995,27299,27300],{"class":1007},"addPassthroughCopy",[995,27302,1799],{"class":1618},[995,27304,27305],{"class":1023},"\"_headers\"",[995,27307,5829],{"class":1618},[14,27309,27310],{},"Long-cache hashed assets, revalidate HTML:",[987,27312,27315],{"className":27313,"code":27314,"language":99,"meta":712},[11603],"\u002Fassets\u002F*\n  Cache-Control: public, max-age=31536000, immutable\n\u002F*.html\n  Cache-Control: public, max-age=0, must-revalidate\n",[253,27316,27314],{"__ignoreMap":712},[14,27318,16255,27319,270,27322,27324,27325,27327],{},[253,27320,27321],{},"s-maxage",[253,27323,14131],{}," tuning and purge automation, see the parent ",[23,27326,26988],{"href":26987},". A fresh Pages deploy invalidates changed files automatically, so you usually do not purge manually.",[34,27329,1166],{"id":1165},[14,27331,27332,27333,27335],{},"On a 180-page Eleventy documentation site deployed from a connected GitHub repo, the Pages build and deploy timeline looked like this. The cold build pays the full ",[253,27334,2072],{}," cost; warm builds reuse the dependency cache Pages keeps between deploys:",[433,27337,27338,27349],{},[436,27339,27340],{},[439,27341,27342,27344,27346],{},[442,27343,16580],{},[442,27345,11010],{},[442,27347,27348],{},"Warm build",[457,27350,27351,27363,27375,27385],{},[439,27352,27353,27358,27361],{},[462,27354,27355,27357],{},[253,27356,2072],{}," (install)",[462,27359,27360],{},"38 s",[462,27362,4886],{},[439,27364,27365,27371,27373],{},[462,27366,27367,27370],{},[253,27368,27369],{},"eleventy --quiet"," (render 180 pages)",[462,27372,13148],{},[462,27374,13148],{},[439,27376,27377,27380,27383],{},[462,27378,27379],{},"Upload + atomic edge propagation",[462,27381,27382],{},"11 s",[462,27384,27382],{},[439,27386,27387,27392,27397],{},[462,27388,27389],{},[229,27390,27391],{},"Total push-to-live",[462,27393,27394],{},[229,27395,27396],{},"~55 s",[462,27398,27399],{},[229,27400,27401],{},"~26 s",[14,27403,27404,27405,27407,27408,27410],{},"Render time is flat because Eleventy rebuilds the whole site each run; the variable cost is the install step. After the first edge ",[253,27406,14685],{},", repeat visits to an HTML route served ",[253,27409,14519],{}," with TTFB around 30 ms from a nearby point of presence, versus roughly 180 ms when the request reached origin.",[34,27412,600],{"id":599},[39,27414,27415,27426,27438,27460,27475],{},[42,27416,27417,27420,27421,1781,27423,27425],{},[229,27418,27419],{},"No Node version pinned:"," Pages may default to an older runtime and Eleventy 3 fails. Add ",[253,27422,27136],{},[253,27424,16501],{},") or set the version in the dashboard.",[42,27427,27428,27431,27432,27435,27436,239],{},[229,27429,27430],{},"Env vars missing in templates:"," they live only in the Node build. Read them via ",[253,27433,27434],{},"process.env"," and expose them with ",[253,27437,5816],{},[42,27439,27440,27446,27447,27450,27451,27453,27454,738,27457,239],{},[229,27441,27442,27445],{},[253,27443,27444],{},"Module not found"," on build:"," a stale lockfile. Run ",[253,27448,27449],{},"npm install"," locally, commit ",[253,27452,26998],{},", and confirm the package is in ",[253,27455,27456],{},"dependencies",[253,27458,27459],{},"devDependencies",[42,27461,27462,9494,27467,27469,27470,27472,27473,15259],{},[229,27463,27464,27466],{},[253,27465,14036],{}," not copied:",[253,27468,27300],{},", the file never reaches ",[253,27471,7303],{}," and all your cache rules silently vanish. Verify with ",[253,27474,14623],{},[42,27476,27477,27479,27480,27483,27484,27487,27488,27490],{},[229,27478,637],{}," open the Pages project's ",[229,27481,27482],{},"Deployments"," tab and click ",[229,27485,27486],{},"Rollback to this deployment"," on a previous build — it re-points the live alias to that immutable artifact in seconds. Because HTML uses ",[253,27489,14640],{},", browsers pick up the rolled-back version on their next request rather than serving the bad release.",[34,27492,642],{"id":641},[14,27494,27495,27496,27498,27499,27501,27502,27504,27505,27508,27509,27511,27512,239],{},"Automating Eleventy on Cloudflare Pages is three settings — build command, ",[253,27497,2245],{}," output, pinned Node — plus a passthrough-copied ",[253,27500,14036],{}," file. After that, ",[253,27503,27034],{}," is your deploy pipeline: production on ",[253,27506,27507],{},"main",", a preview URL on every other branch, and a one-click rollback when you need it. The full edge-cache tuning lives in ",[23,27510,26988],{"href":26987},", and the Hugo equivalent — including pushing logic to the edge — is in ",[23,27513,383],{"href":382},[34,27515,651],{"id":650},[653,27517,27519,27520,27522],{"id":27518},"why-does-my-eleventy-build-fail-with-module-not-found-on-cloudflare-pages","Why does my Eleventy build fail with ",[253,27521,27444],{}," on Cloudflare Pages?",[14,27524,27525,27526,27528,27529,27531,27532,2204,27534,27536,27537,27539],{},"The lockfile is stale or a build dependency is missing. Run ",[253,27527,27449],{}," locally, commit the updated ",[253,27530,26998],{},", and confirm the package is listed in ",[253,27533,27456],{},[253,27535,27459],{}," so ",[253,27538,2072],{}," installs it during the Pages build.",[653,27541,27543],{"id":27542},"how-do-i-purge-the-cache-after-an-eleventy-deploy","How do I purge the cache after an Eleventy deploy?",[14,27545,27546,27547,27550],{},"Usually you do not need to, because a new Pages deploy invalidates the files that changed. For an external zone fronting Pages, POST to the Cloudflare purge API on deploy success with a ",[253,27548,27549],{},"files"," array of the changed URLs.",[653,27552,27554],{"id":27553},"can-i-use-pages-functions-with-an-eleventy-site","Can I use Pages Functions with an Eleventy site?",[14,27556,27557,27558,27561,27562,27564],{},"Yes. Put functions in a ",[253,27559,27560],{},"functions\u002F"," directory at the repo root and Pages deploys them alongside your ",[253,27563,2245],{}," output. Each function only handles its matched route, so the rest of the site stays fully static.",[653,27566,27568],{"id":27567},"how-do-i-expose-a-build-time-environment-variable-to-eleventy-templates","How do I expose a build-time environment variable to Eleventy templates?",[14,27570,27571,27572,27574,27575,27577],{},"Pages injects variables into the Node build process only, so read them via ",[253,27573,27434],{}," in your config and surface them through ",[253,27576,5816],{},". Eleventy does not pass environment variables to templates automatically.",[34,27579,684],{"id":683},[39,27581,27582,27592,27597,27604],{},[42,27583,27584,692,27586,27588,27589,27591],{},[229,27585,691],{},[23,27587,26988],{"href":26987}," — the full ",[253,27590,14036],{}," and purge reference.",[42,27593,27594,27596],{},[23,27595,383],{"href":382}," — the Hugo equivalent with edge functions.",[42,27598,27599,27603],{},[23,27600,27602],{"href":27601},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fgithub-actions-for-automated-ssg-builds\u002Fhow-to-set-up-github-actions-for-hugo-deployments\u002F","How to Set Up GitHub Actions for Hugo Deployments"," — building in CI instead of on the host.",[42,27605,27606,27610],{},[23,27607,27609],{"href":27608},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fnetlify-vs-vercel-deployment-strategies\u002Fnetlify-build-hooks-for-content-updates\u002F","Netlify Build Hooks for Content Updates"," — triggering rebuilds from a CMS or schedule.\n\n",[1346,27612,27613],{},"html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}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 .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 .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}",{"title":712,"searchDepth":713,"depth":713,"links":27615},[27616,27617,27618,27619,27620,27621,27622,27623,27630],{"id":36,"depth":713,"text":37},{"id":27098,"depth":713,"text":27099},{"id":27184,"depth":713,"text":27185},{"id":27272,"depth":713,"text":27273},{"id":1165,"depth":713,"text":1166},{"id":599,"depth":713,"text":600},{"id":641,"depth":713,"text":642},{"id":650,"depth":713,"text":651,"children":27624},[27625,27627,27628,27629],{"id":27518,"depth":730,"text":27626},"Why does my Eleventy build fail with Module not found on Cloudflare Pages?",{"id":27542,"depth":730,"text":27543},{"id":27553,"depth":730,"text":27554},{"id":27567,"depth":730,"text":27568},{"id":683,"depth":713,"text":684},[27632,27633,27634,27635],{"name":737,"item":738},{"name":5505,"item":5504},{"name":26988,"item":26987},{"name":26981,"item":27636},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fcloudflare-pages-edge-caching-setup\u002Fautomating-eleventy-deployments-with-cloudflare-pages\u002F","Deploy Eleventy from Git on Cloudflare Pages: connect the repo, set the build command and _site output, pin Node, wire env vars, and ship cache headers — with build times.",[27639,27641,27643,27645],{"q":27626,"a":27640},"The lockfile is stale or a build dependency is missing. Run npm install locally, commit the updated package-lock.json, and confirm the package is listed in dependencies or devDependencies so npm ci installs it during the Pages build.",{"q":27543,"a":27642},"Usually you do not need to, because a new Pages deploy invalidates the files that changed. For an external zone fronting Pages, POST to the Cloudflare purge API on deploy success with a files array of the changed URLs.",{"q":27554,"a":27644},"Yes. Put functions in a functions directory at the repo root and Pages deploys them alongside your _site output. Each function only handles its matched route, so the rest of the site stays fully static.",{"q":27568,"a":27646},"Pages injects variables into the Node build process only, so read them via process.env in your config and surface them through addGlobalData. Eleventy does not pass environment variables to templates automatically.",{},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fcloudflare-pages-edge-caching-setup\u002Fautomating-eleventy-deployments-with-cloudflare-pages",{"title":26975,"description":27637},"production-ready-deployment-cicd-workflows\u002Fcloudflare-pages-edge-caching-setup\u002Fautomating-eleventy-deployments-with-cloudflare-pages\u002Findex","vRJ-ROwq_dthPwYAPK5LoYy8UUMCGNlJRpQEg3qdADw",{"id":27653,"title":383,"body":27654,"breadcrumb":28427,"dateModified":743,"datePublished":743,"description":28432,"extension":745,"faq":28433,"meta":28441,"navigation":752,"path":28442,"seo":28443,"slug":27658,"stem":28444,"type":756,"__hash__":28445},"content\u002Fproduction-ready-deployment-cicd-workflows\u002Fcloudflare-pages-edge-caching-setup\u002Fdeploying-hugo-to-cloudflare-pages-and-workers\u002Findex.md",{"type":7,"value":27655,"toc":28410},[27656,27659,27674,27676,27703,27787,27791,27797,27849,27863,27872,27876,27882,27933,27947,27951,27979,27985,27991,28014,28020,28022,28028,28092,28101,28105,28112,28141,28151,28176,28191,28202,28235,28241,28243,28310,28312,28331,28333,28337,28346,28350,28361,28365,28368,28372,28381,28383,28407],[10,27657,383],{"id":27658},"deploying-hugo-to-cloudflare-pages-and-workers",[14,27660,27661,27662,5153,27665,27668,27669,27671,27672,17758],{},"Hugo produces a directory of static files, and Cloudflare Pages exists to serve exactly that from its global edge. The deploy itself is almost trivial once two things are correct: the ",[229,27663,27664],{},"Hugo version is pinned",[229,27666,27667],{},"output directory matches what Hugo writes",". Most failed Hugo-on-Cloudflare builds trace back to one of those two settings. This guide walks the working configuration end to end, then draws the line where you actually need a Worker. It sits under ",[23,27670,26988],{"href":26987}," within the wider ",[23,27673,5505],{"href":5504},[34,27675,37],{"id":36},[39,27677,27678,27683,27686,27692],{},[42,27679,27680,27681,239],{},"A Hugo site in a Git repository (GitHub or GitLab) that builds locally with ",[253,27682,259],{},[42,27684,27685],{},"A Cloudflare account with Pages enabled (the free plan is sufficient for a static Hugo site).",[42,27687,27688,27689,27691],{},"The exact Hugo version you build with locally — run ",[253,27690,19316],{}," and note it.",[42,27693,27694,27695,27698,27699,27702],{},"Optional: the ",[253,27696,27697],{},"wrangler"," CLI (",[253,27700,27701],{},"npm i -D wrangler",") if you plan to deploy from your own CI or attach a Worker.",[55,27704,27705,27784],{},[58,27706,66,27710,66,27713,66,27716,66,27723],{"viewBox":11210,"role":61,"ariaLabelledBy":27707,"xmlns":65},[27708,27709],"hugocf-flow-title","hugocf-flow-desc",[68,27711,27712],{"id":27708},"Hugo to Cloudflare Pages deploy flow",[72,27714,27715],{"id":27709},"A git push triggers the Cloudflare Pages build image, which installs the pinned Hugo version, runs hugo minify to produce the public directory, and publishes it to the Cloudflare edge. An optional Worker sits in front for dynamic logic.",[76,27717,78,27718,66],{},[80,27719,88,27721,78],{"id":27720,"viewBox":83,"refX":84,"refY":85,"markerWidth":86,"markerHeight":86,"orient":87},"hugocf-arrow",[90,27722],{"d":92,"fill":93},[95,27724,78,27725,78,27728,78,27730,78,27732,78,27735,78,27737,78,27740,78,27743,78,27746,78,27749,78,27751,78,27753,78,27756,78,27758,78,27761,78,27764,78,27767,78,27780,66],{"style":813},[99,27726,27727],{"x":167,"y":109,"fill":103,"style":104},"git push to Cloudflare edge, with an optional Worker in front",[107,27729],{"x":5393,"y":1431,"width":119,"height":1430,"rx":823,"fill":114,"opacity":186,"stroke":114,"style":116},[99,27731,27034],{"x":873,"y":6153,"fill":114,"style":121},[99,27733,27734],{"x":873,"y":11232,"fill":93,"style":126},"main \u002F PR branch",[107,27736],{"x":142,"y":4682,"width":160,"height":1431,"rx":823,"fill":162,"opacity":163,"stroke":164,"style":116},[99,27738,27739],{"x":1462,"y":2563,"fill":103,"style":121},"Pages build image",[99,27741,27742],{"x":1462,"y":19428,"fill":93,"style":126},"HUGO_VERSION=0.128.0",[99,27744,27745],{"x":1462,"y":11232,"fill":93,"style":126},"hugo --minify",[99,27747,27748],{"x":1462,"y":12795,"fill":93,"style":126},"output: public\u002F",[107,27750],{"x":5338,"y":1431,"width":161,"height":1430,"rx":823,"fill":824,"opacity":186,"stroke":824,"style":116},[99,27752,14908],{"x":20139,"y":6153,"fill":824,"style":121},[99,27754,27755],{"x":20139,"y":11232,"fill":93,"style":126},"300+ locations",[107,27757],{"x":6175,"y":1431,"width":161,"height":1430,"rx":823,"fill":185,"opacity":850,"stroke":187,"style":116},[99,27759,27760],{"x":24737,"y":12791,"fill":187,"style":121},"Worker",[99,27762,27763],{"x":24737,"y":194,"fill":93,"style":126},"optional · dynamic",[99,27765,27766],{"x":24737,"y":8703,"fill":93,"style":126},"logic only",[95,27768,88,27769,88,27773,88,27776,78],{"stroke":93,"fill":205,"style":116},[90,27770],{"d":27771,"style":27772},"M160 160 L198 160","marker-end:url(#hugocf-arrow)",[90,27774],{"d":27775,"style":27772},"M380 160 L418 160",[90,27777],{"d":27778,"style":27779},"M570 160 L608 160","stroke-dasharray:5 4;marker-end:url(#hugocf-arrow)",[99,27781,27783],{"x":27782,"y":5407,"fill":93,"style":126},"589","dashed = only when needed",[218,27785,27786],{},"The static path (solid arrows) needs no Worker; the Worker (dashed) is added only when a request needs logic a static file cannot provide.",[34,27788,27790],{"id":27789},"the-working-cloudflare-pages-configuration","The Working Cloudflare Pages Configuration",[14,27792,27793,27794,27796],{},"In the Cloudflare dashboard, ",[229,27795,27105],{},", pick the repo, then set the build settings:",[433,27798,27799,27808],{},[436,27800,27801],{},[439,27802,27803,27806],{},[442,27804,27805],{},"Setting",[442,27807,16267],{},[457,27809,27810,27817,27826,27835],{},[439,27811,27812,27815],{},[462,27813,27814],{},"Framework preset",[462,27816,265],{},[439,27818,27819,27822],{},[462,27820,27821],{},"Build command",[462,27823,27824],{},[253,27825,27745],{},[439,27827,27828,27831],{},[462,27829,27830],{},"Build output directory",[462,27832,27833],{},[253,27834,14988],{},[439,27836,27837,27840],{},[462,27838,27839],{},"Environment variable",[462,27841,27842,27845,27846],{},[253,27843,27844],{},"HUGO_VERSION"," = ",[253,27847,27848],{},"0.128.0",[14,27850,27851,27853,27854,27856,27857,27859,27860,27862],{},[253,27852,27745],{}," strips whitespace from HTML, CSS, and JS in the generated output, and ",[253,27855,14988],{}," is where Hugo writes by default — no extra flags needed. The single most important entry is ",[253,27858,27844],{},". Cloudflare's build image ships a deliberately old Hugo if you don't pin one, and Hugo's template and Markdown behavior shifts between minor versions, so a site that builds locally on 0.128 can fail on Cloudflare's default with errors like \"function does not exist\" or unexpected shortcode output. Set it to the exact version ",[253,27861,19316],{}," printed.",[14,27864,27865,27866,27868,27869,27871],{},"If your theme uses the extended build (Sass\u002FSCSS via Hugo Pipes), also set ",[253,27867,27844],{}," to an ",[229,27870,19295],{}," release — Cloudflare installs the extended binary when the version string resolves to one, and SCSS compilation fails loudly if it doesn't.",[34,27873,27875],{"id":27874},"pinning-the-version-in-the-repo-too","Pinning the Version in the Repo Too",[14,27877,27878,27879,27881],{},"The dashboard variable works, but committing the version keeps local and CI builds in lockstep. Add a ",[253,27880,15754],{},"-style equivalent is not used here; instead Cloudflare reads environment variables. To keep it in the repo, you can drive the version from a build script and read it from a file:",[987,27883,27885],{"className":989,"code":27884,"language":991,"meta":712,"style":712},"# build.sh — committed to the repo, set as the Cloudflare build command\nset -euo pipefail\nHUGO_VERSION=\"0.128.0\"\necho \"Building with Hugo ${HUGO_VERSION}\"\nhugo --minify --gc\n",[253,27886,27887,27892,27903,27912,27924],{"__ignoreMap":712},[995,27888,27889],{"class":997,"line":998},[995,27890,27891],{"class":1001},"# build.sh — committed to the repo, set as the Cloudflare build command\n",[995,27893,27894,27897,27900],{"class":997,"line":713},[995,27895,27896],{"class":1010},"set",[995,27898,27899],{"class":1010}," -euo",[995,27901,27902],{"class":1023}," pipefail\n",[995,27904,27905,27907,27909],{"class":997,"line":730},[995,27906,27844],{"class":1618},[995,27908,7317],{"class":1614},[995,27910,27911],{"class":1023},"\"0.128.0\"\n",[995,27913,27914,27916,27919,27921],{"class":997,"line":1544},[995,27915,18967],{"class":1010},[995,27917,27918],{"class":1023}," \"Building with Hugo ${",[995,27920,27844],{"class":1618},[995,27922,27923],{"class":1023},"}\"\n",[995,27925,27926,27928,27930],{"class":997,"line":1550},[995,27927,259],{"class":1007},[995,27929,3642],{"class":1010},[995,27931,27932],{"class":1010}," --gc\n",[14,27934,27935,27937,27938,27940,27941,27944,27945,239],{},[253,27936,5730],{}," runs garbage collection on the cache after the build (removing stale resources from ",[253,27939,3253],{},"), which keeps the published artifact lean. Set the build command to ",[253,27942,27943],{},"bash build.sh"," and you have one source of truth for flags. Keeping the version in Git also means a reviewer sees the bump in the diff, the same discipline applied across ",[23,27946,26981],{"href":27636},[34,27948,27950],{"id":27949},"caching-the-output-correctly","Caching the Output Correctly",[14,27952,27953,27954,27956,27957,27959,27960,27963,27964,27967,27968,27970,27971,27973,27974,27976,27977,3962],{},"Cloudflare Pages serves your ",[253,27955,14988],{}," directory from the edge, but the repeat-visit win depends on ",[253,27958,14837],{}," headers. Hugo fingerprints assets when you use ",[253,27961,27962],{},"resources.Fingerprint"," in your asset pipeline, producing names like ",[253,27965,27966],{},"main.min.7f3a.css",". Cache those forever and revalidate HTML, using a ",[253,27969,14036],{}," file in your ",[253,27972,19347],{}," directory (Hugo copies ",[253,27975,19347],{}," verbatim into ",[253,27978,8881],{},[987,27980,27983],{"className":27981,"code":27982,"language":99,"meta":712},[11603],"\u002F*.html\n  Cache-Control: public, max-age=0, must-revalidate\n\u002Fcss\u002F*\n  Cache-Control: public, max-age=31536000, immutable\n\u002Fjs\u002F*\n  Cache-Control: public, max-age=31536000, immutable\n",[253,27984,27982],{"__ignoreMap":712},[14,27986,27987,27988,27990],{},"This is the same two-tier policy detailed in ",[23,27989,26988],{"href":26987},". Verify it after a deploy:",[987,27992,27994],{"className":989,"code":27993,"language":991,"meta":712,"style":712},"curl -sI https:\u002F\u002Fyour-project.pages.dev\u002Fcss\u002Fmain.min.7f3a.css | grep -i 'cache-control\\|cf-cache-status'\n",[253,27995,27996],{"__ignoreMap":712},[995,27997,27998,28000,28002,28005,28007,28009,28011],{"class":997,"line":998},[995,27999,14076],{"class":1007},[995,28001,14471],{"class":1010},[995,28003,28004],{"class":1023}," https:\u002F\u002Fyour-project.pages.dev\u002Fcss\u002Fmain.min.7f3a.css",[995,28006,14477],{"class":1614},[995,28008,14480],{"class":1007},[995,28010,14511],{"class":1010},[995,28012,28013],{"class":1023}," 'cache-control\\|cf-cache-status'\n",[14,28015,28016,28017,28019],{},"A second request should report ",[253,28018,14519],{}," on the fingerprinted asset.",[34,28021,1166],{"id":1165},[14,28023,28024,28025,28027],{},"On a 1,200-page Hugo documentation site, the difference between the unpinned default and the configuration above was decisive — the default simply failed, and ",[253,28026,5734],{}," trimmed the payload:",[433,28029,28030,28046],{},[436,28031,28032],{},[439,28033,28034,28037,28040,28043],{},[442,28035,28036],{},"Configuration",[442,28038,28039],{},"Build result",[442,28041,28042],{},"First-load HTML transferred",[442,28044,28045],{},"Build time (Cloudflare)",[457,28047,28048,28063,28078],{},[439,28049,28050,28055,28058,28061],{},[462,28051,14582,28052,28054],{},[253,28053,27844],{}," (default image)",[462,28056,28057],{},"Build fails (template error)",[462,28059,28060],{},"—",[462,28062,28060],{},[439,28064,28065,28070,28073,28075],{},[462,28066,28067,28069],{},[253,28068,27742],{},", no minify",[462,28071,28072],{},"Success",[462,28074,17504],{},[462,28076,28077],{},"39 s",[439,28079,28080,28086,28088,28090],{},[462,28081,28082,10331,28084],{},[253,28083,27742],{},[253,28085,27745],{},[462,28087,28072],{},[462,28089,22008],{},[462,28091,956],{},[14,28093,8896,28094,28096,28097,28100],{},[253,28095,5734],{}," flag cut transferred HTML by roughly 35% (48 KB to 31 KB measured with ",[253,28098,28099],{},"curl -s ... | wc -c"," on the deployed page) for about 2 s of extra build time. The build time itself was read from the Cloudflare Pages deploy log.",[34,28102,28104],{"id":28103},"when-you-actually-need-workers-and-wrangler","When You Actually Need Workers and Wrangler",[14,28106,28107,28108,28111],{},"A plain Hugo site needs ",[229,28109,28110],{},"no Worker at all"," — Pages serves the files directly. Reach for a Worker only when a request needs logic that a static file can't express:",[39,28113,28114,28123,28129,28135],{},[42,28115,28116,28119,28120,28122],{},[229,28117,28118],{},"Edge redirects with logic"," — geo-based or cookie-based routing. (Simple redirects belong in a ",[253,28121,11598],{}," file, which needs no Worker.)",[42,28124,28125,28128],{},[229,28126,28127],{},"Authentication \u002F gating"," — checking a token before serving a protected page.",[42,28130,28131,28134],{},[229,28132,28133],{},"A\u002FB tests"," — rewriting the response or choosing a variant at the edge.",[42,28136,28137,28140],{},[229,28138,28139],{},"API proxying"," — hiding an upstream key, or adding CORS headers to a fetch.",[14,28142,28143,28144,28147,28148,28150],{},"You can attach a Worker to a Pages project as a ",[229,28145,28146],{},"Pages Function"," by adding a ",[253,28149,27560],{}," directory, or deploy a standalone Worker with Wrangler:",[987,28152,28154],{"className":2792,"code":28153,"language":2794,"meta":712,"style":712},"# wrangler.toml\nname = \"hugo-edge-redirects\"\nmain = \"src\u002Fworker.js\"\ncompatibility_date = \"2026-01-01\"\n",[253,28155,28156,28161,28166,28171],{"__ignoreMap":712},[995,28157,28158],{"class":997,"line":998},[995,28159,28160],{},"# wrangler.toml\n",[995,28162,28163],{"class":997,"line":713},[995,28164,28165],{},"name = \"hugo-edge-redirects\"\n",[995,28167,28168],{"class":997,"line":730},[995,28169,28170],{},"main = \"src\u002Fworker.js\"\n",[995,28172,28173],{"class":997,"line":1544},[995,28174,28175],{},"compatibility_date = \"2026-01-01\"\n",[987,28177,28179],{"className":989,"code":28178,"language":991,"meta":712,"style":712},"npx wrangler deploy\n",[253,28180,28181],{"__ignoreMap":712},[995,28182,28183,28185,28188],{"class":997,"line":998},[995,28184,1079],{"class":1007},[995,28186,28187],{"class":1023}," wrangler",[995,28189,28190],{"class":1023}," deploy\n",[14,28192,2360,28193,28196,28197,28201],{},[229,28194,28195],{},"Wrangler for the static site too"," when you build in your own CI rather than Cloudflare's build image — for example to run a link check or Lighthouse gate first. Build locally or in ",[23,28198,28200],{"href":28199},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fgithub-actions-for-automated-ssg-builds\u002F","GitHub Actions for Automated SSG Builds",", then push the finished directory:",[987,28203,28205],{"className":989,"code":28204,"language":991,"meta":712,"style":712},"hugo --minify\nnpx wrangler pages deploy public --project-name hugo-docs\n",[253,28206,28207,28214],{"__ignoreMap":712},[995,28208,28209,28211],{"class":997,"line":998},[995,28210,259],{"class":1007},[995,28212,28213],{"class":1010}," --minify\n",[995,28215,28216,28218,28220,28223,28226,28229,28232],{"class":997,"line":713},[995,28217,1079],{"class":1007},[995,28219,28187],{"class":1023},[995,28221,28222],{"class":1023}," pages",[995,28224,28225],{"class":1023}," deploy",[995,28227,28228],{"class":1023}," public",[995,28230,28231],{"class":1010}," --project-name",[995,28233,28234],{"class":1023}," hugo-docs\n",[14,28236,28237,28238,28240],{},"This bypasses Cloudflare's Git build entirely; your CI owns the Hugo version, so the ",[253,28239,27844],{}," dashboard variable no longer applies.",[34,28242,600],{"id":599},[39,28244,28245,28254,28265,28271,28293,28299],{},[42,28246,28247,28250,28251,28253],{},[229,28248,28249],{},"Unpinned Hugo:"," the number-one failure. Always set ",[253,28252,27844],{}," to an exact value.",[42,28255,28256,28259,28260,28262,28263,239],{},[229,28257,28258],{},"Wrong output directory:"," if you point Pages at ",[253,28261,2242],{}," or the repo root, you get a 404 or the raw repo. Hugo writes to ",[253,28264,14988],{},[42,28266,28267,28270],{},[229,28268,28269],{},"Non-extended version with SCSS:"," SCSS compilation needs an extended Hugo build; pin an extended version.",[42,28272,28273,28279,28280,28283,28284,3725,28286,28288,28289,28292],{},[229,28274,28275,28278],{},[253,28276,28277],{},"baseURL"," mismatch:"," if absolute URLs come out as ",[253,28281,28282],{},"localhost",", set ",[253,28285,28277],{},[253,28287,12901],{}," or pass ",[253,28290,28291],{},"--baseURL https:\u002F\u002Fyour-domain"," so canonical links and sitemaps are correct.",[42,28294,28295,28298],{},[229,28296,28297],{},"Submodule themes:"," if your theme is a Git submodule, Cloudflare clones submodules by default, but a detached or private submodule can fail the checkout — vendor the theme or use a Hugo module instead.",[42,28300,28301,28303,28304,28306,28307,28309],{},[229,28302,637],{}," every Pages deploy is an immutable versioned artifact. In the dashboard, open ",[229,28305,27482],{},", find the last good one, and choose ",[229,28308,27486],{}," — it re-points the live alias in seconds with no rebuild.",[34,28311,642],{"id":641},[14,28313,28314,28315,28317,28318,28320,28321,28323,28324,28326,28327,239],{},"Deploying Hugo to Cloudflare Pages is mostly about getting two settings right: pin ",[253,28316,27844],{}," to the exact version you build with, and point the output directory at ",[253,28319,14988],{},". Add ",[253,28322,27745],{}," to trim the payload, layer the two-tier ",[253,28325,14036],{}," policy for caching, and you have a fast, globally distributed site with no servers to run. Bring in a Worker only when a request genuinely needs edge logic, and reach for Wrangler when you'd rather own the build in your own CI. For the per-branch preview side of the same workflow, see ",[23,28328,28330],{"href":28329},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fpreview-environments-for-pull-requests\u002F","Preview Environments for Pull Requests",[34,28332,651],{"id":650},[653,28334,28336],{"id":28335},"why-does-my-cloudflare-build-use-the-wrong-hugo-version","Why does my Cloudflare build use the wrong Hugo version?",[14,28338,28339,28340,28342,28343,28345],{},"Cloudflare Pages defaults to an old Hugo unless you pin it. Set the ",[253,28341,27844],{}," environment variable to an exact version like ",[253,28344,27848],{}," so the build image installs that binary instead of the stale default, which is the usual cause of works-locally-fails-on-Cloudflare errors.",[653,28347,28349],{"id":28348},"what-build-command-and-output-directory-should-i-use-for-hugo","What build command and output directory should I use for Hugo?",[14,28351,2360,28352,28354,28355,28357,28358,28360],{},[253,28353,27745],{}," as the build command and ",[253,28356,14988],{}," as the output directory. Hugo writes the generated site into ",[253,28359,14988],{}," by default, so pointing Cloudflare Pages at that folder is all the wiring it needs.",[653,28362,28364],{"id":28363},"do-i-need-cloudflare-workers-to-host-a-hugo-site","Do I need Cloudflare Workers to host a Hugo site?",[14,28366,28367],{},"No. A plain Hugo site is fully served by Cloudflare Pages with no Workers involved. You add a Worker only when you need dynamic logic at the edge such as auth, redirects with logic, A\u002FB tests, or API proxying that a static file cannot do.",[653,28369,28371],{"id":28370},"should-i-deploy-with-wrangler-or-the-git-integration","Should I deploy with Wrangler or the Git integration?",[14,28373,28374,28375,28377,28378,239],{},"Use the Git integration for normal pushes because it gives you preview URLs per branch automatically. Reach for Wrangler when you build in your own CI and want to push the finished ",[253,28376,14988],{}," directory yourself with ",[253,28379,28380],{},"wrangler pages deploy",[34,28382,684],{"id":683},[39,28384,28385,28392,28397,28402],{},[42,28386,28387,692,28389,28391],{},[229,28388,691],{},[23,28390,26988],{"href":26987}," — the two-tier caching policy this page reuses.",[42,28393,28394,28396],{},[23,28395,26981],{"href":27636}," — the same host with a different generator.",[42,28398,28399,28401],{},[23,28400,28200],{"href":28199}," — when you'd rather build in CI and deploy with Wrangler.",[42,28403,28404,28406],{},[23,28405,5505],{"href":5504}," — where edge deploys fit the full release lifecycle.",[1346,28408,28409],{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}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 pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}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 .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":712,"searchDepth":713,"depth":713,"links":28411},[28412,28413,28414,28415,28416,28417,28418,28419,28420,28426],{"id":36,"depth":713,"text":37},{"id":27789,"depth":713,"text":27790},{"id":27874,"depth":713,"text":27875},{"id":27949,"depth":713,"text":27950},{"id":1165,"depth":713,"text":1166},{"id":28103,"depth":713,"text":28104},{"id":599,"depth":713,"text":600},{"id":641,"depth":713,"text":642},{"id":650,"depth":713,"text":651,"children":28421},[28422,28423,28424,28425],{"id":28335,"depth":730,"text":28336},{"id":28348,"depth":730,"text":28349},{"id":28363,"depth":730,"text":28364},{"id":28370,"depth":730,"text":28371},{"id":683,"depth":713,"text":684},[28428,28429,28430,28431],{"name":737,"item":738},{"name":5505,"item":5504},{"name":26988,"item":26987},{"name":383,"item":382},"Deploy a Hugo site to Cloudflare Pages — pin HUGO_VERSION, set the build command and public output dir — and learn when Workers and Wrangler earn their place.",[28434,28436,28438,28439],{"q":28336,"a":28435},"Cloudflare Pages defaults to an old Hugo unless you pin it. Set the HUGO_VERSION environment variable to an exact version like 0.128.0 so the build image installs that binary instead of the stale default, which is the usual cause of works-locally-fails-on-Cloudflare errors.",{"q":28349,"a":28437},"Use hugo --minify as the build command and public as the output directory. Hugo writes the generated site into public by default, so pointing Cloudflare Pages at that folder is all the wiring it needs.",{"q":28364,"a":28367},{"q":28371,"a":28440},"Use the Git integration for normal pushes because it gives you preview URLs per branch automatically. Reach for Wrangler when you build in your own CI and want to push the finished public directory yourself with wrangler pages deploy.",{},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fcloudflare-pages-edge-caching-setup\u002Fdeploying-hugo-to-cloudflare-pages-and-workers",{"title":383,"description":28432},"production-ready-deployment-cicd-workflows\u002Fcloudflare-pages-edge-caching-setup\u002Fdeploying-hugo-to-cloudflare-pages-and-workers\u002Findex","kXVNlGv2oJB1Jgj0Dwna1bJfqkBMon_3kqQ2FJSMQNM",{"id":28447,"title":26988,"body":28448,"breadcrumb":29077,"dateModified":743,"datePublished":2446,"description":29081,"extension":745,"faq":29082,"meta":29093,"navigation":752,"path":29094,"seo":29095,"slug":28452,"stem":29096,"type":2460,"__hash__":29097},"content\u002Fproduction-ready-deployment-cicd-workflows\u002Fcloudflare-pages-edge-caching-setup\u002Findex.md",{"type":7,"value":28449,"toc":29058},[28450,28453,28464,28578,28584,28590,28596,28610,28614,28617,28654,28663,28667,28670,28725,28742,28746,28749,28775,28798,28802,28811,28842,28847,28849,28901,28903,28944,28946,28950,28973,28977,28989,28993,29003,29007,29013,29019,29024,29026,29055],[10,28451,26988],{"id":28452},"cloudflare-pages-edge-caching-setup",[14,28454,28455,28456,28458,28459,28461,28462,239],{},"Cloudflare Pages serves your static output from Cloudflare's global edge network, and a ",[253,28457,14036],{}," file is how you take control of the cache. The entire win comes from one split that recurs on every host: hash-fingerprinted assets cached for a year, HTML kept fresh. This guide walks through the ",[253,28460,14036],{}," syntax, edge-cache tuning, purge automation in CI, and the verification that proves it worked — the Cloudflare-specific piece of ",[23,28463,5505],{"href":5504},[55,28465,28466,28575],{},[58,28467,66,28471,66,28474,66,28477,66,28479,66,28568],{"viewBox":24690,"role":61,"ariaLabelledBy":28468,"xmlns":65},[28469,28470],"cfedge-title","cfedge-desc",[68,28472,28473],{"id":28469},"Cloudflare Pages deploy and edge-cache flow",[72,28475,28476],{"id":28470},"A Git push triggers a Pages build that emits hashed assets and a _headers file; the edge applies a one-year immutable cache to assets and a short stale-while-revalidate cache to HTML, and an optional scoped purge runs only on production merges.",[107,28478],{"x":2515,"y":2515,"width":2516,"height":6144,"fill":205},[95,28480,78,28481,78,28484,78,28487,78,28490,78,28493,78,28495,78,28498,78,28501,78,28503,78,28505,78,28508,78,28511,78,28513,78,28516,78,28519,78,28522,78,28524,78,28526,78,28529,78,28533,78,28537,78,28539,78,28541,78,28544,78,28547,78,28550,66],{"style":813},[99,28482,28483],{"x":1415,"y":2521,"fill":103,"style":1416},"From Git push to two cache lifetimes at the edge",[107,28485],{"x":109,"y":849,"width":161,"height":28486,"rx":823,"fill":114,"opacity":186,"stroke":114,"style":116},"76",[99,28488,28489],{"x":3484,"y":3485,"fill":114,"style":121},"Git push",[99,28491,28492],{"x":3484,"y":4661,"fill":93,"style":126},"commit to branch",[107,28494],{"x":3500,"y":849,"width":7852,"height":28486,"rx":823,"fill":824,"opacity":186,"stroke":824,"style":116},[99,28496,28497],{"x":1462,"y":4682,"fill":824,"style":121},"Pages build",[99,28499,28500],{"x":1462,"y":9723,"fill":93,"style":126},"hashed assets",[99,28502,27058],{"x":1462,"y":119,"fill":93,"style":126},[107,28504],{"x":101,"y":849,"width":7852,"height":28486,"rx":823,"fill":162,"opacity":877,"stroke":164,"style":116},[99,28506,28507],{"x":885,"y":3485,"fill":103,"style":121},"Global edge",[99,28509,28510],{"x":885,"y":4661,"fill":93,"style":126},"300+ PoPs",[107,28512],{"x":7842,"y":849,"width":142,"height":28486,"rx":823,"fill":2564,"opacity":115,"stroke":2565,"style":116},[99,28514,28515],{"x":3558,"y":4682,"fill":2565,"style":121},"Scoped purge",[99,28517,28518],{"x":3558,"y":9723,"fill":93,"style":126},"production merges",[99,28520,28521],{"x":3558,"y":119,"fill":93,"style":4658},"files array only",[107,28523],{"x":6144,"y":142,"width":142,"height":13902,"rx":823,"fill":185,"opacity":825,"stroke":187,"style":116},[99,28525,15533],{"x":6171,"y":2596,"fill":187,"style":121},[99,28527,28528],{"x":6171,"y":21557,"fill":93,"style":126},"app.abc123.js",[99,28530,28532],{"x":6171,"y":28531,"fill":103,"style":126},"274","max-age 1yr · immutable",[99,28534,28536],{"x":6171,"y":28535,"fill":93,"style":4658},"294","never revalidated",[107,28538],{"x":10820,"y":142,"width":4634,"height":13902,"rx":823,"fill":2564,"opacity":6172,"stroke":2565,"style":116},[99,28540,15551],{"x":11255,"y":2596,"fill":2565,"style":121},[99,28542,28543],{"x":11255,"y":21557,"fill":93,"style":126},"\u002Findex.html · \u002Fguide\u002F",[99,28545,28546],{"x":11255,"y":28531,"fill":103,"style":126},"s-maxage 300 + SWR",[99,28548,28549],{"x":11255,"y":28535,"fill":93,"style":4658},"fresh, revalidated",[95,28551,88,28552,88,28556,88,28559,88,28562,88,28565,78],{"stroke":93,"fill":205,"style":116},[90,28553],{"d":28554,"style":28555},"M180 108 L208 108","marker-end:url(#cfedge-arrow)",[90,28557],{"d":28558,"style":28555},"M370 108 L398 108",[90,28560],{"d":28561,"style":28555},"M560 108 L588 108",[90,28563],{"d":28564,"style":28555},"M480 146 L460 198",[90,28566],{"d":28567,"style":28555},"M500 146 L640 198",[76,28569,78,28570,66],{},[80,28571,88,28573,78],{"id":28572,"viewBox":83,"refX":84,"refY":85,"markerWidth":86,"markerHeight":86,"orient":87},"cfedge-arrow",[90,28574],{"d":92,"fill":93},[218,28576,28577],{},"A Pages build emits hashed assets and a `_headers` file; the edge caches assets immutably for a year and HTML briefly with background revalidation, while purges stay scoped to production merges.",[34,28579,28581,28582],{"id":28580},"how-pages-reads-_headers","How Pages Reads ",[253,28583,14036],{},[14,28585,28586,28587,28589],{},"Cloudflare Pages processes a ",[253,28588,14036],{}," file found at the root of your published output directory automatically — no dashboard configuration, no build plugin. Each rule is a path glob followed by indented header lines. Long-cache hashed assets, and give HTML a short shared-cache TTL with background revalidation:",[987,28591,28594],{"className":28592,"code":28593,"language":99,"meta":712},[11603],"\u002Fassets\u002F*\n  Cache-Control: public, max-age=31536000, immutable\n\n\u002F*.html\n  Cache-Control: public, max-age=0, s-maxage=300, stale-while-revalidate=86400\n",[253,28595,28593],{"__ignoreMap":712},[14,28597,28598,28600,28601,28603,28604,28606,28607,28609],{},[253,28599,27321],{}," controls the edge cache while ",[253,28602,15059],{}," keeps the browser revalidating, and ",[253,28605,14131],{}," lets the edge serve a slightly stale page while it refreshes in the background. This is safe only because static generators emit content-hashed asset filenames — a new build produces new URLs — so caching the old ones forever can never serve a wrong asset. Author the file in your input directory and let your generator copy it through to the output; the ",[23,28608,26981],{"href":27636}," guide shows the passthrough-copy step in full.",[34,28611,28613],{"id":28612},"tuning-the-two-tiers","Tuning the Two Tiers",[14,28615,28616],{},"The two tiers fail for opposite reasons, so they get opposite policies. Assets are immutable because their URL changes whenever their content does; HTML is mutable because the same URL must always point at the latest build. Switching from Pages' conservative defaults to an explicit immutable rule on assets produces a sharp repeat-visit improvement:",[433,28618,28619,28629],{},[436,28620,28621],{},[439,28622,28623,28625,28627],{},[442,28624,940],{},[442,28626,15658],{},[442,28628,14092],{},[457,28630,28631,28642],{},[439,28632,28633,28637,28639],{},[462,28634,15174,28635,982],{},[253,28636,14036],{},[462,28638,15184],{},[462,28640,28641],{},"590 ms",[439,28643,28644,28648,28651],{},[462,28645,15679,28646,14113],{},[253,28647,11756],{},[462,28649,28650],{},"1 (HTML revalidation only)",[462,28652,28653],{},"160 ms",[14,28655,28656,28657,28659,28660,28662],{},"The first visit is identical in both rows; the entire win is on repeat navigation, where ",[253,28658,11756],{}," lets the browser skip revalidation for every hashed asset. The HTML tier trades a few hundred milliseconds of edge freshness for instant rollbacks — the moment you re-point to a previous deploy, the short ",[253,28661,27321],{}," means the edge picks it up within the window rather than serving the rolled-back release for hours.",[34,28664,28666],{"id":28665},"cache-invalidation-in-ci","Cache Invalidation in CI",[14,28668,28669],{},"A new Pages deploy invalidates the files that changed, so you rarely purge manually. When you must — for example clearing an external Cloudflare zone cache that fronts Pages — trigger it only on production merges and scope it as tightly as you can:",[987,28671,28673],{"className":1912,"code":28672,"language":1914,"meta":712,"style":712},"- name: Purge Cloudflare cache\n  if: github.ref == 'refs\u002Fheads\u002Fmain'\n  run: |\n    curl -X POST \\\n      \"https:\u002F\u002Fapi.cloudflare.com\u002Fclient\u002Fv4\u002Fzones\u002F${{ secrets.CF_ZONE_ID }}\u002Fpurge_cache\" \\\n      -H \"Authorization: Bearer ${{ secrets.CF_API_TOKEN }}\" \\\n      -H \"Content-Type: application\u002Fjson\" \\\n      --data '{\"files\":[\"https:\u002F\u002Fexample.com\u002Findex.html\"]}'\n",[253,28674,28675,28686,28695,28703,28707,28712,28716,28720],{"__ignoreMap":712},[995,28676,28677,28679,28681,28683],{"class":997,"line":998},[995,28678,3191],{"class":1618},[995,28680,1922],{"class":1921},[995,28682,1925],{"class":1618},[995,28684,28685],{"class":1023},"Purge Cloudflare cache\n",[995,28687,28688,28690,28692],{"class":997,"line":713},[995,28689,18900],{"class":1921},[995,28691,1925],{"class":1618},[995,28693,28694],{"class":1023},"github.ref == 'refs\u002Fheads\u002Fmain'\n",[995,28696,28697,28699,28701],{"class":997,"line":730},[995,28698,14197],{"class":1921},[995,28700,1925],{"class":1618},[995,28702,3215],{"class":1614},[995,28704,28705],{"class":997,"line":1544},[995,28706,14206],{"class":1023},[995,28708,28709],{"class":997,"line":1550},[995,28710,28711],{"class":1023},"      \"https:\u002F\u002Fapi.cloudflare.com\u002Fclient\u002Fv4\u002Fzones\u002F${{ secrets.CF_ZONE_ID }}\u002Fpurge_cache\" \\\n",[995,28713,28714],{"class":997,"line":1673},[995,28715,14216],{"class":1023},[995,28717,28718],{"class":997,"line":1678},[995,28719,14221],{"class":1023},[995,28721,28722],{"class":997,"line":1693},[995,28723,28724],{"class":1023},"      --data '{\"files\":[\"https:\u002F\u002Fexample.com\u002Findex.html\"]}'\n",[14,28726,28727,28728,28731,28732,28734,28735,28738,28739,28741],{},"Scope the API token to ",[253,28729,28730],{},"Zone → Cache Purge"," only, and prefer a ",[253,28733,27549],{}," array over ",[253,28736,28737],{},"purge_everything"," whenever you can compute the changed URLs — a full purge cold-starts the cache and spikes origin load. Wire this into your build job alongside ",[23,28740,28200],{"href":28199}," so the purge runs only after a successful production deploy.",[34,28743,28745],{"id":28744},"verifying-the-edge-cache","Verifying the Edge Cache",[14,28747,28748],{},"Local dev does not reproduce edge headers, so confirm caching against the deployed URL by reading the response headers:",[987,28750,28752],{"className":989,"code":28751,"language":991,"meta":712,"style":712},"curl -s -I https:\u002F\u002Fexample.com\u002Findex.html | grep -iE 'cf-cache-status|cache-control|age'\n",[253,28753,28754],{"__ignoreMap":712},[995,28755,28756,28758,28761,28763,28766,28768,28770,28772],{"class":997,"line":998},[995,28757,14076],{"class":1007},[995,28759,28760],{"class":1010}," -s",[995,28762,15089],{"class":1010},[995,28764,28765],{"class":1023}," https:\u002F\u002Fexample.com\u002Findex.html",[995,28767,14477],{"class":1614},[995,28769,14480],{"class":1007},[995,28771,14483],{"class":1010},[995,28773,28774],{"class":1023}," 'cf-cache-status|cache-control|age'\n",[14,28776,28777,28779,28780,2204,28782,28784,28785,28787,28788,28790,28791,28793,28794,239],{},[253,28778,14519],{}," means it served from the edge; ",[253,28781,14685],{},[253,28783,15136],{}," means it reached origin. The ",[253,28786,14693],{}," header climbs toward your ",[253,28789,27321],{}," on repeat requests, which is your proof the edge is holding the page. Run the same check on an asset URL and confirm it carries the one-year ",[253,28792,11756],{}," value. Benchmark the resulting TTFB against other hosts using the comparison in ",[23,28795,28797],{"href":28796},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fnetlify-vs-vercel-deployment-strategies\u002F","Netlify vs Vercel Deployment Strategies",[34,28799,28801],{"id":28800},"framework-output-directories-routing","Framework Output Directories & Routing",[14,28803,28804,28805,28807,28808,28810],{},"Point the build at the right output directory and keep a ",[253,28806,11598],{}," file alongside ",[253,28809,14036],{}," so unmatched routes hit your fallback instead of a bare 404:",[39,28812,28813,28823,28831],{},[42,28814,28815,692,28818,7242,28820,28822],{},[229,28816,28817],{},"Astro:",[253,28819,8885],{},[253,28821,2986],{}," emits fingerprinted assets out of the box.",[42,28824,28825,692,28828,28830],{},[229,28826,28827],{},"Eleventy \u002F Jekyll:",[253,28829,7303],{}," — the shared default for both.",[42,28832,28833,692,28836,28838,28839,28841],{},[229,28834,28835],{},"Hugo:",[253,28837,8881],{}," — enable ",[253,28840,21517],{}," so asset hashing stays stable across builds.",[14,28843,28844,28845,239],{},"For Hugo specifically, you can push further than static hosting and put dynamic logic at the edge with Pages Functions and Workers — that path is covered in ",[23,28846,383],{"href":382},[34,28848,2266],{"id":2265},[39,28850,28851,28863,28873,28882,28892],{},[42,28852,28853,28858,28859,14637,28861,239],{},[229,28854,28855,28856,14585],{},"Long ",[253,28857,14636],{}," serves stale pages that reference assets which no longer exist, and breaks rollbacks. Use short ",[253,28860,27321],{},[253,28862,14131],{},[42,28864,28865,28869,28870,28872],{},[229,28866,16685,28867,931],{},[253,28868,11598],{}," without it, unmatched routes 404 instead of hitting your fallback. Ship it next to ",[253,28871,14036],{}," in the output directory.",[42,28874,28875,28878,28879,28881],{},[229,28876,28877],{},"Purging on every commit:"," a full purge cold-starts the cache and spikes origin load. Limit purges to production merges and prefer a ",[253,28880,27549],{}," array.",[42,28883,28884,28889,28890,15259],{},[229,28885,28886,28888],{},[253,28887,14036],{}," in the wrong place:"," the file must be at the root of the published output. A malformed glob is dropped silently, so verify with ",[253,28891,14076],{},[42,28893,28894,28897,28898,28900],{},[229,28895,28896],{},"Over-broad API token:"," an account-wide token in CI is a liability. Scope it to ",[253,28899,28730],{}," and nothing more.",[34,28902,2321],{"id":2320},[39,28904,28905,28919,28925,28928],{},[42,28906,28907,28908,28910,28911,28913,28914,14637,28916,28918],{},"One ",[253,28909,14036],{}," file controls everything: ",[253,28912,11756],{}," year-long caching for hashed assets, short ",[253,28915,27321],{},[253,28917,14131],{}," for HTML.",[42,28920,28921,28922,28924],{},"The repeat-visit win comes entirely from ",[253,28923,11756],{}," assets, which let the browser skip revalidation.",[42,28926,28927],{},"Let per-deploy invalidation do the work; reach for the purge API only on production merges, scoped to changed files.",[42,28929,28930,28931,28933,28934,7242,28936,28938,28939,738,28941,28943],{},"Verify with ",[253,28932,14623],{}," and read ",[253,28935,14679],{},[253,28937,14682],{}," is edge, ",[253,28940,14685],{},[253,28942,15136],{}," is origin.",[34,28945,651],{"id":650},[653,28947,28949],{"id":28948},"how-do-i-confirm-content-is-being-served-from-cloudflares-edge","How do I confirm content is being served from Cloudflare's edge?",[14,28951,28952,28953,28955,28956,28958,28959,2204,28961,28963,28964,28966,28967,28969,28970,28972],{},"Read the ",[253,28954,14679],{}," response header. ",[253,28957,14682],{}," means the edge served it, ",[253,28960,14685],{},[253,28962,15136],{}," means it reached the origin. Run ",[253,28965,14623],{}," against an HTML route and an asset and watch the ",[253,28968,14693],{}," header climb toward your ",[253,28971,27321],{}," on repeat requests.",[653,28974,28976],{"id":28975},"what-is-the-recommended-cache-policy-for-ssg-html-on-pages","What is the recommended cache policy for SSG HTML on Pages?",[14,28978,28979,28980,28982,28983,28985,28986,28988],{},"Keep HTML short-lived with ",[253,28981,15059],{}," so the browser revalidates, an ",[253,28984,27321],{}," around 300 seconds so the edge caches it briefly, and ",[253,28987,14131],{}," so the edge can serve a slightly stale page while it refreshes in the background.",[653,28990,28992],{"id":28991},"does-cloudflare-pages-cache-assets-automatically","Does Cloudflare Pages cache assets automatically?",[14,28994,28995,28996,28998,28999,29002],{},"It does, but conservatively. Add a ",[253,28997,14036],{}," rule applying ",[253,29000,29001],{},"public, max-age=31536000, immutable"," to your hashed asset paths so browsers skip revalidation entirely and serve those files straight from local cache for a year.",[653,29004,29006],{"id":29005},"how-do-i-invalidate-specific-paths-after-a-deploy","How do I invalidate specific paths after a deploy?",[14,29008,29009,29010,29012],{},"Usually you do not need to, because a new Pages deploy invalidates the files that changed. When you must clear an external zone cache, call the purge API with a ",[253,29011,27549],{}," array of the exact URLs rather than purging everything, and only on production merges.",[653,29014,15796,29016,29018],{"id":29015},"why-are-my-_headers-rules-being-ignored",[253,29017,14036],{}," rules being ignored?",[14,29020,29021,29022,239],{},"The file must sit at the root of the published output directory and the globs must be valid. A malformed rule is dropped silently and the path falls back to defaults, so check the deploy log for parse warnings and verify the live response with ",[253,29023,14076],{},[34,29025,684],{"id":683},[39,29027,29028,29035,29040,29045,29050],{},[42,29029,29030,692,29032,29034],{},[229,29031,691],{},[23,29033,5505],{"href":5504}," — where edge caching fits the deploy lifecycle.",[42,29036,29037,29039],{},[23,29038,26981],{"href":27636}," — the end-to-end Git-connected setup.",[42,29041,29042,29044],{},[23,29043,383],{"href":382}," — pushing dynamic logic to the edge.",[42,29046,29047,29049],{},[23,29048,28200],{"href":28199}," — wiring the build and purge into CI.",[42,29051,29052,29054],{},[23,29053,28797],{"href":28796}," — benchmark Cloudflare's edge against other hosts.\n\n",[1346,29056,29057],{},"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 .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}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 .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}",{"title":712,"searchDepth":713,"depth":713,"links":29059},[29060,29062,29063,29064,29065,29066,29067,29068,29076],{"id":28580,"depth":713,"text":29061},"How Pages Reads _headers",{"id":28612,"depth":713,"text":28613},{"id":28665,"depth":713,"text":28666},{"id":28744,"depth":713,"text":28745},{"id":28800,"depth":713,"text":28801},{"id":2265,"depth":713,"text":2266},{"id":2320,"depth":713,"text":2321},{"id":650,"depth":713,"text":651,"children":29069},[29070,29071,29072,29073,29074],{"id":28948,"depth":730,"text":28949},{"id":28975,"depth":730,"text":28976},{"id":28991,"depth":730,"text":28992},{"id":29005,"depth":730,"text":29006},{"id":29015,"depth":730,"text":29075},"Why are my _headers rules being ignored?",{"id":683,"depth":713,"text":684},[29078,29079,29080],{"name":737,"item":738},{"name":5505,"item":5504},{"name":26988,"item":26987},"Control Cloudflare Pages caching with a _headers file: immutable year-long caching for fingerprinted assets, fresh HTML, scoped purges, and cf-cache-status verification.",[29083,29085,29087,29089,29091],{"q":28949,"a":29084},"Read the cf-cache-status response header. HIT means the edge served it, MISS or DYNAMIC means it reached the origin. Run curl with the head flag against an HTML route and an asset and watch the age header climb toward your s-maxage on repeat requests.",{"q":28976,"a":29086},"Keep HTML short-lived with max-age zero so the browser revalidates, an s-maxage around 300 seconds so the edge caches it briefly, and stale-while-revalidate so the edge can serve a slightly stale page while it refreshes in the background.",{"q":28992,"a":29088},"It does, but conservatively. Add a _headers rule applying public max-age 31536000 immutable to your hashed asset paths so browsers skip revalidation entirely and serve those files straight from local cache for a year.",{"q":29006,"a":29090},"Usually you do not need to, because a new Pages deploy invalidates the files that changed. When you must clear an external zone cache, call the purge API with a files array of the exact URLs rather than purging everything, and only on production merges.",{"q":29075,"a":29092},"The file must sit at the root of the published output directory and the globs must be valid. A malformed rule is dropped silently and the path falls back to defaults, so check the deploy log for parse warnings and verify the live response with curl.",{},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fcloudflare-pages-edge-caching-setup",{"title":26988,"description":29081},"production-ready-deployment-cicd-workflows\u002Fcloudflare-pages-edge-caching-setup\u002Findex","3PZlLW2tY9bkZaeBkqEz3_VjeQ2P0EozRsnp6q1zxvk",{"id":29099,"title":29100,"body":29101,"breadcrumb":29905,"dateModified":743,"datePublished":743,"description":29910,"extension":745,"faq":29911,"meta":29920,"navigation":752,"path":29921,"seo":29922,"slug":29923,"stem":29924,"type":756,"__hash__":29925},"content\u002Fproduction-ready-deployment-cicd-workflows\u002Fgithub-actions-for-automated-ssg-builds\u002Fcaching-node-modules-in-github-actions-for-faster-ssg-builds\u002Findex.md","Caching node_modules in GitHub Actions",{"type":7,"value":29102,"toc":29884},[29103,29106,29114,29116,29135,29228,29235,29251,29315,29340,29387,29406,29414,29431,29536,29559,29566,29585,29587,29597,29668,29685,29689,29707,29709,29767,29769,29797,29799,29803,29824,29828,29835,29839,29845,29849,29855,29857,29881],[10,29104,1049],{"id":29105},"caching-node_modules-in-github-actions-for-faster-ssg-builds",[14,29107,29108,29109,29111,29112,239],{},"Every static site built on a Node toolchain — Astro, Eleventy, an Eleventy-driven Jekyll alternative, or a Next.js static export — pays an install tax on every CI run. Re-downloading and re-installing the same dependency tree on a cold runner is pure waste, and it's the easiest minutes you'll ever recover. The fix is dependency caching keyed on your lockfile, so a run only reinstalls when dependencies genuinely change. This guide shows the two ways to do it, when to choose each, and the measured time saved. It sits under ",[23,29110,28200],{"href":28199}," within ",[23,29113,5505],{"href":5504},[34,29115,37],{"id":36},[39,29117,29118,29129,29132],{},[42,29119,29120,29121,1850,29123,10335,29126,260],{},"An SSG repo with a committed lockfile (",[253,29122,26998],{},[253,29124,29125],{},"yarn.lock",[253,29127,29128],{},"pnpm-lock.yaml",[42,29130,29131],{},"A GitHub Actions workflow that already runs an install + build (even an unoptimized one).",[42,29133,29134],{},"Builds triggered often enough that install time matters — every PR push and merge.",[55,29136,29137,29225],{},[58,29138,66,29143,66,29146,66,29149,66,29156],{"viewBox":29139,"role":61,"ariaLabelledBy":29140,"xmlns":65},"0 0 800 330",[29141,29142],"npmcache-flow-title","npmcache-flow-desc",[68,29144,29145],{"id":29141},"Lockfile-hash cache key and restore flow",[72,29147,29148],{"id":29142},"The workflow hashes the lockfile to form a cache key. On a hit the npm download cache is restored and npm ci installs from it quickly. On a miss npm downloads from the registry and the run saves a new cache under the same key.",[76,29150,78,29151,66],{},[80,29152,88,29154,78],{"id":29153,"viewBox":83,"refX":84,"refY":85,"markerWidth":86,"markerHeight":86,"orient":87},"npmcache-arrow",[90,29155],{"d":92,"fill":93},[95,29157,78,29158,78,29161,78,29163,78,29166,78,29168,78,29170,78,29173,78,29176,78,29178,78,29180,78,29183,78,29186,78,29188,78,29190,78,29193,78,29196,78,29198,78,29201,78,29205,78,29220,78,29222,66],{"style":813},[99,29159,29160],{"x":101,"y":102,"fill":103,"style":104},"Lockfile hash decides: restore the cache or rebuild it",[107,29162],{"x":109,"y":2563,"width":7852,"height":1430,"rx":823,"fill":114,"opacity":186,"stroke":114,"style":116},[99,29164,29165],{"x":159,"y":18406,"fill":114,"style":121},"hashFiles",[99,29167,26998],{"x":159,"y":845,"fill":93,"style":126},[107,29169],{"x":184,"y":2563,"width":161,"height":1430,"rx":823,"fill":162,"opacity":163,"stroke":164,"style":116},[99,29171,29172],{"x":23265,"y":171,"fill":103,"style":121},"cache key",[99,29174,29175],{"x":23265,"y":8703,"fill":93,"style":126},"hit? miss?",[107,29177],{"x":863,"y":110,"width":194,"height":1430,"rx":823,"fill":185,"opacity":850,"stroke":187,"style":116},[99,29179,14682],{"x":15550,"y":873,"fill":187,"style":121},[99,29181,29182],{"x":15550,"y":125,"fill":93,"style":126},"restore ~\u002F.npm",[99,29184,29185],{"x":15550,"y":2563,"fill":93,"style":126},"npm ci · ~12 s",[107,29187],{"x":863,"y":142,"width":194,"height":1430,"rx":823,"fill":2564,"opacity":825,"stroke":2565,"style":116},[99,29189,14685],{"x":15550,"y":4634,"fill":2565,"style":121},[99,29191,29192],{"x":15550,"y":21557,"fill":93,"style":126},"download registry",[99,29194,29195],{"x":15550,"y":5332,"fill":93,"style":126},"npm ci · ~45 s",[107,29197],{"x":2562,"y":142,"width":1431,"height":1430,"rx":823,"fill":824,"opacity":186,"stroke":824,"style":116},[99,29199,29200],{"x":11910,"y":15982,"fill":824,"style":121},"save cache",[99,29202,29204],{"x":11910,"y":29203,"fill":93,"style":126},"258","under key",[95,29206,88,29207,88,29211,88,29214,88,29217,78],{"stroke":93,"fill":205,"style":116},[90,29208],{"d":29209,"style":29210},"M190 170 L238 170","marker-end:url(#npmcache-arrow)",[90,29212],{"d":29213,"style":29210},"M390 158 L448 110",[90,29215],{"d":29216,"style":29210},"M390 182 L448 235",[90,29218],{"d":29219,"style":29210},"M620 240 L658 240",[99,29221,4241],{"x":5338,"y":4682,"fill":187,"style":2624},[99,29223,29224],{"x":5338,"y":3500,"fill":2565,"style":2624},"miss",[218,29226,29227],{},"The lockfile hash is the whole mechanism: it changes only when dependencies change, so most runs hit the cache and skip the registry entirely.",[34,29229,29231,29232],{"id":29230},"option-a-the-built-in-cache-on-actionssetup-node","Option A: The Built-In Cache on ",[253,29233,29234],{},"actions\u002Fsetup-node",[14,29236,29237,29238,29240,29241,29244,29245,1781,29248,29250],{},"For most SSG repos this is all you need. ",[253,29239,29234],{}," has a ",[253,29242,29243],{},"cache"," input that caches the ",[229,29246,29247],{},"package manager's download cache",[253,29249,2046],{}," for npm) and keys it on the detected lockfile automatically:",[987,29252,29254],{"className":1912,"code":29253,"language":1914,"meta":712,"style":712},"- uses: actions\u002Fsetup-node@v4\n  with:\n    node-version: 20\n    cache: npm\n\n- run: npm ci\n- run: npm run build\n",[253,29255,29256,29266,29272,29282,29291,29295,29305],{"__ignoreMap":712},[995,29257,29258,29260,29262,29264],{"class":997,"line":998},[995,29259,3191],{"class":1618},[995,29261,1978],{"class":1921},[995,29263,1925],{"class":1618},[995,29265,1994],{"class":1023},[995,29267,29268,29270],{"class":997,"line":713},[995,29269,3203],{"class":1921},[995,29271,1946],{"class":1618},[995,29273,29274,29277,29279],{"class":997,"line":730},[995,29275,29276],{"class":1921},"    node-version",[995,29278,1925],{"class":1618},[995,29280,29281],{"class":1010},"20\n",[995,29283,29284,29287,29289],{"class":997,"line":1544},[995,29285,29286],{"class":1921},"    cache",[995,29288,1925],{"class":1618},[995,29290,2021],{"class":1023},[995,29292,29293],{"class":997,"line":1550},[995,29294,1541],{"emptyLinePlaceholder":752},[995,29296,29297,29299,29301,29303],{"class":997,"line":1673},[995,29298,3191],{"class":1618},[995,29300,2028],{"class":1921},[995,29302,1925],{"class":1618},[995,29304,12365],{"class":1023},[995,29306,29307,29309,29311,29313],{"class":997,"line":1678},[995,29308,3191],{"class":1618},[995,29310,2028],{"class":1921},[995,29312,1925],{"class":1618},[995,29314,12386],{"class":1023},[14,29316,29317,29318,29320,29321,29323,29324,29326,29327,29329,29330,2204,29333,29336,29337,931],{},"That's the entire optimization. ",[253,29319,2042],{}," finds ",[253,29322,26998],{},", hashes it, restores ",[253,29325,2046],{}," on a hit, and saves it after the run. ",[253,29328,2072],{}," then installs from the local cache instead of hitting the registry. Use ",[253,29331,29332],{},"cache: yarn",[253,29334,29335],{},"cache: pnpm"," for the other managers. If your lockfile isn't at the repo root (a monorepo), point at it with ",[253,29338,29339],{},"cache-dependency-path",[987,29341,29343],{"className":1912,"code":29342,"language":1914,"meta":712,"style":712},"- uses: actions\u002Fsetup-node@v4\n  with:\n    node-version: 20\n    cache: npm\n    cache-dependency-path: sites\u002Fdocs\u002Fpackage-lock.json\n",[253,29344,29345,29355,29361,29369,29377],{"__ignoreMap":712},[995,29346,29347,29349,29351,29353],{"class":997,"line":998},[995,29348,3191],{"class":1618},[995,29350,1978],{"class":1921},[995,29352,1925],{"class":1618},[995,29354,1994],{"class":1023},[995,29356,29357,29359],{"class":997,"line":713},[995,29358,3203],{"class":1921},[995,29360,1946],{"class":1618},[995,29362,29363,29365,29367],{"class":997,"line":730},[995,29364,29276],{"class":1921},[995,29366,1925],{"class":1618},[995,29368,29281],{"class":1010},[995,29370,29371,29373,29375],{"class":997,"line":1544},[995,29372,29286],{"class":1921},[995,29374,1925],{"class":1618},[995,29376,2021],{"class":1023},[995,29378,29379,29382,29384],{"class":997,"line":1550},[995,29380,29381],{"class":1921},"    cache-dependency-path",[995,29383,1925],{"class":1618},[995,29385,29386],{"class":1023},"sites\u002Fdocs\u002Fpackage-lock.json\n",[14,29388,29389,692,29397,29399,29400,29402,29403,29405],{},[229,29390,29391,29392,29394,29395,931],{},"Why ",[253,29393,2072],{}," and not ",[253,29396,27449],{},[253,29398,2072],{}," installs strictly from the lockfile, deletes any existing ",[253,29401,417],{}," first, and errors if the lockfile and ",[253,29404,21912],{}," disagree. That determinism is exactly what a reproducible CI build wants, and it pairs cleanly with a restored download cache.",[34,29407,29409,29410,29413],{"id":29408},"option-b-actionscache-for-full-control","Option B: ",[253,29411,29412],{},"actions\u002Fcache"," for Full Control",[14,29415,29416,29417,29419,29420,29422,29423,1850,29425,29427,29428,29430],{},"When you need to cache something ",[253,29418,2038],{}," doesn't — the SSG's own build cache (",[253,29421,3170],{},", Hugo's ",[253,29424,3253],{},[253,29426,2309],{},") — reach for ",[253,29429,29412],{}," directly with a lockfile-hash key:",[987,29432,29434],{"className":1912,"code":29433,"language":1914,"meta":712,"style":712},"- uses: actions\u002Fsetup-node@v4\n  with:\n    node-version: 20\n\n- name: Cache npm download cache\n  uses: actions\u002Fcache@v4\n  with:\n    path: ~\u002F.npm\n    key: deps-node20-${{ hashFiles('package-lock.json') }}\n    restore-keys: |\n      deps-node20-\n\n- run: npm ci\n",[253,29435,29436,29446,29452,29460,29464,29475,29484,29490,29499,29508,29517,29522,29526],{"__ignoreMap":712},[995,29437,29438,29440,29442,29444],{"class":997,"line":998},[995,29439,3191],{"class":1618},[995,29441,1978],{"class":1921},[995,29443,1925],{"class":1618},[995,29445,1994],{"class":1023},[995,29447,29448,29450],{"class":997,"line":713},[995,29449,3203],{"class":1921},[995,29451,1946],{"class":1618},[995,29453,29454,29456,29458],{"class":997,"line":730},[995,29455,29276],{"class":1921},[995,29457,1925],{"class":1618},[995,29459,29281],{"class":1010},[995,29461,29462],{"class":997,"line":1544},[995,29463,1541],{"emptyLinePlaceholder":752},[995,29465,29466,29468,29470,29472],{"class":997,"line":1550},[995,29467,3191],{"class":1618},[995,29469,1922],{"class":1921},[995,29471,1925],{"class":1618},[995,29473,29474],{"class":1023},"Cache npm download cache\n",[995,29476,29477,29480,29482],{"class":997,"line":1673},[995,29478,29479],{"class":1921},"  uses",[995,29481,1925],{"class":1618},[995,29483,3198],{"class":1023},[995,29485,29486,29488],{"class":997,"line":1678},[995,29487,3203],{"class":1921},[995,29489,1946],{"class":1618},[995,29491,29492,29494,29496],{"class":997,"line":1693},[995,29493,3210],{"class":1921},[995,29495,1925],{"class":1618},[995,29497,29498],{"class":1023},"~\u002F.npm\n",[995,29500,29501,29503,29505],{"class":997,"line":1705},[995,29502,3235],{"class":1921},[995,29504,1925],{"class":1618},[995,29506,29507],{"class":1023},"deps-node20-${{ hashFiles('package-lock.json') }}\n",[995,29509,29510,29513,29515],{"class":997,"line":1711},[995,29511,29512],{"class":1921},"    restore-keys",[995,29514,1925],{"class":1618},[995,29516,3215],{"class":1614},[995,29518,29519],{"class":997,"line":1717},[995,29520,29521],{"class":1023},"      deps-node20-\n",[995,29523,29524],{"class":997,"line":1726},[995,29525,1541],{"emptyLinePlaceholder":752},[995,29527,29528,29530,29532,29534],{"class":997,"line":1732},[995,29529,3191],{"class":1618},[995,29531,2028],{"class":1921},[995,29533,1925],{"class":1618},[995,29535,12365],{"class":1023},[14,29537,8896,29538,29541,29542,29544,29545,29548,29549,29552,29553,29556,29557,239],{},[253,29539,29540],{},"key"," changes only when ",[253,29543,26998],{}," changes, so you reinstall exactly when dependencies move and hit the cache every other run. The ",[253,29546,29547],{},"restore-keys"," prefix is the safety net: on a key miss it restores the most recent cache that starts with ",[253,29550,29551],{},"deps-node20-",", so even a lockfile bump starts from a warm cache and only downloads the delta. Including ",[253,29554,29555],{},"node20"," in the key prevents a cache built on one Node major from being restored under another, which is the same hashing discipline used for ",[23,29558,5002],{"href":5001},[653,29560,29562,29563,29565],{"id":29561},"dont-cache-node_modules-directly","Don't cache ",[253,29564,417],{}," directly",[14,29567,29568,29569,29571,29572,29575,29576,29578,29579,29581,29582,29584],{},"It's tempting to cache ",[253,29570,417],{}," itself, but it's fragile: the tree contains platform-specific binaries (Sharp, esbuild, ",[253,29573,29574],{},"node-gyp"," output), so a cache built on one runner image or Node version can restore a subtly broken tree that fails at build time, not install time. Caching ",[253,29577,2046],{}," and running ",[253,29580,2072],{}," rebuilds ",[253,29583,417],{}," cleanly from a warm download cache — you get most of the speed with none of the corruption risk.",[34,29586,1166],{"id":1165},[14,29588,29589,29590,29593,29594,29596],{},"On an Eleventy documentation site with ~310 packages in the lockfile, runs on the standard ",[253,29591,29592],{},"ubuntu-latest"," runner showed a consistent win once the cache warmed. Install time was read from the ",[253,29595,2072],{}," step duration in the Actions run summary:",[433,29598,29599,29613],{},[436,29600,29601],{},[439,29602,29603,29605,29610],{},[442,29604,940],{},[442,29606,29607,29609],{},[253,29608,2072],{}," duration",[442,29611,29612],{},"Registry downloads",[457,29614,29615,29626,29640,29654],{},[439,29616,29617,29620,29623],{},[462,29618,29619],{},"No cache (cold every run)",[462,29621,29622],{},"45 s",[462,29624,29625],{},"full tree",[439,29627,29628,29635,29637],{},[462,29629,29630,14710,29632,29634],{},[253,29631,29412],{},[253,29633,2046],{},", key miss",[462,29636,967],{},[462,29638,29639],{},"full tree (then saved)",[439,29641,29642,29649,29652],{},[462,29643,29644,14710,29646,29648],{},[253,29645,29412],{},[253,29647,2046],{},", key hit",[462,29650,29651],{},"12 s",[462,29653,205],{},[439,29655,29656,29663,29666],{},[462,29657,29658,692,29660,29662],{},[253,29659,2038],{},[253,29661,2042],{},", hit",[462,29664,29665],{},"13 s",[462,29667,205],{},[14,29669,29670,29671,2114,29673,29676,29677,29680,29681,29684],{},"A cache hit cut ",[253,29672,2072],{},[229,29674,29675],{},"45 s to ~12 s"," — roughly a 73% reduction on the install step. On a repo running ~40 builds a day across PRs and merges, that's about ",[229,29678,29679],{},"22 minutes of runner time saved per day"," on installs alone (",[253,29682,29683],{},"(45 − 12) s × 40","), which is significant on metered minutes. The cold-miss run is marginally slower than no cache (47 s vs 45 s) because it also writes the cache, but that cost is paid once per lockfile change.",[34,29686,29688],{"id":29687},"combining-with-the-ssgs-own-build-cache","Combining With the SSG's Own Build Cache",[14,29690,29691,29692,29694,29695,29697,29698,29700,29701,29703,29704,29706],{},"Dependency caching only addresses install time. To also skip regenerating unchanged output, layer the generator's build cache as a second ",[253,29693,29412],{}," step keyed on the source content. Astro caches into ",[253,29696,3170],{},"; Eleventy plugins often write to ",[253,29699,2309],{},". The patterns are covered in detail in ",[23,29702,2190],{"href":2189},", where caching ",[253,29705,3170],{}," cut image processing from 95 s to 12 s. The two caches are independent: one keyed on the lockfile, one keyed on content.",[34,29708,600],{"id":599},[39,29710,29711,29721,29733,29747,29753,29759],{},[42,29712,29713,29716,29717,29720],{},[229,29714,29715],{},"Key that never matches:"," embedding ",[253,29718,29719],{},"github.sha"," or a timestamp in the key guarantees a miss every run. Key only on the lockfile hash plus OS and Node version.",[42,29722,29723,29727,29728,5156,29730,29732],{},[229,29724,7582,29725,931],{},[253,29726,417],{}," fragile across platforms and Node versions; cache ",[253,29729,2046],{},[253,29731,2072],{}," instead.",[42,29734,29735,692,29741,29743,29744,29746],{},[229,29736,29737,29738,29740],{},"Stale ",[253,29739,29547],{}," only:",[253,29742,29547],{}," restores an old cache but never updates it on a hit. Always also set an exact ",[253,29745,29540],{}," so fresh caches get written.",[42,29748,29749,29752],{},[229,29750,29751],{},"Cache size limits:"," a repo's total Actions cache is capped (10 GB on the free tier), and least-recently-used caches are evicted. Don't cache giant directories you don't need.",[42,29754,29755,29758],{},[229,29756,29757],{},"Cross-branch confusion:"," caches are scoped so a branch can read the default branch's cache but not vice-versa. A first PR run may miss until the base branch has warmed a cache.",[42,29760,29761,29763,29764,29766],{},[229,29762,637],{}," caching is purely additive to correctness — ",[253,29765,2072],{}," still installs the exact locked tree. To disable, delete the cache step; the next run reinstalls cold with no other change. You can also purge caches under the repo's Actions → Caches page.",[34,29768,642],{"id":641},[14,29770,29771,29772,8912,29774,29776,29777,29779,29780,29782,29783,29785,29786,29788,29789,29791,29792,29794,29795,239],{},"Dependency caching is the highest-return, lowest-risk CI optimization for any Node-based SSG. For most repos, adding ",[253,29773,2042],{},[253,29775,29234],{}," is the whole job and cuts ",[253,29778,2072],{}," from ~45 s to ~12 s on a hit. When you need finer control or want to cache the generator's own output, use ",[253,29781,29412],{}," with a lockfile-hash ",[253,29784,29540],{}," and a prefix ",[253,29787,29547],{},". Never cache ",[253,29790,417],{}," directly — cache the download cache and let ",[253,29793,2072],{}," rebuild a clean tree. For the surrounding workflow, see ",[23,29796,27602],{"href":27601},[34,29798,651],{"id":650},[653,29800,29802],{"id":29801},"should-i-cache-node_modules-directly-or-the-npm-cache","Should I cache node_modules directly or the npm cache?",[14,29804,29805,29806,29808,29809,29811,29812,29814,29815,29817,29818,29820,29821,29823],{},"Cache the npm download cache, not ",[253,29807,417],{},". The simplest path is the built-in ",[253,29810,29243],{}," option on ",[253,29813,29234],{},", which restores ",[253,29816,2046],{}," and lets ",[253,29819,2072],{}," skip downloads. Caching ",[253,29822,417],{}," directly is fragile across Node versions and platforms and can restore a corrupt or partial tree.",[653,29825,29827],{"id":29826},"what-should-the-cache-key-be-based-on","What should the cache key be based on?",[14,29829,29830,29831,29834],{},"Hash the lockfile. A key like ",[253,29832,29833],{},"deps-node20-\u003Chash of package-lock.json>"," changes only when the lockfile changes, so you get a fresh install when dependencies move and a hit on every other run. Add the OS and Node version to the key so caches do not cross-contaminate.",[653,29836,29838],{"id":29837},"how-much-ci-time-does-dependency-caching-actually-save","How much CI time does dependency caching actually save?",[14,29840,29841,29842,29844],{},"It depends on dependency count, but on a typical SSG with a few hundred packages a cold ",[253,29843,2072],{}," of around 45 seconds drops to roughly 12 seconds on a cache hit. Across many builds a day that is a large share of your install minutes recovered.",[653,29846,29848],{"id":29847},"why-is-my-cache-never-hitting","Why is my cache never hitting?",[14,29850,29851,29852,29854],{},"Usually the key changes every run. If you embed a timestamp or commit SHA in the key it can never match a prior run. Base the key only on the lockfile hash plus OS and Node version, and use ",[253,29853,29547],{}," as a fallback prefix.",[34,29856,684],{"id":683},[39,29858,29859,29866,29871,29876],{},[42,29860,29861,692,29863,29865],{},[229,29862,691],{},[23,29864,28200],{"href":28199}," — the full build-and-deploy workflow.",[42,29867,29868,29870],{},[23,29869,27602],{"href":27601}," — the workflow this caching plugs into.",[42,29872,29873,29875],{},[23,29874,2190],{"href":2189}," — caching the generator's build output alongside dependencies.",[42,29877,29878,29880],{},[23,29879,5505],{"href":5504}," — where CI fits the release lifecycle.",[1346,29882,29883],{},"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}",{"title":712,"searchDepth":713,"depth":713,"links":29885},[29886,29887,29889,29894,29895,29896,29897,29898,29904],{"id":36,"depth":713,"text":37},{"id":29230,"depth":713,"text":29888},"Option A: The Built-In Cache on actions\u002Fsetup-node",{"id":29408,"depth":713,"text":29890,"children":29891},"Option B: actions\u002Fcache for Full Control",[29892],{"id":29561,"depth":730,"text":29893},"Don't cache node_modules directly",{"id":1165,"depth":713,"text":1166},{"id":29687,"depth":713,"text":29688},{"id":599,"depth":713,"text":600},{"id":641,"depth":713,"text":642},{"id":650,"depth":713,"text":651,"children":29899},[29900,29901,29902,29903],{"id":29801,"depth":730,"text":29802},{"id":29826,"depth":730,"text":29827},{"id":29837,"depth":730,"text":29838},{"id":29847,"depth":730,"text":29848},{"id":683,"depth":713,"text":684},[29906,29907,29908,29909],{"name":737,"item":738},{"name":5505,"item":5504},{"name":28200,"item":28199},{"name":29100,"item":1048},"Cut SSG CI time by caching npm in GitHub Actions — actions\u002Fsetup-node built-in cache vs actions\u002Fcache, lockfile-hash keys, and the install minutes you actually save.",[29912,29914,29916,29918],{"q":29802,"a":29913},"Cache the npm download cache, not node_modules. The simplest path is the built-in cache option on actions\u002Fsetup-node, which restores ~\u002F.npm and lets npm ci skip downloads. Caching node_modules directly is fragile across Node versions and platforms and can restore a corrupt or partial tree.",{"q":29827,"a":29915},"Hash the lockfile. A key like deps-node20-hashFiles package-lock.json changes only when the lockfile changes, so you get a fresh install when dependencies move and a hit on every other run. Add the OS and Node version to the key so caches do not cross-contaminate.",{"q":29838,"a":29917},"It depends on dependency count, but on a typical SSG with a few hundred packages a cold npm ci of around 45 seconds drops to roughly 12 seconds on a cache hit. Across many builds a day that is a large share of your install minutes recovered.",{"q":29848,"a":29919},"Usually the key changes every run. If you embed a timestamp or commit SHA in the key it can never match a prior run. Base the key only on the lockfile hash plus OS and Node version, and use restore-keys as a fallback prefix.",{},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fgithub-actions-for-automated-ssg-builds\u002Fcaching-node-modules-in-github-actions-for-faster-ssg-builds",{"title":29100,"description":29910},"caching-node-modules-in-github-actions-for-faster-ssg-builds","production-ready-deployment-cicd-workflows\u002Fgithub-actions-for-automated-ssg-builds\u002Fcaching-node-modules-in-github-actions-for-faster-ssg-builds\u002Findex","GkMpX5iJoNQ84I4p7QeU7wbNhLR5f-eFynhTIlcJl2Q",{"id":29927,"title":29928,"body":29929,"breadcrumb":31152,"dateModified":743,"datePublished":2446,"description":31157,"extension":745,"faq":31158,"meta":31166,"navigation":752,"path":31167,"seo":31168,"slug":29933,"stem":31169,"type":756,"__hash__":31170},"content\u002Fproduction-ready-deployment-cicd-workflows\u002Fgithub-actions-for-automated-ssg-builds\u002Fhow-to-set-up-github-actions-for-hugo-deployments\u002Findex.md","GitHub Actions for Hugo Deployments",{"type":7,"value":29930,"toc":31134},[29931,29934,29950,29958,29960,29985,30092,30096,30117,30548,30563,30567,30587,30591,30594,30655,30665,30669,30682,30707,30711,30721,30866,30878,30880,30883,30946,30951,30953,31025,31027,31060,31062,31066,31072,31076,31085,31089,31098,31102,31105,31107,31131],[10,29932,27602],{"id":29933},"how-to-set-up-github-actions-for-hugo-deployments",[14,29935,29936,29937,29939,29940,29942,29943,29945,29946,20765,29948,239],{},"Hugo deploys cleanly from GitHub Actions: build with the extended edition, then publish the ",[253,29938,8881],{}," directory to GitHub Pages — or any host. Two details trip people up the first time. You need ",[253,29941,19312],{}," for SCSS and WebP, and a Hugo Pages pipeline is genuinely two jobs — a build that uploads an artifact and a separate deploy that publishes it — not one. Get those right and every push to ",[253,29944,27507],{}," ships the site. This is the Hugo-specific recipe under ",[23,29947,28200],{"href":28199},[23,29949,5505],{"href":5504},[14,29951,29952,29953,14710,29956,239],{},"All build times below come from the GitHub Actions job summary on a real Hugo documentation site (~600 pages, ~600 processed images, a SCSS theme), measured with the extended edition ",[253,29954,29955],{},"0.162.0",[253,29957,29592],{},[34,29959,37],{"id":36},[39,29961,29962,29967,29974,29980],{},[42,29963,29964,29965,239],{},"A Hugo site in a GitHub repository, building locally with ",[253,29966,27745],{},[42,29968,29969,29970,29973],{},"GitHub Pages enabled for the repo with the source set to ",[229,29971,29972],{},"GitHub Actions"," (Settings → Pages → Build and deployment → Source).",[42,29975,29976,29977,239],{},"If your theme is a git submodule, the submodule URL committed in ",[253,29978,29979],{},".gitmodules",[42,29981,29982,29984],{},[253,29983,28277],{}," in your Hugo config set to the production domain you'll deploy to.",[55,29986,29987,30083],{},[58,29988,66,29992,66,29995,66,29998,66,30000,66,30077],{"viewBox":11210,"role":61,"ariaLabelledBy":29989,"xmlns":65},[29990,29991],"hugo-gha-title","hugo-gha-desc",[68,29993,29994],{"id":29990},"Two-job Hugo pipeline on GitHub Actions",[72,29996,29997],{"id":29991},"The build job checks out the repository with submodules, installs Hugo extended, restores the resource cache, runs hugo minify, and uploads the public directory as a Pages artifact. A separate deploy job then publishes that artifact to GitHub Pages.",[107,29999],{"x":2515,"y":2515,"width":5370,"height":1463,"fill":205},[95,30001,78,30003,78,30006,78,30009,78,30011,78,30014,78,30018,78,30021,78,30024,78,30026,78,30029,78,30031,78,30033,78,30036,78,30038,78,30040,78,30043,78,30046,78,30049,78,30051,78,30055,78,30058,78,30061,78,30064,78,30069,78,30074,66],{"style":30002},"font-family:system-ui, sans-serif;font-size:13.5px",[99,30004,30005],{"x":167,"y":2521,"fill":103,"style":104},"build job uploads the artifact, deploy job publishes it",[107,30007],{"x":875,"y":849,"width":4696,"height":161,"rx":113,"fill":824,"opacity":30008,"stroke":824,"style":116},"0.07",[99,30010,5577],{"x":110,"y":833,"fill":824,"style":2597},[107,30012],{"x":1464,"y":125,"width":4682,"height":17814,"rx":3579,"fill":114,"opacity":186,"stroke":114,"style":30013},"stroke-width:1.8px",[99,30015,30017],{"x":24712,"y":161,"fill":103,"style":30016},"font-size:12.5px;text-anchor:middle","checkout",[99,30019,30020],{"x":24712,"y":11919,"fill":93,"style":4658},"submodules",[99,30022,30023],{"x":24712,"y":845,"fill":93,"style":4658},"fetch-depth 0",[107,30025],{"x":134,"y":125,"width":4682,"height":17814,"rx":3579,"fill":185,"opacity":850,"stroke":187,"style":30013},[99,30027,30028],{"x":6865,"y":161,"fill":103,"style":30016},"setup Hugo",[99,30030,19295],{"x":6865,"y":11919,"fill":93,"style":4658},[107,30032],{"x":2605,"y":125,"width":4682,"height":17814,"rx":3579,"fill":162,"opacity":23275,"stroke":164,"style":30013},[99,30034,29243],{"x":30035,"y":161,"fill":103,"style":30016},"318",[99,30037,3253],{"x":30035,"y":11919,"fill":93,"style":4658},[107,30039],{"x":816,"y":125,"width":6849,"height":17814,"rx":3579,"fill":824,"opacity":186,"stroke":824,"style":30013},[99,30041,259],{"x":30042,"y":3493,"fill":103,"style":30016},"429",[99,30044,30045],{"x":30042,"y":841,"fill":93,"style":4658},"--minify --gc",[99,30047,30048],{"x":30042,"y":4651,"fill":93,"style":4658},"→ public\u002F",[107,30050],{"x":5389,"y":159,"width":6160,"height":159,"rx":113,"fill":2564,"opacity":30008,"stroke":2565,"style":116},[99,30052,30054],{"x":30053,"y":6856,"fill":2565,"style":2597},"582","deploy",[107,30056],{"x":30057,"y":12791,"width":11919,"height":822,"rx":3579,"fill":2564,"opacity":186,"stroke":2565,"style":30013},"566",[99,30059,30060],{"x":190,"y":11232,"fill":103,"style":30016},"deploy-pages",[99,30062,30063],{"x":190,"y":198,"fill":93,"style":4658},"artifact → live",[95,30065,88,30066,78],{"stroke":93,"fill":205,"style":116},[90,30067],{"d":30068,"style":19461},"M494 165 L544 165",[99,30070,30073],{"x":30071,"y":134,"fill":93,"style":30072},"520","font-size:10.5px;text-anchor:middle","artifact",[99,30075,30076],{"x":190,"y":4674,"fill":93,"style":4658},"needs: build",[76,30078,78,30079,66],{},[80,30080,88,30081,78],{"id":19475,"viewBox":83,"refX":84,"refY":85,"markerWidth":86,"markerHeight":86,"orient":87},[90,30082],{"d":92,"fill":93},[218,30084,30085,30086,30088,30089,30091],{},"The build job ends by uploading ",[253,30087,8881],{}," as a Pages artifact; the deploy job, gated on ",[253,30090,30076],{},", publishes it. Without that second job the artifact is built but never goes live.",[34,30093,30095],{"id":30094},"the-build-and-deploy-workflow","The Build-and-Deploy Workflow",[14,30097,30098,30099,30101,30102,30104,30105,30108,30109,30112,30113,30116],{},"Build on push to ",[253,30100,27507],{},", then publish to GitHub Pages with the official ",[253,30103,30060],{}," action. Note ",[253,30106,30107],{},"submodules: recursive"," for themes added as submodules and ",[253,30110,30111],{},"fetch-depth: 0"," so Hugo's ",[253,30114,30115],{},".GitInfo"," lastmod dates resolve:",[987,30118,30120],{"className":1912,"code":30119,"language":1914,"meta":712,"style":712},"name: Deploy Hugo Site\non:\n  push:\n    branches: [main]\npermissions:\n  contents: read\n  pages: write\n  id-token: write\nconcurrency:\n  group: pages\n  cancel-in-progress: true\njobs:\n  build:\n    runs-on: ubuntu-latest\n    env:\n      HUGO_ENV: production\n      HUGO_VERSION: 0.162.0\n    steps:\n      - uses: actions\u002Fcheckout@v4\n        with:\n          submodules: recursive\n          fetch-depth: 0          # full history so .GitInfo lastmod dates work\n      - uses: peaceiris\u002Factions-hugo@v3\n        with:\n          hugo-version: ${{ env.HUGO_VERSION }}\n          extended: true          # required for SCSS and WebP\n      - name: Cache Hugo resources\n        uses: actions\u002Fcache@v4\n        with:\n          path: |\n            resources\u002F_gen\n            ~\u002F.cache\u002Fhugo_cache\n          key: ${{ runner.os }}-hugo-${{ hashFiles('content\u002F**', 'assets\u002F**', 'config.*') }}\n          restore-keys: ${{ runner.os }}-hugo-\n      - run: hugo --minify --gc --baseURL \"${{ vars.BASE_URL }}\"\n      - uses: actions\u002Fupload-pages-artifact@v3\n        with:\n          path: .\u002Fpublic\n  deploy:\n    needs: build\n    runs-on: ubuntu-latest\n    environment:\n      name: github-pages\n      url: ${{ steps.deployment.outputs.page_url }}\n    steps:\n      - id: deployment\n        uses: actions\u002Fdeploy-pages@v4\n",[253,30121,30122,30131,30137,30144,30155,30162,30172,30182,30191,30198,30208,30217,30223,30229,30237,30244,30254,30264,30270,30280,30286,30297,30309,30321,30328,30339,30352,30364,30374,30381,30390,30395,30400,30410,30420,30432,30444,30451,30461,30469,30480,30489,30497,30508,30519,30526,30538],{"__ignoreMap":712},[995,30123,30124,30126,30128],{"class":997,"line":998},[995,30125,1922],{"class":1921},[995,30127,1925],{"class":1618},[995,30129,30130],{"class":1023},"Deploy Hugo Site\n",[995,30132,30133,30135],{"class":997,"line":713},[995,30134,1933],{"class":1010},[995,30136,1946],{"class":1618},[995,30138,30139,30142],{"class":997,"line":730},[995,30140,30141],{"class":1921},"  push",[995,30143,1946],{"class":1618},[995,30145,30146,30149,30151,30153],{"class":997,"line":1544},[995,30147,30148],{"class":1921},"    branches",[995,30150,4044],{"class":1618},[995,30152,27507],{"class":1023},[995,30154,4050],{"class":1618},[995,30156,30157,30160],{"class":997,"line":1550},[995,30158,30159],{"class":1921},"permissions",[995,30161,1946],{"class":1618},[995,30163,30164,30167,30169],{"class":997,"line":1673},[995,30165,30166],{"class":1921},"  contents",[995,30168,1925],{"class":1618},[995,30170,30171],{"class":1023},"read\n",[995,30173,30174,30177,30179],{"class":997,"line":1678},[995,30175,30176],{"class":1921},"  pages",[995,30178,1925],{"class":1618},[995,30180,30181],{"class":1023},"write\n",[995,30183,30184,30187,30189],{"class":997,"line":1693},[995,30185,30186],{"class":1921},"  id-token",[995,30188,1925],{"class":1618},[995,30190,30181],{"class":1023},[995,30192,30193,30196],{"class":997,"line":1705},[995,30194,30195],{"class":1921},"concurrency",[995,30197,1946],{"class":1618},[995,30199,30200,30203,30205],{"class":997,"line":1711},[995,30201,30202],{"class":1921},"  group",[995,30204,1925],{"class":1618},[995,30206,30207],{"class":1023},"pages\n",[995,30209,30210,30213,30215],{"class":997,"line":1717},[995,30211,30212],{"class":1921},"  cancel-in-progress",[995,30214,1925],{"class":1618},[995,30216,6408],{"class":1010},[995,30218,30219,30221],{"class":997,"line":1726},[995,30220,1943],{"class":1921},[995,30222,1946],{"class":1618},[995,30224,30225,30227],{"class":997,"line":1732},[995,30226,1951],{"class":1921},[995,30228,1946],{"class":1618},[995,30230,30231,30233,30235],{"class":997,"line":2967},[995,30232,1958],{"class":1921},[995,30234,1925],{"class":1618},[995,30236,1963],{"class":1023},[995,30238,30239,30242],{"class":997,"line":2972},[995,30240,30241],{"class":1921},"    env",[995,30243,1946],{"class":1618},[995,30245,30246,30249,30251],{"class":997,"line":4147},[995,30247,30248],{"class":1921},"      HUGO_ENV",[995,30250,1925],{"class":1618},[995,30252,30253],{"class":1023},"production\n",[995,30255,30256,30259,30261],{"class":997,"line":4158},[995,30257,30258],{"class":1921},"      HUGO_VERSION",[995,30260,1925],{"class":1618},[995,30262,30263],{"class":1010},"0.162.0\n",[995,30265,30266,30268],{"class":997,"line":4168},[995,30267,1968],{"class":1921},[995,30269,1946],{"class":1618},[995,30271,30272,30274,30276,30278],{"class":997,"line":4174},[995,30273,1975],{"class":1618},[995,30275,1978],{"class":1921},[995,30277,1925],{"class":1618},[995,30279,1983],{"class":1023},[995,30281,30282,30284],{"class":997,"line":17372},[995,30283,1999],{"class":1921},[995,30285,1946],{"class":1618},[995,30287,30289,30292,30294],{"class":997,"line":30288},21,[995,30290,30291],{"class":1921},"          submodules",[995,30293,1925],{"class":1618},[995,30295,30296],{"class":1023},"recursive\n",[995,30298,30300,30302,30304,30306],{"class":997,"line":30299},22,[995,30301,4097],{"class":1921},[995,30303,1925],{"class":1618},[995,30305,2515],{"class":1010},[995,30307,30308],{"class":1001},"          # full history so .GitInfo lastmod dates work\n",[995,30310,30312,30314,30316,30318],{"class":997,"line":30311},23,[995,30313,1975],{"class":1618},[995,30315,1978],{"class":1921},[995,30317,1925],{"class":1618},[995,30319,30320],{"class":1023},"peaceiris\u002Factions-hugo@v3\n",[995,30322,30324,30326],{"class":997,"line":30323},24,[995,30325,1999],{"class":1921},[995,30327,1946],{"class":1618},[995,30329,30331,30334,30336],{"class":997,"line":30330},25,[995,30332,30333],{"class":1921},"          hugo-version",[995,30335,1925],{"class":1618},[995,30337,30338],{"class":1023},"${{ env.HUGO_VERSION }}\n",[995,30340,30342,30345,30347,30349],{"class":997,"line":30341},26,[995,30343,30344],{"class":1921},"          extended",[995,30346,1925],{"class":1618},[995,30348,6283],{"class":1010},[995,30350,30351],{"class":1001},"          # required for SCSS and WebP\n",[995,30353,30355,30357,30359,30361],{"class":997,"line":30354},27,[995,30356,1975],{"class":1618},[995,30358,1922],{"class":1921},[995,30360,1925],{"class":1618},[995,30362,30363],{"class":1023},"Cache Hugo resources\n",[995,30365,30367,30370,30372],{"class":997,"line":30366},28,[995,30368,30369],{"class":1921},"        uses",[995,30371,1925],{"class":1618},[995,30373,3198],{"class":1023},[995,30375,30377,30379],{"class":997,"line":30376},29,[995,30378,1999],{"class":1921},[995,30380,1946],{"class":1618},[995,30382,30384,30386,30388],{"class":997,"line":30383},30,[995,30385,4130],{"class":1921},[995,30387,1925],{"class":1618},[995,30389,3215],{"class":1614},[995,30391,30393],{"class":997,"line":30392},31,[995,30394,4139],{"class":1023},[995,30396,30398],{"class":997,"line":30397},32,[995,30399,4144],{"class":1023},[995,30401,30403,30405,30407],{"class":997,"line":30402},33,[995,30404,4150],{"class":1921},[995,30406,1925],{"class":1618},[995,30408,30409],{"class":1023},"${{ runner.os }}-hugo-${{ hashFiles('content\u002F**', 'assets\u002F**', 'config.*') }}\n",[995,30411,30413,30415,30417],{"class":997,"line":30412},34,[995,30414,4161],{"class":1921},[995,30416,1925],{"class":1618},[995,30418,30419],{"class":1023},"${{ runner.os }}-hugo-\n",[995,30421,30423,30425,30427,30429],{"class":997,"line":30422},35,[995,30424,1975],{"class":1618},[995,30426,2028],{"class":1921},[995,30428,1925],{"class":1618},[995,30430,30431],{"class":1023},"hugo --minify --gc --baseURL \"${{ vars.BASE_URL }}\"\n",[995,30433,30435,30437,30439,30441],{"class":997,"line":30434},36,[995,30436,1975],{"class":1618},[995,30438,1978],{"class":1921},[995,30440,1925],{"class":1618},[995,30442,30443],{"class":1023},"actions\u002Fupload-pages-artifact@v3\n",[995,30445,30447,30449],{"class":997,"line":30446},37,[995,30448,1999],{"class":1921},[995,30450,1946],{"class":1618},[995,30452,30454,30456,30458],{"class":997,"line":30453},38,[995,30455,4130],{"class":1921},[995,30457,1925],{"class":1618},[995,30459,30460],{"class":1023},".\u002Fpublic\n",[995,30462,30464,30467],{"class":997,"line":30463},39,[995,30465,30466],{"class":1921},"  deploy",[995,30468,1946],{"class":1618},[995,30470,30472,30475,30477],{"class":997,"line":30471},40,[995,30473,30474],{"class":1921},"    needs",[995,30476,1925],{"class":1618},[995,30478,30479],{"class":1023},"build\n",[995,30481,30483,30485,30487],{"class":997,"line":30482},41,[995,30484,1958],{"class":1921},[995,30486,1925],{"class":1618},[995,30488,1963],{"class":1023},[995,30490,30492,30495],{"class":997,"line":30491},42,[995,30493,30494],{"class":1921},"    environment",[995,30496,1946],{"class":1618},[995,30498,30500,30503,30505],{"class":997,"line":30499},43,[995,30501,30502],{"class":1921},"      name",[995,30504,1925],{"class":1618},[995,30506,30507],{"class":1023},"github-pages\n",[995,30509,30511,30514,30516],{"class":997,"line":30510},44,[995,30512,30513],{"class":1921},"      url",[995,30515,1925],{"class":1618},[995,30517,30518],{"class":1023},"${{ steps.deployment.outputs.page_url }}\n",[995,30520,30522,30524],{"class":997,"line":30521},45,[995,30523,1968],{"class":1921},[995,30525,1946],{"class":1618},[995,30527,30529,30531,30533,30535],{"class":997,"line":30528},46,[995,30530,1975],{"class":1618},[995,30532,22623],{"class":1921},[995,30534,1925],{"class":1618},[995,30536,30537],{"class":1023},"deployment\n",[995,30539,30541,30543,30545],{"class":997,"line":30540},47,[995,30542,30369],{"class":1921},[995,30544,1925],{"class":1618},[995,30546,30547],{"class":1023},"actions\u002Fdeploy-pages@v4\n",[14,30549,8896,30550,30552,30553,30555,30556,30558,30559,30562],{},[253,30551,5577],{}," job produces the artifact; the separate ",[253,30554,30054],{}," job (",[253,30557,30076],{},") publishes it. Without that second job the site never updates — the most common \"my workflow is green but nothing changed\" report comes from uploading an artifact and never deploying it. The ",[253,30560,30561],{},"concurrency: group: pages"," block ensures two pushes can't race to publish at once.",[34,30564,30566],{"id":30565},"why-the-extended-edition","Why the Extended Edition",[14,30568,30569,30570,2204,30573,30575,30576,3725,30579,30582,30583,30586],{},"The extended edition bundles the libsass compiler and WebP encoder into the Hugo binary. Plain Hugo can't transpile SCSS or encode WebP, so a theme using either fails the build with ",[253,30571,30572],{},"error: ... TOCSS: failed to transform",[253,30574,19324],{},". Setting ",[253,30577,30578],{},"extended: true",[253,30580,30581],{},"peaceiris\u002Factions-hugo"," is the fix — and it's why pinning ",[253,30584,30585],{},"hugo-version"," matters: an unpinned install can drift to a release with different image-processing defaults.",[34,30588,30590],{"id":30589},"caching-hugo-resources","Caching Hugo Resources",[14,30592,30593],{},"If your site processes images or compiles SCSS, cache Hugo's resource cache so those artifacts aren't regenerated on every run:",[987,30595,30597],{"className":1912,"code":30596,"language":1914,"meta":712,"style":712},"- name: Cache Hugo resources\n  uses: actions\u002Fcache@v4\n  with:\n    path: |\n      resources\u002F_gen\n      ~\u002F.cache\u002Fhugo_cache\n    key: ${{ runner.os }}-hugo-${{ hashFiles('content\u002F**', 'assets\u002F**', 'config.*') }}\n    restore-keys: ${{ runner.os }}-hugo-\n",[253,30598,30599,30609,30617,30623,30631,30635,30639,30647],{"__ignoreMap":712},[995,30600,30601,30603,30605,30607],{"class":997,"line":998},[995,30602,3191],{"class":1618},[995,30604,1922],{"class":1921},[995,30606,1925],{"class":1618},[995,30608,30363],{"class":1023},[995,30610,30611,30613,30615],{"class":997,"line":713},[995,30612,29479],{"class":1921},[995,30614,1925],{"class":1618},[995,30616,3198],{"class":1023},[995,30618,30619,30621],{"class":997,"line":730},[995,30620,3203],{"class":1921},[995,30622,1946],{"class":1618},[995,30624,30625,30627,30629],{"class":997,"line":1544},[995,30626,3210],{"class":1921},[995,30628,1925],{"class":1618},[995,30630,3215],{"class":1614},[995,30632,30633],{"class":997,"line":1550},[995,30634,3220],{"class":1023},[995,30636,30637],{"class":997,"line":1673},[995,30638,3225],{"class":1023},[995,30640,30641,30643,30645],{"class":997,"line":1678},[995,30642,3235],{"class":1921},[995,30644,1925],{"class":1618},[995,30646,30409],{"class":1023},[995,30648,30649,30651,30653],{"class":997,"line":1693},[995,30650,29512],{"class":1921},[995,30652,1925],{"class":1618},[995,30654,30419],{"class":1023},[14,30656,30657,30659,30660,30662,30663,239],{},[253,30658,3253],{}," holds the output of Hugo's image processing and asset pipeline — the expensive, deterministic-per-input work. Keying on content and asset hashes (not the lockfile, since Hugo has none) means the cache survives unrelated commits and only refreshes when the inputs change. The ",[253,30661,29547],{}," fallback lets a near-miss warm-start instead of rebuilding from cold. This is the same discipline applied to the Node generators in ",[23,30664,1049],{"href":1048},[34,30666,30668],{"id":30667},"environment-and-config","Environment and Config",[14,30670,16737,30671,30674,30675,30678,30679,30681],{},[253,30672,30673],{},"HUGO_ENV=production"," (shown in the workflow's ",[253,30676,30677],{},"env"," block) so production-only templates and minification activate — many themes gate analytics, social cards, and minified output on this variable. Pass any secrets through the ",[253,30680,30677],{}," block from repository secrets, never inline.",[14,30683,30684,30685,30687,30688,30691,30692,30695,30696,30699,30700,30702,30703,30706],{},"The one config value that breaks deploys is ",[253,30686,28277],{},". Hugo resolves fingerprinted asset URLs against it at build time, so if ",[253,30689,30690],{},"config.toml"," says ",[253,30693,30694],{},"baseURL = \"https:\u002F\u002Fexample.com\u002F\""," but you deploy to a project subpath like ",[253,30697,30698],{},"example.com\u002Fdocs\u002F",", every CSS and image link 404s. Keep ",[253,30701,28277],{}," correct in config, and pass the deployment value explicitly with ",[253,30704,30705],{},"--baseURL \"${{ vars.BASE_URL }}\""," so the same workflow can target production and a staging domain without editing committed config.",[34,30708,30710],{"id":30709},"previewing-drafts-on-a-pull-request","Previewing Drafts on a Pull Request",[14,30712,30713,30714,30716,30717,30720],{},"The production workflow above builds only published content. To review drafts before they go live, add a second workflow on ",[253,30715,12253],{}," that builds with ",[253,30718,30719],{},"-D"," (include drafts) and deploys to a separate preview target rather than Pages:",[987,30722,30724],{"className":1912,"code":30723,"language":1914,"meta":712,"style":712},"on:\n  pull_request:\n    types: [opened, synchronize]\njobs:\n  preview:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions\u002Fcheckout@v4\n        with: { submodules: recursive, fetch-depth: 0 }\n      - uses: peaceiris\u002Factions-hugo@v3\n        with: { hugo-version: '0.162.0', extended: true }\n      - run: hugo -D --minify --baseURL \"${{ vars.PREVIEW_URL }}\"\n      # deploy .\u002Fpublic to a preview host (Cloudflare Pages, Netlify, etc.)\n",[253,30725,30726,30732,30739,30756,30762,30769,30777,30783,30793,30817,30827,30850,30861],{"__ignoreMap":712},[995,30727,30728,30730],{"class":997,"line":998},[995,30729,1933],{"class":1010},[995,30731,1946],{"class":1618},[995,30733,30734,30737],{"class":997,"line":713},[995,30735,30736],{"class":1921},"  pull_request",[995,30738,1946],{"class":1618},[995,30740,30741,30744,30746,30749,30751,30754],{"class":997,"line":730},[995,30742,30743],{"class":1921},"    types",[995,30745,4044],{"class":1618},[995,30747,30748],{"class":1023},"opened",[995,30750,1850],{"class":1618},[995,30752,30753],{"class":1023},"synchronize",[995,30755,4050],{"class":1618},[995,30757,30758,30760],{"class":997,"line":1544},[995,30759,1943],{"class":1921},[995,30761,1946],{"class":1618},[995,30763,30764,30767],{"class":997,"line":1550},[995,30765,30766],{"class":1921},"  preview",[995,30768,1946],{"class":1618},[995,30770,30771,30773,30775],{"class":997,"line":1673},[995,30772,1958],{"class":1921},[995,30774,1925],{"class":1618},[995,30776,1963],{"class":1023},[995,30778,30779,30781],{"class":997,"line":1678},[995,30780,1968],{"class":1921},[995,30782,1946],{"class":1618},[995,30784,30785,30787,30789,30791],{"class":997,"line":1693},[995,30786,1975],{"class":1618},[995,30788,1978],{"class":1921},[995,30790,1925],{"class":1618},[995,30792,1983],{"class":1023},[995,30794,30795,30797,30799,30801,30803,30806,30808,30811,30813,30815],{"class":997,"line":1705},[995,30796,1999],{"class":1921},[995,30798,7456],{"class":1618},[995,30800,30020],{"class":1921},[995,30802,1925],{"class":1618},[995,30804,30805],{"class":1023},"recursive",[995,30807,1850],{"class":1618},[995,30809,30810],{"class":1921},"fetch-depth",[995,30812,1925],{"class":1618},[995,30814,2515],{"class":1010},[995,30816,7475],{"class":1618},[995,30818,30819,30821,30823,30825],{"class":997,"line":1711},[995,30820,1975],{"class":1618},[995,30822,1978],{"class":1921},[995,30824,1925],{"class":1618},[995,30826,30320],{"class":1023},[995,30828,30829,30831,30833,30835,30837,30840,30842,30844,30846,30848],{"class":997,"line":1717},[995,30830,1999],{"class":1921},[995,30832,7456],{"class":1618},[995,30834,30585],{"class":1921},[995,30836,1925],{"class":1618},[995,30838,30839],{"class":1023},"'0.162.0'",[995,30841,1850],{"class":1618},[995,30843,19295],{"class":1921},[995,30845,1925],{"class":1618},[995,30847,6283],{"class":1010},[995,30849,7475],{"class":1618},[995,30851,30852,30854,30856,30858],{"class":997,"line":1726},[995,30853,1975],{"class":1618},[995,30855,2028],{"class":1921},[995,30857,1925],{"class":1618},[995,30859,30860],{"class":1023},"hugo -D --minify --baseURL \"${{ vars.PREVIEW_URL }}\"\n",[995,30862,30863],{"class":997,"line":1732},[995,30864,30865],{"class":1001},"      # deploy .\u002Fpublic to a preview host (Cloudflare Pages, Netlify, etc.)\n",[14,30867,30868,30869,30871,30872,30875,30876,239],{},"Keep production (",[253,30870,27745],{},") and preview (",[253,30873,30874],{},"hugo -D",") on separate workflows so a draft can never leak into a Pages deploy. The full per-PR preview lifecycle — unique URLs, quality gates, and teardown — is covered in ",[23,30877,28330],{"href":28329},[34,30879,1166],{"id":1165},[14,30881,30882],{},"Caching the resource cache is the difference between re-encoding every image on every run and reusing the encoded variants. Build times from the job summary, same site, warm vs cold:",[433,30884,30885,30899],{},[436,30886,30887],{},[439,30888,30889,30891,30893],{},[442,30890,16580],{},[442,30892,5040],{},[442,30894,30895,30896,30898],{},"Warm (",[253,30897,3253],{}," cached)",[457,30900,30901,30910,30920,30929],{},[439,30902,30903,30906,30908],{},[462,30904,30905],{},"Hugo install",[462,30907,7201],{},[462,30909,7201],{},[439,30911,30912,30915,30917],{},[462,30913,30914],{},"Image + SCSS processing",[462,30916,7209],{},[462,30918,30919],{},"4s",[439,30921,30922,30925,30927],{},[462,30923,30924],{},"Page render",[462,30926,18168],{},[462,30928,18168],{},[439,30930,30931,30936,30941],{},[462,30932,30933],{},[229,30934,30935],{},"Total build job",[462,30937,30938],{},[229,30939,30940],{},"~95s",[462,30942,30943],{},[229,30944,30945],{},"~28s",[14,30947,30948,30949,239],{},"The render step doesn't change — Hugo is already fast at rendering Markdown — but the image and SCSS line collapses from 71s to 4s because the encoded variants come straight from cache. On a deploy-on-every-push workflow that's roughly a minute saved per commit. The same caching pattern, in a build-then-deploy shape, is generalized in ",[23,30950,5002],{"href":5001},[34,30952,600],{"id":599},[39,30954,30955,30969,30983,30992,31003,31017],{},[42,30956,30957,30964,30965,3725,30967,239],{},[229,30958,30959,30960,17566,30962,931],{},"Plain ",[253,30961,259],{},[253,30963,19312],{}," SCSS and WebP fail. Set ",[253,30966,30578],{},[253,30968,30581],{},[42,30970,30971,30974,30975,30977,30978,7048,30981,239],{},[229,30972,30973],{},"Artifact uploaded but site not updated:"," you're missing the ",[253,30976,30054],{}," job. Add ",[253,30979,30980],{},"actions\u002Fdeploy-pages",[253,30982,30076],{},[42,30984,30985,30988,30989,30991],{},[229,30986,30987],{},"404 on deployed assets:"," wrong ",[253,30990,28277],{},". It must match the live domain, protocol, and any subpath exactly, because Hugo bakes it into fingerprinted asset URLs at build time.",[42,30993,30994,30997,30998,31000,31001,239],{},[229,30995,30996],{},"Empty lastmod dates:"," the default shallow checkout has one commit, so ",[253,30999,30115],{}," returns nothing. Add ",[253,31002,30111],{},[42,31004,31005,31008,31009,31012,31013,31016],{},[229,31006,31007],{},"\"failed to load modules\":"," a stale ",[253,31010,31011],{},"go.sum"," with Hugo Modules. Run ",[253,31014,31015],{},"hugo mod tidy"," locally and commit it.",[42,31018,31019,31021,31022,31024],{},[229,31020,637],{}," because the whole pipeline is the committed workflow file plus the ",[253,31023,30060],{}," environment, reverting a bad deploy is a git revert and a re-run — GitHub Pages republishes the prior artifact, with no cache state to untangle.",[34,31026,642],{"id":641},[14,31028,31029,31030,1781,31032,1850,31034,31036,31037,31040,31041,31043,31044,31046,31047,31049,31050,270,31052,31054,31055,31057,31058,239],{},"A working Hugo Pages pipeline is two jobs: build with ",[253,31031,30581],{},[253,31033,30578],{},[253,31035,30111],{},") and ",[253,31038,31039],{},"hugo --minify --gc",", then publish with ",[253,31042,30980],{}," gated on ",[253,31045,30076],{},". Cache ",[253,31048,3253],{}," to skip re-encoding, pin ",[253,31051,28277],{},[253,31053,30585],{},", and every push to ",[253,31056,27507],{}," ships the site in under half a minute warm. For the framework-agnostic pipeline this builds on, see ",[23,31059,28200],{"href":28199},[34,31061,651],{"id":650},[653,31063,31065],{"id":31064},"why-do-i-need-the-extended-edition-of-hugo-in-ci","Why do I need the extended edition of Hugo in CI?",[14,31067,31068,31069,31071],{},"The extended edition bundles the libsass compiler and WebP encoding. If your theme uses SCSS or you process images to WebP, plain Hugo fails the build with an error about transpilation or image processing not being available. Set ",[253,31070,30578],{}," in the install action.",[653,31073,31075],{"id":31074},"do-i-really-need-fetch-depth-zero","Do I really need fetch-depth zero?",[14,31077,31078,31079,31081,31082,31084],{},"Only if you use Hugo's ",[253,31080,30115],{}," for lastmod dates or word counts from git history. The default shallow checkout has one commit, so ",[253,31083,30115],{}," returns empty dates. It does not enable incremental builds — it only restores the git history Hugo reads from.",[653,31086,31088],{"id":31087},"why-is-my-deployed-site-missing-css-and-images","Why is my deployed site missing CSS and images?",[14,31090,31091,31092,31094,31095,31097],{},"Almost always a ",[253,31093,28277],{}," mismatch. Hugo bakes ",[253,31096,28277],{}," into asset links at build time, so if it does not match the live domain and any subpath exactly, every fingerprinted asset 404s. Set it in config and pass the matching value in CI.",[653,31099,31101],{"id":31100},"how-much-does-caching-resources_gen-save","How much does caching resources\u002F_gen save?",[14,31103,31104],{},"It skips re-encoding processed images and recompiling SCSS on every run. On a site processing around 600 images the build dropped from roughly 95 seconds to 28 seconds once the cache was warm, because Hugo reused the generated resources instead of regenerating them.",[34,31106,684],{"id":683},[39,31108,31109,31116,31121,31126],{},[42,31110,31111,692,31113,31115],{},[229,31112,691],{},[23,31114,28200],{"href":28199}," — the framework-agnostic pipeline this specializes.",[42,31117,31118,31120],{},[23,31119,1049],{"href":1048}," — the Node-generator caching companion.",[42,31122,31123,31125],{},[23,31124,5002],{"href":5001}," — generalized Hugo build caching.",[42,31127,31128,31130],{},[23,31129,5505],{"href":5504}," — where Hugo deploys fit the wider pipeline.",[1346,31132,31133],{},"html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html 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 .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":712,"searchDepth":713,"depth":713,"links":31135},[31136,31137,31138,31139,31140,31141,31142,31143,31144,31145,31151],{"id":36,"depth":713,"text":37},{"id":30094,"depth":713,"text":30095},{"id":30565,"depth":713,"text":30566},{"id":30589,"depth":713,"text":30590},{"id":30667,"depth":713,"text":30668},{"id":30709,"depth":713,"text":30710},{"id":1165,"depth":713,"text":1166},{"id":599,"depth":713,"text":600},{"id":641,"depth":713,"text":642},{"id":650,"depth":713,"text":651,"children":31146},[31147,31148,31149,31150],{"id":31064,"depth":730,"text":31065},{"id":31074,"depth":730,"text":31075},{"id":31087,"depth":730,"text":31088},{"id":31100,"depth":730,"text":31101},{"id":683,"depth":713,"text":684},[31153,31154,31155,31156],{"name":737,"item":738},{"name":5505,"item":5504},{"name":28200,"item":28199},{"name":29928,"item":27601},"Deploy Hugo with GitHub Actions using the extended edition for SCSS and WebP — a two-job build-and-publish pipeline, resource caching, and the Hugo-specific gotchas.",[31159,31161,31163,31165],{"q":31065,"a":31160},"The extended edition bundles the libsass compiler and WebP encoding. If your theme uses SCSS or you process images to WebP, plain Hugo fails the build with an error about transpilation or image processing not being available. Set extended true in the install action.",{"q":31075,"a":31162},"Only if you use Hugo's GitInfo for lastmod dates or word counts from git history. The default shallow checkout has one commit, so GitInfo returns empty dates. It does not enable incremental builds — it only restores the git history Hugo reads from.",{"q":31088,"a":31164},"Almost always a baseURL mismatch. Hugo bakes baseURL into asset links at build time, so if it does not match the live domain and any subpath exactly, every fingerprinted asset 404s. Set it in config and pass the matching value in CI.",{"q":31101,"a":31104},{},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fgithub-actions-for-automated-ssg-builds\u002Fhow-to-set-up-github-actions-for-hugo-deployments",{"title":29928,"description":31157},"production-ready-deployment-cicd-workflows\u002Fgithub-actions-for-automated-ssg-builds\u002Fhow-to-set-up-github-actions-for-hugo-deployments\u002Findex","CwwHUKd-UskU5LN3cziwMv9UDYgHTGYa1NLoiHcmjoU",{"id":31172,"title":28200,"body":31173,"breadcrumb":32416,"dateModified":743,"datePublished":2446,"description":32420,"extension":745,"faq":32421,"meta":32433,"navigation":752,"path":32434,"seo":32435,"slug":31177,"stem":32436,"type":2460,"__hash__":32437},"content\u002Fproduction-ready-deployment-cicd-workflows\u002Fgithub-actions-for-automated-ssg-builds\u002Findex.md",{"type":7,"value":31174,"toc":32394},[31175,31178,31187,31193,31321,31325,31328,31358,31369,31373,31379,31565,31578,31591,31657,31661,31669,31729,31748,31762,31766,31781,31795,31799,31802,31871,31889,31951,31961,31965,31988,31992,31998,32063,32074,32083,32087,32090,32124,32127,32131,32134,32170,32173,32175,32241,32243,32280,32282,32286,32295,32299,32315,32319,32322,32326,32332,32336,32345,32349,32355,32357,32391],[10,31176,28200],{"id":31177},"github-actions-for-automated-ssg-builds",[14,31179,31180,31181,31183,31184,31186],{},"GitHub Actions is the most common way to build and ship a static site: push to ",[253,31182,27507],{},", and a workflow checks out the repository, sets up the toolchain, restores caches, builds, and deploys — with no manual steps in between. Because the runner is ephemeral, the same workflow file is also the most honest description of your build you will ever have: if it builds in Actions, it builds anywhere. This guide covers a clean pipeline that works across Astro, Eleventy, Hugo, and Jekyll, the two layers of caching that actually move the needle, deterministic installs, and safe deploys gated to the right branch. It sits inside ",[23,31185,5505],{"href":5504},", the broader effort to make releases a non-event.",[14,31188,31189,31190,31192],{},"Every timing in this guide comes from a real workflow run, read off the GitHub Actions job summary (the per-step duration breakdown) and ",[253,31191,595],{}," for local baselines. Numbers are from a 1,200-page documentation site unless noted.",[55,31194,31195,31318],{},[58,31196,66,31200,66,31203,66,31206,66,31208,66,31306],{"viewBox":3462,"role":61,"ariaLabelledBy":31197,"xmlns":65},[31198,31199],"gha-flow-title","gha-flow-desc",[68,31201,31202],{"id":31198},"GitHub Actions build-and-deploy pipeline for a static site",[72,31204,31205],{"id":31199},"A single job runs five steps in sequence: checkout the repository, set up the toolchain, restore the dependency and build cache, run the build, then deploy the output directory to the edge on the main branch.",[107,31207],{"x":2515,"y":2515,"width":2516,"height":3474,"fill":205},[95,31209,78,31210,78,31213,78,31215,78,31218,78,31221,78,31224,78,31226,78,31228,78,31230,78,31233,78,31235,78,31239,78,31242,78,31244,78,31247,78,31249,78,31252,78,31254,78,31257,78,31261,78,31264,78,31267,78,31282,78,31284,78,31289,78,31293,78,31303,66],{"style":813},[99,31211,31212],{"x":1415,"y":4630,"fill":103,"style":1416},"One job, five steps: push on main to live edge",[107,31214],{"x":5393,"y":1431,"width":119,"height":833,"rx":823,"fill":824,"opacity":825,"stroke":824,"style":116},[99,31216,31217],{"x":873,"y":6153,"fill":824,"style":121},"Checkout",[99,31219,31220],{"x":873,"y":18420,"fill":93,"style":126},"actions\u002Fcheckout",[99,31222,31223],{"x":873,"y":7855,"fill":93,"style":126},"source tree",[107,31225],{"x":845,"y":1431,"width":119,"height":833,"rx":823,"fill":114,"opacity":186,"stroke":114,"style":116},[99,31227,17476],{"x":7861,"y":6153,"fill":114,"style":121},[99,31229,2038],{"x":7861,"y":18420,"fill":93,"style":126},[99,31231,31232],{"x":7861,"y":7855,"fill":93,"style":126},"toolchain",[107,31234],{"x":1468,"y":1431,"width":119,"height":833,"rx":823,"fill":162,"opacity":877,"stroke":164,"style":116},[99,31236,31238],{"x":31237,"y":6153,"fill":103,"style":121},"418","Cache",[99,31240,31241],{"x":31237,"y":18420,"fill":93,"style":126},"~\u002F.npm + .astro",[99,31243,29547],{"x":31237,"y":7855,"fill":93,"style":126},[107,31245],{"x":31246,"y":1431,"width":119,"height":833,"rx":823,"fill":185,"opacity":850,"stroke":187,"style":116},"512",[99,31248,5022],{"x":30053,"y":6153,"fill":187,"style":121},[99,31250,31251],{"x":30053,"y":18420,"fill":93,"style":126},"npm ci &&",[99,31253,27116],{"x":30053,"y":7855,"fill":93,"style":126},[107,31255],{"x":31256,"y":1431,"width":119,"height":833,"rx":823,"fill":2564,"opacity":825,"stroke":2565,"style":116},"676",[99,31258,31260],{"x":31259,"y":6153,"fill":2565,"style":121},"746","Deploy",[99,31262,31263],{"x":31259,"y":18420,"fill":93,"style":126},"main only",[99,31265,31266],{"x":31259,"y":7855,"fill":93,"style":126},"to edge",[95,31268,88,31269,88,31273,88,31276,88,31279,78],{"stroke":93,"fill":205,"style":116},[90,31270],{"d":31271,"style":31272},"M160 168 L182 168","marker-end:url(#gha-arrow)",[90,31274],{"d":31275,"style":31272},"M324 168 L346 168",[90,31277],{"d":31278,"style":31272},"M488 168 L510 168",[90,31280],{"d":31281,"style":31272},"M652 168 L674 168",[107,31283],{"x":845,"y":29203,"width":5417,"height":822,"rx":3579,"fill":162,"opacity":186,"stroke":164,"style":878},[99,31285,31288],{"x":31286,"y":31287,"fill":103,"style":30016},"336","282","Cache restored before build, saved after",[99,31290,31292],{"x":31286,"y":158,"fill":93,"style":31291},"font-size:11.5px;text-anchor:middle","cold 4m10s → warm ~1m05s on a 1,200-page site",[95,31294,88,31296,88,31300,78],{"stroke":164,"fill":205,"style":31295},"stroke-width:1.5px;stroke-dasharray:4 3",[90,31297],{"d":31298,"style":31299},"M418 216 L336 256","marker-end:url(#gha-arrow-y)",[90,31301],{"d":31302,"style":31299},"M460 286 L582 220",[99,31304,31305],{"x":31259,"y":150,"fill":93,"style":31291},"PRs build but skip deploy",[76,31307,78,31308,78,31313,66],{},[80,31309,88,31311,78],{"id":31310,"viewBox":83,"refX":84,"refY":85,"markerWidth":86,"markerHeight":86,"orient":87},"gha-arrow",[90,31312],{"d":92,"fill":93},[80,31314,88,31316,78],{"id":31315,"viewBox":83,"refX":84,"refY":85,"markerWidth":876,"markerHeight":876,"orient":87},"gha-arrow-y",[90,31317],{"d":92,"fill":164},[218,31319,31320],{},"The five steps run top to bottom in one job; the cache step restores before the build and saves after, turning most builds into warm builds.",[34,31322,31324],{"id":31323},"what-you-will-build","What You Will Build",[14,31326,31327],{},"This guide assembles a production pipeline in layers, each independently useful:",[39,31329,31330,31336,31344,31350],{},[42,31331,31332,31335],{},[229,31333,31334],{},"A path-filtered trigger"," with concurrency cancellation, so rapid commits don't queue up redundant runs.",[42,31337,31338,31341,31342,239],{},[229,31339,31340],{},"Two caches"," — the npm download cache and a framework build cache — covered in depth in ",[23,31343,1049],{"href":1048},[42,31345,31346,31349],{},[229,31347,31348],{},"A deterministic install and build"," that produces the same artifact every time.",[42,31351,31352,31357],{},[229,31353,31354,31355],{},"A deploy step gated to ",[253,31356,27507],{},", with credentials pulled from secrets and never written to the log.",[14,31359,31360,31361,31363,31364,31366,31367,239],{},"The same skeleton carries every generator; the Hugo-specific variant — extended edition, ",[253,31362,30581],{},", the separate ",[253,31365,30060],{}," job — lives in ",[23,31368,27602],{"href":27601},[34,31370,31372],{"id":31371},"triggers-path-filters-and-concurrency","Triggers, Path Filters, and Concurrency",[14,31374,31375,31376,31378],{},"Trigger on pushes to ",[253,31377,27507],{}," and on pull requests, filter to the paths that actually affect the build, and cancel superseded runs so a burst of commits doesn't pile three obsolete builds on top of each other:",[987,31380,31382],{"className":1912,"code":31381,"language":1914,"meta":712,"style":712},"name: SSG Build\non:\n  push:\n    branches: [main]\n    paths: ['content\u002F**', 'src\u002F**', 'package-lock.json']\n  pull_request:\n    branches: [main]\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: true\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions\u002Fcheckout@v4\n      - uses: actions\u002Fsetup-node@v4\n        with:\n          node-version: '22'\n          cache: npm\n      - run: npm ci\n      - run: npm run build\n",[253,31383,31384,31393,31399,31405,31415,31437,31443,31453,31459,31468,31476,31482,31488,31496,31502,31512,31522,31528,31537,31545,31555],{"__ignoreMap":712},[995,31385,31386,31388,31390],{"class":997,"line":998},[995,31387,1922],{"class":1921},[995,31389,1925],{"class":1618},[995,31391,31392],{"class":1023},"SSG Build\n",[995,31394,31395,31397],{"class":997,"line":713},[995,31396,1933],{"class":1010},[995,31398,1946],{"class":1618},[995,31400,31401,31403],{"class":997,"line":730},[995,31402,30141],{"class":1921},[995,31404,1946],{"class":1618},[995,31406,31407,31409,31411,31413],{"class":997,"line":1544},[995,31408,30148],{"class":1921},[995,31410,4044],{"class":1618},[995,31412,27507],{"class":1023},[995,31414,4050],{"class":1618},[995,31416,31417,31420,31422,31425,31427,31430,31432,31435],{"class":997,"line":1550},[995,31418,31419],{"class":1921},"    paths",[995,31421,4044],{"class":1618},[995,31423,31424],{"class":1023},"'content\u002F**'",[995,31426,1850],{"class":1618},[995,31428,31429],{"class":1023},"'src\u002F**'",[995,31431,1850],{"class":1618},[995,31433,31434],{"class":1023},"'package-lock.json'",[995,31436,4050],{"class":1618},[995,31438,31439,31441],{"class":997,"line":1673},[995,31440,30736],{"class":1921},[995,31442,1946],{"class":1618},[995,31444,31445,31447,31449,31451],{"class":997,"line":1678},[995,31446,30148],{"class":1921},[995,31448,4044],{"class":1618},[995,31450,27507],{"class":1023},[995,31452,4050],{"class":1618},[995,31454,31455,31457],{"class":997,"line":1693},[995,31456,30195],{"class":1921},[995,31458,1946],{"class":1618},[995,31460,31461,31463,31465],{"class":997,"line":1705},[995,31462,30202],{"class":1921},[995,31464,1925],{"class":1618},[995,31466,31467],{"class":1023},"${{ github.workflow }}-${{ github.ref }}\n",[995,31469,31470,31472,31474],{"class":997,"line":1711},[995,31471,30212],{"class":1921},[995,31473,1925],{"class":1618},[995,31475,6408],{"class":1010},[995,31477,31478,31480],{"class":997,"line":1717},[995,31479,1943],{"class":1921},[995,31481,1946],{"class":1618},[995,31483,31484,31486],{"class":997,"line":1726},[995,31485,1951],{"class":1921},[995,31487,1946],{"class":1618},[995,31489,31490,31492,31494],{"class":997,"line":1732},[995,31491,1958],{"class":1921},[995,31493,1925],{"class":1618},[995,31495,1963],{"class":1023},[995,31497,31498,31500],{"class":997,"line":2967},[995,31499,1968],{"class":1921},[995,31501,1946],{"class":1618},[995,31503,31504,31506,31508,31510],{"class":997,"line":2972},[995,31505,1975],{"class":1618},[995,31507,1978],{"class":1921},[995,31509,1925],{"class":1618},[995,31511,1983],{"class":1023},[995,31513,31514,31516,31518,31520],{"class":997,"line":4147},[995,31515,1975],{"class":1618},[995,31517,1978],{"class":1921},[995,31519,1925],{"class":1618},[995,31521,1994],{"class":1023},[995,31523,31524,31526],{"class":997,"line":4158},[995,31525,1999],{"class":1921},[995,31527,1946],{"class":1618},[995,31529,31530,31532,31534],{"class":997,"line":4168},[995,31531,2006],{"class":1921},[995,31533,1925],{"class":1618},[995,31535,31536],{"class":1023},"'22'\n",[995,31538,31539,31541,31543],{"class":997,"line":4174},[995,31540,2016],{"class":1921},[995,31542,1925],{"class":1618},[995,31544,2021],{"class":1023},[995,31546,31547,31549,31551,31553],{"class":997,"line":17372},[995,31548,1975],{"class":1618},[995,31550,2028],{"class":1921},[995,31552,1925],{"class":1618},[995,31554,12365],{"class":1023},[995,31556,31557,31559,31561,31563],{"class":997,"line":30288},[995,31558,1975],{"class":1618},[995,31560,2028],{"class":1921},[995,31562,1925],{"class":1618},[995,31564,12386],{"class":1023},[14,31566,8896,31567,31570,31571,270,31574,31577],{},[253,31568,31569],{},"paths"," filter matters more than it looks: a docs site that also holds design assets and CI config will otherwise rebuild on a README typo. On a busy repository, scoping triggers to ",[253,31572,31573],{},"content\u002F**",[253,31575,31576],{},"src\u002F**"," cut our weekly Actions minutes by roughly 40% — a direct billing line, not just a convenience.",[14,31579,8896,31580,31582,31583,31586,31587,31590],{},[253,31581,30195],{}," block keyed on ",[253,31584,31585],{},"github.ref"," is what makes a fast feedback loop affordable. Without ",[253,31588,31589],{},"cancel-in-progress",", pushing three fixes in a minute spawns three full builds; with it, the first two are cancelled the instant the third starts. On preview branches that rebuild on every push, this is the single highest-leverage setting in the file.",[433,31592,31593,31606],{},[436,31594,31595],{},[439,31596,31597,31600,31603],{},[442,31598,31599],{},"Trigger setting",[442,31601,31602],{},"Effect",[442,31604,31605],{},"Measured result",[457,31607,31608,31619,31633,31644],{},[439,31609,31610,31613,31616],{},[462,31611,31612],{},"No path filter",[462,31614,31615],{},"Every commit rebuilds",[462,31617,31618],{},"~210 runs\u002Fweek",[439,31620,31621,31627,31630],{},[462,31622,31623,31626],{},[253,31624,31625],{},"paths:"," scoped to build inputs",[462,31628,31629],{},"Only content\u002Fcode triggers",[462,31631,31632],{},"~125 runs\u002Fweek",[439,31634,31635,31638,31641],{},[462,31636,31637],{},"No concurrency group",[462,31639,31640],{},"Superseded runs finish anyway",[462,31642,31643],{},"up to 3 redundant builds\u002Fpush",[439,31645,31646,31651,31654],{},[462,31647,31648],{},[253,31649,31650],{},"cancel-in-progress: true",[462,31652,31653],{},"Old runs cancelled on new push",[462,31655,31656],{},"1 live build per ref",[34,31658,31660],{"id":31659},"toolchain-setup-across-generators","Toolchain Setup Across Generators",[14,31662,31663,31664,7048,31666,31668],{},"The skeleton is identical for every generator — only the setup and build steps differ. For Node-based generators (Astro, Eleventy, and Eleventy\u002FJekyll hybrids running Node tooling), ",[253,31665,29234],{},[253,31667,2042],{}," is all you need:",[987,31670,31672],{"className":1912,"code":31671,"language":1914,"meta":712,"style":712},"- uses: actions\u002Fsetup-node@v4\n  with:\n    node-version: '22'\n    cache: npm\n- run: npm ci\n- run: npm run build      # Astro → dist\u002F, Eleventy → _site\u002F\n",[253,31673,31674,31684,31690,31698,31706,31716],{"__ignoreMap":712},[995,31675,31676,31678,31680,31682],{"class":997,"line":998},[995,31677,3191],{"class":1618},[995,31679,1978],{"class":1921},[995,31681,1925],{"class":1618},[995,31683,1994],{"class":1023},[995,31685,31686,31688],{"class":997,"line":713},[995,31687,3203],{"class":1921},[995,31689,1946],{"class":1618},[995,31691,31692,31694,31696],{"class":997,"line":730},[995,31693,29276],{"class":1921},[995,31695,1925],{"class":1618},[995,31697,31536],{"class":1023},[995,31699,31700,31702,31704],{"class":997,"line":1544},[995,31701,29286],{"class":1921},[995,31703,1925],{"class":1618},[995,31705,2021],{"class":1023},[995,31707,31708,31710,31712,31714],{"class":997,"line":1550},[995,31709,3191],{"class":1618},[995,31711,2028],{"class":1921},[995,31713,1925],{"class":1618},[995,31715,12365],{"class":1023},[995,31717,31718,31720,31722,31724,31726],{"class":997,"line":1673},[995,31719,3191],{"class":1618},[995,31721,2028],{"class":1921},[995,31723,1925],{"class":1618},[995,31725,27116],{"class":1023},[995,31727,31728],{"class":1001},"      # Astro → dist\u002F, Eleventy → _site\u002F\n",[14,31730,31731,31732,7048,31734,31736,31737,31739,31740,31742,31743,31745,31746,239],{},"For Hugo, swap in ",[253,31733,30581],{},[253,31735,30578],{}," (required for SCSS and WebP) and build with ",[253,31738,31039],{},"; for Jekyll, set up Ruby and run ",[253,31741,6524],{},". The full Hugo pipeline, including the separate ",[253,31744,30054],{}," job that GitHub Pages requires, is in ",[23,31747,27602],{"href":27601},[14,31749,31750,31751,270,31754,31757,31758,31761],{},"One detail worth pinning: always pin the toolchain version. ",[253,31752,31753],{},"node-version: '22'",[253,31755,31756],{},"hugo-version: '0.162.0'"," mean a runner image update can't silently change your build. An unpinned ",[253,31759,31760],{},"node-version: 'lts\u002F*'"," will eventually bump a major version on a routine Tuesday and break a native dependency with no commit of yours to blame.",[34,31763,31765],{"id":31764},"dependency-caching-that-actually-helps","Dependency Caching That Actually Helps",[14,31767,31768,31769,31771,31772,31774,31775,31777,31778,31780],{},"The simplest reliable speedup is ",[253,31770,2038],{},"'s built-in ",[253,31773,2042],{},", shown above — it caches the npm download cache at ",[253,31776,2046],{},", keyed on your lockfile, so ",[253,31779,2072],{}," reinstalls from a warm cache instead of re-downloading every tarball. On our site that step alone dropped from 38s to 9s once warm.",[14,31782,31783,31784,31786,31787,31789,31790,31792,31793,239],{},"Resist the temptation to cache ",[253,31785,417],{}," directly. A restored ",[253,31788,417],{}," can carry platform-specific compiled binaries (Sharp, esbuild, Lightning CSS) that mismatch the runner image or Node version and fail in confusing ways. Cache the download cache and let ",[253,31791,2072],{}," rebuild the tree deterministically — it's the difference between a fast build and a flaky one. The full reasoning, with side-by-side timings, is in ",[23,31794,1049],{"href":1048},[34,31796,31798],{"id":31797},"caching-the-framework-build-not-just-dependencies","Caching the Framework Build, Not Just Dependencies",[14,31800,31801],{},"Dependency caching only covers install time. The bigger win on content-heavy sites is caching the generator's own artifacts — processed images, compiled CSS, parsed content — so the build does less work, not just installs faster:",[987,31803,31805],{"className":1912,"code":31804,"language":1914,"meta":712,"style":712},"- name: Cache build artifacts\n  uses: actions\u002Fcache@v4\n  with:\n    path: |\n      .cache\n      node_modules\u002F.astro\n      resources\u002F_gen\n    key: ${{ runner.os }}-build-${{ hashFiles('content\u002F**', 'src\u002F**') }}\n    restore-keys: ${{ runner.os }}-build-\n",[253,31806,31807,31818,31826,31832,31840,31845,31849,31853,31862],{"__ignoreMap":712},[995,31808,31809,31811,31813,31815],{"class":997,"line":998},[995,31810,3191],{"class":1618},[995,31812,1922],{"class":1921},[995,31814,1925],{"class":1618},[995,31816,31817],{"class":1023},"Cache build artifacts\n",[995,31819,31820,31822,31824],{"class":997,"line":713},[995,31821,29479],{"class":1921},[995,31823,1925],{"class":1618},[995,31825,3198],{"class":1023},[995,31827,31828,31830],{"class":997,"line":730},[995,31829,3203],{"class":1921},[995,31831,1946],{"class":1618},[995,31833,31834,31836,31838],{"class":997,"line":1544},[995,31835,3210],{"class":1921},[995,31837,1925],{"class":1618},[995,31839,3215],{"class":1614},[995,31841,31842],{"class":997,"line":1550},[995,31843,31844],{"class":1023},"      .cache\n",[995,31846,31847],{"class":997,"line":1673},[995,31848,3230],{"class":1023},[995,31850,31851],{"class":997,"line":1678},[995,31852,3220],{"class":1023},[995,31854,31855,31857,31859],{"class":997,"line":1693},[995,31856,3235],{"class":1921},[995,31858,1925],{"class":1618},[995,31860,31861],{"class":1023},"${{ runner.os }}-build-${{ hashFiles('content\u002F**', 'src\u002F**') }}\n",[995,31863,31864,31866,31868],{"class":997,"line":1705},[995,31865,29512],{"class":1921},[995,31867,1925],{"class":1618},[995,31869,31870],{"class":1023},"${{ runner.os }}-build-\n",[14,31872,31873,31874,31876,31877,31879,31880,31882,31883,31885,31886,31888],{},"That single cache covers Eleventy's ",[253,31875,2309],{}," (used by ",[253,31878,2125],{},"), Astro's ",[253,31881,3170],{}," (its processed-image and content cache), and Hugo's ",[253,31884,3253],{},". Keying on a content hash rather than the lockfile means the cache survives dependency bumps and only refreshes when the actual inputs change. The ",[253,31887,29547],{}," fallback is mandatory: without it, the first content change after any commit misses the cache entirely and rebuilds from cold.",[433,31890,31891,31903],{},[436,31892,31893],{},[439,31894,31895,31898,31900],{},[442,31896,31897],{},"Build stage",[442,31899,5040],{},[442,31901,31902],{},"Warm (both caches)",[457,31904,31905,31915,31924,31935],{},[439,31906,31907,31911,31913],{},[462,31908,31909],{},[253,31910,2072],{},[462,31912,2075],{},[462,31914,2078],{},[439,31916,31917,31919,31922],{},[462,31918,5025],{},[462,31920,31921],{},"1m52s",[462,31923,7201],{},[439,31925,31926,31929,31932],{},[462,31927,31928],{},"Site render",[462,31930,31931],{},"1m20s",[462,31933,31934],{},"50s",[439,31936,31937,31941,31946],{},[462,31938,31939],{},[229,31940,5033],{},[462,31942,31943],{},[229,31944,31945],{},"~4m10s",[462,31947,31948],{},[229,31949,31950],{},"~1m05s",[14,31952,31953,31954,2204,31956,31958,31959,239],{},"The image-processing line is where caching pays off most — re-encoding 800 source images on every run is pure waste, and a content-keyed ",[253,31955,3170],{},[253,31957,3253],{}," cache makes it nearly free. The same caching discipline carries across generators; for the Hugo-on-Pages variant see ",[23,31960,5002],{"href":5001},[34,31962,31964],{"id":31963},"deterministic-installs","Deterministic Installs",[14,31966,2360,31967,256,31969,10335,31972,31975,31976,31978,31979,31981,31982,31984,31985,31987],{},[253,31968,2072],{},[253,31970,31971],{},"pnpm install --frozen-lockfile",[253,31973,31974],{},"yarn install --immutable",") for every CI install. Unlike ",[253,31977,27449],{},", it installs strictly from the committed lockfile, fails if ",[253,31980,21912],{}," and the lockfile disagree, and removes ",[253,31983,417],{}," first so there's no leftover state. The result is the same dependency tree on every runner — which is the whole point of CI. A workflow that uses ",[253,31986,27449],{}," will, sooner or later, resolve a slightly different transitive dependency on one runner and produce an artifact that doesn't match what you tested locally.",[34,31989,31991],{"id":31990},"safe-conditional-deploys","Safe, Conditional Deploys",[14,31993,31994,31995,31997],{},"Restrict production deploys to ",[253,31996,27507],{}," pushes so a pull-request build can't ship, and inject credentials from secrets rather than hardcoding them:",[987,31999,32001],{"className":1912,"code":32000,"language":1914,"meta":712,"style":712},"- name: Deploy to production\n  if: github.ref == 'refs\u002Fheads\u002Fmain' && github.event_name == 'push'\n  env:\n    CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}\n  run: |\n    npx wrangler pages deploy .\u002Fdist \\\n      --project-name \"${{ vars.PROJECT_NAME }}\" \\\n      --branch main\n",[253,32002,32003,32014,32023,32030,32040,32048,32053,32058],{"__ignoreMap":712},[995,32004,32005,32007,32009,32011],{"class":997,"line":998},[995,32006,3191],{"class":1618},[995,32008,1922],{"class":1921},[995,32010,1925],{"class":1618},[995,32012,32013],{"class":1023},"Deploy to production\n",[995,32015,32016,32018,32020],{"class":997,"line":713},[995,32017,18900],{"class":1921},[995,32019,1925],{"class":1618},[995,32021,32022],{"class":1023},"github.ref == 'refs\u002Fheads\u002Fmain' && github.event_name == 'push'\n",[995,32024,32025,32028],{"class":997,"line":730},[995,32026,32027],{"class":1921},"  env",[995,32029,1946],{"class":1618},[995,32031,32032,32035,32037],{"class":997,"line":1544},[995,32033,32034],{"class":1921},"    CLOUDFLARE_API_TOKEN",[995,32036,1925],{"class":1618},[995,32038,32039],{"class":1023},"${{ secrets.CLOUDFLARE_API_TOKEN }}\n",[995,32041,32042,32044,32046],{"class":997,"line":1550},[995,32043,14197],{"class":1921},[995,32045,1925],{"class":1618},[995,32047,3215],{"class":1614},[995,32049,32050],{"class":997,"line":1673},[995,32051,32052],{"class":1023},"    npx wrangler pages deploy .\u002Fdist \\\n",[995,32054,32055],{"class":997,"line":1678},[995,32056,32057],{"class":1023},"      --project-name \"${{ vars.PROJECT_NAME }}\" \\\n",[995,32059,32060],{"class":997,"line":1693},[995,32061,32062],{"class":1023},"      --branch main\n",[14,32064,8896,32065,32067,32068,32070,32071,32073],{},[253,32066,22753],{}," guard is the safety rail: the build job still runs on every pull request (so reviewers get a green check and any quality gates run), but only a push to ",[253,32069,27507],{}," reaches the deploy step. Pull requests get their own throwaway URL through ",[23,32072,28330],{"href":28329},", kept entirely separate from production.",[14,32075,32076,32077,32079,32080,32082],{},"For credentials, prefer OIDC where the provider supports it: the workflow exchanges GitHub's identity token for a short-lived cloud credential, so there's no long-lived secret to leak or rotate. When you must use a static token, scope it to a protected GitHub environment with required reviewers on production. Pair the deploy with edge caching from ",[23,32078,26988],{"href":26987},", and compare host-managed build caches in ",[23,32081,28797],{"href":28796}," if you'd rather let the platform own the pipeline.",[34,32084,32086],{"id":32085},"monitoring-pipeline-health","Monitoring Pipeline Health",[14,32088,32089],{},"A fast pipeline that fails silently is worse than a slow one you trust. Track five numbers and the workflow tells you when it's drifting:",[39,32091,32092,32097,32103,32112,32118],{},[42,32093,32094,32096],{},[229,32095,18134],{}," — the headline number. A creeping increase usually means a cache key that stopped matching or a media directory that outgrew the cache.",[42,32098,32099,32102],{},[229,32100,32101],{},"Cache hit ratio"," — read it off the cache step in the job summary. A sudden run of misses points at a key change, a content-hash churn, or a cache eviction from GitHub's 10 GB-per-repo limit.",[42,32104,32105,32108,32109,32111],{},[229,32106,32107],{},"Artifact size"," — a ",[253,32110,8885],{}," that jumps 30% overnight is a regression (an un-optimized image, a vendored dependency) you want to catch before it ships.",[42,32113,32114,32117],{},[229,32115,32116],{},"Deploy success rate"," — flaky deploys are almost always a credential or rate-limit issue, not your code.",[42,32119,32120,32123],{},[229,32121,32122],{},"Time-to-preview"," — how long from PR push to a clickable URL; this is the number contributors actually feel.",[14,32125,32126],{},"You don't need a dashboard to start — the per-step durations in the Actions job summary are enough to spot the trend. When a build slows down, the cache step is the first place to look.",[34,32128,32130],{"id":32129},"matrix-builds-when-you-actually-need-them","Matrix Builds, When You Actually Need Them",[14,32132,32133],{},"A build matrix runs the same job across several configurations in parallel. For a static site it's overkill in the common case — you ship one artifact from one Node version — but it earns its keep in two situations. The first is validating a library or theme across Node versions before you bump the floor:",[987,32135,32137],{"className":1912,"code":32136,"language":1914,"meta":712,"style":712},"strategy:\n  matrix:\n    node: ['20', '22']\n",[253,32138,32139,32146,32153],{"__ignoreMap":712},[995,32140,32141,32144],{"class":997,"line":998},[995,32142,32143],{"class":1921},"strategy",[995,32145,1946],{"class":1618},[995,32147,32148,32151],{"class":997,"line":713},[995,32149,32150],{"class":1921},"  matrix",[995,32152,1946],{"class":1618},[995,32154,32155,32158,32160,32163,32165,32168],{"class":997,"line":730},[995,32156,32157],{"class":1921},"    node",[995,32159,4044],{"class":1618},[995,32161,32162],{"class":1023},"'20'",[995,32164,1850],{"class":1618},[995,32166,32167],{"class":1023},"'22'",[995,32169,4050],{"class":1618},[14,32171,32172],{},"The second is a monorepo with several independent sites, where a matrix over project directories builds each in its own runner. For a single site, prefer a single pinned Node version: a matrix triples your Actions minutes for a guarantee you rarely need, and the \"which combination actually deploys\" question gets murky fast. Validate multiple versions only when a real consumer runs them.",[34,32174,2266],{"id":2265},[39,32176,32177,32189,32205,32215,32224,32233],{},[42,32178,32179,32185,32186,32188],{},[229,32180,32181,17566,32183,931],{},[253,32182,27449],{},[253,32184,2072],{}," non-deterministic resolution across runners produces an artifact that doesn't match local. Use ",[253,32187,2072],{}," against a committed lockfile.",[42,32190,32191,32196,32197,7666,32199,32201,32202,32204],{},[229,32192,7582,32193,32195],{},[253,32194,417],{}," directly:"," brittle across Node versions and runner images because of compiled binaries. Cache ",[253,32198,2046],{},[253,32200,2038],{}," and let ",[253,32203,2072],{}," rebuild.",[42,32206,32207,32211,32212,32214],{},[229,32208,14582,32209,931],{},[253,32210,29547],{}," without a fallback prefix, any input change misses the cache entirely and rebuilds from cold. Always add a ",[253,32213,29547],{}," prefix.",[42,32216,32217,32220,32221,32223],{},[229,32218,32219],{},"Secrets in YAML:"," never hardcode tokens. Use repository or environment secrets injected via ",[253,32222,30677],{},", or OIDC for cloud providers.",[42,32225,32226,692,32229,32232],{},[229,32227,32228],{},"Unpinned toolchain:",[253,32230,32231],{},"lts\u002F*"," or a floating Hugo version means a runner image update can break your build with no code change. Pin exact versions.",[42,32234,32235,32240],{},[229,32236,14582,32237,32239],{},[253,32238,31569],{}," filter on a mixed repo:"," every README edit triggers a full build and burns Actions minutes. Scope triggers to real build inputs.",[34,32242,2321],{"id":2320},[39,32244,32245,32254,32260,32268,32277],{},[42,32246,32247,32248,1850,32251,32253],{},"The pipeline is small and the same across generators: checkout, setup, cache, ",[253,32249,32250],{},"npm ci && npm run build",[253,32252,27507],{},"-only deploy.",[42,32255,32256,32257,32259],{},"Two caches do the work — ",[253,32258,2046],{}," for installs and a content-keyed framework cache for artifacts. Together they took our build from ~4m10s to ~1m05s.",[42,32261,2360,32262,32264,32265,32267],{},[253,32263,2072],{}," for deterministic installs; never cache ",[253,32266,417],{}," directly.",[42,32269,32270,32271,32273,32274,32276],{},"Gate deploys to ",[253,32272,27507],{}," pushes with an ",[253,32275,22753],{}," guard and pull credentials from secrets, preferring OIDC.",[42,32278,32279],{},"Pin toolchain versions and filter triggers by path so neither a runner update nor a docs typo can derail a build.",[34,32281,651],{"id":650},[653,32283,32285],{"id":32284},"how-do-i-cut-build-time-for-large-ssg-projects","How do I cut build time for large SSG projects?",[14,32287,32288,32289,32291,32292,32294],{},"Cache dependencies with ",[253,32290,2038],{},"'s npm cache, add a framework build cache for the generated artifacts, run ",[253,32293,2072],{}," for deterministic installs, and offload heavy media to a CDN. On our 1,200-page Astro site these changes took a cold 4m10s build down to about 1m05s warm.",[653,32296,32298],{"id":32297},"should-i-cache-node_modules-directly-in-github-actions","Should I cache node_modules directly in GitHub Actions?",[14,32300,32301,32302,7666,32304,32201,32306,32308,32309,32311,32312,32314],{},"No. Cache the npm download cache at ",[253,32303,2046],{},[253,32305,2038],{},[253,32307,2072],{}," rebuild ",[253,32310,417],{},". A restored ",[253,32313,417],{}," can carry platform-specific binaries that break across Node versions or runner images, which produces hard-to-debug failures.",[653,32316,32318],{"id":32317},"can-github-actions-deploy-to-the-edge-without-a-third-party-platform-action","Can GitHub Actions deploy to the edge without a third-party platform action?",[14,32320,32321],{},"Yes. Wrangler pushes to Cloudflare Pages, the AWS CLI syncs to S3 plus CloudFront, and rsync over SSH copies to your own server. You only need the credentials in repository secrets and a deploy step gated to pushes on the main branch.",[653,32323,32325],{"id":32324},"how-do-i-keep-secrets-safe-in-a-public-repo-workflow","How do I keep secrets safe in a public-repo workflow?",[14,32327,32328,32329,32331],{},"Store them as repository or environment secrets and inject them through the ",[253,32330,30677],{}," block, never inline in YAML. For cloud providers prefer OIDC so the workflow exchanges a short-lived token instead of holding a long-lived key, and scope deploy secrets to a protected environment.",[653,32333,32335],{"id":32334},"why-does-my-cache-never-get-a-hit","Why does my cache never get a hit?",[14,32337,32338,32339,32341,32342,32344],{},"Usually a missing ",[253,32340,29547],{}," fallback or a key that changes every run. Key the cache on the lockfile hash for dependencies and on content hashes for build artifacts, and always add a ",[253,32343,29547],{}," prefix so a near-miss still warm-starts instead of rebuilding from cold.",[653,32346,32348],{"id":32347},"does-this-workflow-work-the-same-for-hugo-and-jekyll","Does this workflow work the same for Hugo and Jekyll?",[14,32350,32351,32352,32354],{},"The shape is identical — checkout, set up the toolchain, restore cache, build, deploy. Only the setup and build steps change. Hugo needs the extended edition and the hugo CLI, Jekyll needs Ruby and ",[253,32353,6524],{},", and the output directory differs per generator.",[34,32356,684],{"id":683},[39,32358,32359,32366,32371,32381,32386],{},[42,32360,32361,692,32363,32365],{},[229,32362,691],{},[23,32364,5505],{"href":5504}," — reproducible builds, atomic deploys, and rollback.",[42,32367,32368,32370],{},[23,32369,27602],{"href":27601}," — the Hugo-specific two-job pipeline.",[42,32372,32373,32375,32376,32378,32379,239],{},[23,32374,1049],{"href":1048}," — why to cache ",[253,32377,2046],{},", not ",[253,32380,417],{},[42,32382,32383,32385],{},[23,32384,28330],{"href":28329}," — give every PR its own deployed URL.",[42,32387,32388,32390],{},[23,32389,26988],{"href":26987}," — the two-tier cache policy at the edge.",[1346,32392,32393],{},"html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .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}",{"title":712,"searchDepth":713,"depth":713,"links":32395},[32396,32397,32398,32399,32400,32401,32402,32403,32404,32405,32406,32407,32415],{"id":31323,"depth":713,"text":31324},{"id":31371,"depth":713,"text":31372},{"id":31659,"depth":713,"text":31660},{"id":31764,"depth":713,"text":31765},{"id":31797,"depth":713,"text":31798},{"id":31963,"depth":713,"text":31964},{"id":31990,"depth":713,"text":31991},{"id":32085,"depth":713,"text":32086},{"id":32129,"depth":713,"text":32130},{"id":2265,"depth":713,"text":2266},{"id":2320,"depth":713,"text":2321},{"id":650,"depth":713,"text":651,"children":32408},[32409,32410,32411,32412,32413,32414],{"id":32284,"depth":730,"text":32285},{"id":32297,"depth":730,"text":32298},{"id":32317,"depth":730,"text":32318},{"id":32324,"depth":730,"text":32325},{"id":32334,"depth":730,"text":32335},{"id":32347,"depth":730,"text":32348},{"id":683,"depth":713,"text":684},[32417,32418,32419],{"name":737,"item":738},{"name":5505,"item":5504},{"name":28200,"item":28199},"Build and deploy static sites with GitHub Actions — path-filtered triggers, dependency and build caching, deterministic installs, and atomic deploys on every push.",[32422,32424,32426,32427,32429,32431],{"q":32285,"a":32423},"Cache dependencies with setup-node's npm cache, add a framework build cache for the generated artifacts, run npm ci for deterministic installs, and offload heavy media to a CDN. On our 1,200-page Astro site these changes took a cold 4m10s build down to about 1m05s warm.",{"q":32298,"a":32425},"No. Cache the npm download cache at ~\u002F.npm via setup-node and let npm ci rebuild node_modules. A restored node_modules can carry platform-specific binaries that break across Node versions or runner images, which produces hard-to-debug failures.",{"q":32318,"a":32321},{"q":32325,"a":32428},"Store them as repository or environment secrets and inject them through the env block, never inline in YAML. For cloud providers prefer OIDC so the workflow exchanges a short-lived token instead of holding a long-lived key, and scope deploy secrets to a protected environment.",{"q":32335,"a":32430},"Usually a missing restore-keys fallback or a key that changes every run. Key the cache on the lockfile hash for dependencies and on content hashes for build artifacts, and always add a restore-keys prefix so a near-miss still warm-starts instead of rebuilding from cold.",{"q":32348,"a":32432},"The shape is identical — checkout, set up the toolchain, restore cache, build, deploy. Only the setup and build steps change. Hugo needs the extended edition and the hugo CLI, Jekyll needs Ruby and bundle exec jekyll build, and the output directory differs per generator.",{},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fgithub-actions-for-automated-ssg-builds",{"title":28200,"description":32420},"production-ready-deployment-cicd-workflows\u002Fgithub-actions-for-automated-ssg-builds\u002Findex","Ra8skhMqn5AFxYUni_KcZYIZNiWNQgbC8_1xPHPhqjQ",{"id":32439,"title":5002,"body":32440,"breadcrumb":33166,"dateModified":743,"datePublished":743,"description":33171,"extension":745,"faq":33172,"meta":33182,"navigation":752,"path":33183,"seo":33184,"slug":32444,"stem":33185,"type":756,"__hash__":33186},"content\u002Fproduction-ready-deployment-cicd-workflows\u002Fincremental-builds-and-build-caching-for-ssgs\u002Fcaching-hugo-builds-in-github-actions\u002Findex.md",{"type":7,"value":32441,"toc":33150},[32442,32445,32456,32458,32481,32485,32488,32511,32602,32604,32610,32886,32906,32908,32919,32975,32989,32991,33045,33047,33064,33066,33070,33073,33077,33083,33087,33096,33100,33108,33112,33118,33120,33148],[10,32443,5002],{"id":32444},"caching-hugo-builds-in-github-actions",[14,32446,32447,32448,32451,32452,239],{},"Hugo is famously fast at rendering Markdown, so people are often surprised when their GitHub Actions build is slow. The render is rarely the bottleneck — it is the ",[18,32449,32450],{},"asset processing",". Resizing images, compiling Sass, and fetching Hugo Modules all happen on every cold runner unless you cache them. The good news is that Hugo stores those results in predictable, content-hashed directories, so caching them in CI is both safe and effective. This guide gives the workflow, the cache key design, and the measured CI minutes saved. It is the Hugo companion to ",[23,32453,32455],{"href":32454},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fincremental-builds-and-build-caching-for-ssgs\u002F","Incremental Builds and Build Caching for SSGs",[34,32457,37],{"id":36},[39,32459,32460,32463,32475],{},[42,32461,32462],{},"A Hugo site building in GitHub Actions (extended Hugo if you compile Sass\u002FSCSS).",[42,32464,32465,32466,738,32468,32470,32471,32474],{},"Asset processing worth caching — image ",[253,32467,3699],{},[253,32469,3705],{}," operations, ",[253,32472,32473],{},"resources.ToCSS",", or Hugo Modules. A pure-Markdown site with a vendored theme gains little.",[42,32476,32477,32480],{},[253,32478,32479],{},"actions\u002Fcache@v4"," available (it is, on GitHub-hosted runners).",[34,32482,32484],{"id":32483},"what-hugo-caches-and-where","What Hugo Caches and Where",[14,32486,32487],{},"Two directories carry the cost between builds:",[39,32489,32490,32501],{},[42,32491,32492,32496,32497,32500],{},[229,32493,32494],{},[253,32495,3253],{}," — processed images and compiled stylesheets, each named by a hash of its source plus the transform options. Because the names are content-hashed, a restored cache is correct by construction: an entry only matches when the source and options are byte-for-byte identical, so Hugo regenerates exactly what changed and reuses the rest. This is the same fingerprinting logic that makes ",[23,32498,32499],{"href":14049},"proper cache headers on Netlify"," safe at the edge.",[42,32502,32503,32506,32507,32510],{},[229,32504,32505],{},"The Go module cache"," — if you use Hugo Modules, dependencies are downloaded into the module cache (",[253,32508,32509],{},"$HOME\u002Fgo\u002Fpkg\u002Fmod"," and Hugo's own module cache). Restoring it skips the network fetch on every run.",[55,32512,32513,32599],{},[58,32514,66,32518,66,32521,66,32524,66,32593],{"viewBox":4608,"role":61,"ariaLabelledBy":32515,"xmlns":65},[32516,32517],"hugo-cache-title","hugo-cache-desc",[68,32519,32520],{"id":32516},"GitHub Actions cache hit and miss flow for Hugo",[72,32522,32523],{"id":32517},"A flow showing a build that hashes assets into a cache key, then branches: on a cache hit it restores resources\u002F_gen and processes nothing, finishing fast; on a miss it processes all assets and saves a new cache.",[95,32525,78,32526,78,32529,78,32531,78,32534,78,32537,78,32539,78,32543,78,32554,78,32556,78,32558,78,32561,78,32564,78,32566,78,32570,78,32572,78,32574,78,32577,78,32580,78,32582,78,32585,66],{"style":97},[99,32527,32528],{"x":816,"y":109,"fill":103,"style":104},"Cache hit vs. miss on resources\u002F_gen",[107,32530],{"x":158,"y":822,"width":7852,"height":2595,"rx":3579,"fill":824,"opacity":186,"stroke":824,"style":116},[99,32532,32533],{"x":816,"y":1430,"fill":103,"style":829},"hash assets\u002F**",[99,32535,32536],{"x":816,"y":6849,"fill":93,"style":126},"→ cache key",[107,32538],{"x":158,"y":2563,"width":7852,"height":1464,"rx":3579,"fill":114,"opacity":186,"stroke":114,"style":116},[99,32540,32542],{"x":816,"y":32541,"fill":114,"style":121},"157","key match?",[95,32544,88,32545,88,32548,88,32551,78],{"stroke":93,"fill":205,"style":116},[90,32546],{"d":32547,"style":19461},"M380 106 L380 128",[90,32549],{"d":32550,"style":19461},"M300 152 L180 152",[90,32552],{"d":32553,"style":19461},"M460 152 L580 152",[99,32555,14682],{"x":159,"y":5379,"fill":187,"style":882},[107,32557],{"x":3578,"y":7852,"width":7852,"height":2595,"rx":3579,"fill":185,"opacity":886,"stroke":187,"style":116},[99,32559,32560],{"x":1431,"y":845,"fill":103,"style":829},"restore _gen",[99,32562,32563],{"x":1431,"y":17841,"fill":93,"style":126},"process nothing",[107,32565],{"x":3578,"y":5407,"width":7852,"height":1464,"rx":3579,"fill":187,"opacity":163,"stroke":187,"style":116},[99,32567,32569],{"x":1431,"y":32568,"fill":187,"style":121},"259","build ~25s",[99,32571,14685],{"x":190,"y":5379,"fill":2565,"style":882},[107,32573],{"x":10820,"y":7852,"width":194,"height":2595,"rx":3579,"fill":2564,"opacity":825,"stroke":2565,"style":116},[99,32575,32576],{"x":3534,"y":845,"fill":103,"style":829},"process all assets",[99,32578,32579],{"x":3534,"y":17841,"fill":93,"style":126},"save new cache",[107,32581],{"x":10820,"y":5407,"width":194,"height":1464,"rx":3579,"fill":2565,"opacity":850,"stroke":2565,"style":116},[99,32583,32584],{"x":3534,"y":32568,"fill":2565,"style":121},"build ~95s",[95,32586,88,32587,88,32590,78],{"stroke":93,"fill":205,"style":116},[90,32588],{"d":32589,"style":19461},"M120 210 L120 230",[90,32591],{"d":32592,"style":19461},"M645 210 L645 230",[76,32594,78,32595,66],{},[80,32596,88,32597,78],{"id":19475,"viewBox":83,"refX":84,"refY":85,"markerWidth":86,"markerHeight":86,"orient":87},[90,32598],{"d":92,"fill":93},[218,32600,32601],{},"The build hashes its assets into a cache key. A hit restores resources\u002F_gen and processes nothing; a miss reprocesses everything and saves a fresh cache for next time.",[34,32603,19484],{"id":19483},[14,32605,32606,32607,32609],{},"Add two cache steps before the build — one for ",[253,32608,3253],{},", one for the module cache — each keyed on the inputs that determine its contents:",[987,32611,32613],{"className":1912,"code":32612,"language":1914,"meta":712,"style":712},"name: Build Hugo\non:\n  push:\n    branches: [main]\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions\u002Fcheckout@v4\n        with:\n          submodules: recursive   # theme submodules\n\n      - name: Cache Hugo processed resources\n        uses: actions\u002Fcache@v4\n        with:\n          path: resources\u002F_gen\n          key: hugo-gen-${{ runner.os }}-${{ hashFiles('assets\u002F**', 'config\u002F**', 'hugo.toml') }}\n          restore-keys: |\n            hugo-gen-${{ runner.os }}-\n\n      - name: Cache Hugo module cache\n        uses: actions\u002Fcache@v4\n        with:\n          path: |\n            ~\u002Fgo\u002Fpkg\u002Fmod\n            ~\u002F.cache\u002Fhugo_cache\n          key: hugo-mod-${{ runner.os }}-${{ hashFiles('go.sum') }}\n          restore-keys: |\n            hugo-mod-${{ runner.os }}-\n\n      - name: Setup Hugo\n        uses: peaceiris\u002Factions-hugo@v3\n        with:\n          hugo-version: '0.140.0'\n          extended: true\n\n      - run: hugo --gc --minify\n",[253,32614,32615,32623,32629,32635,32645,32651,32657,32665,32671,32681,32687,32698,32702,32713,32721,32727,32736,32745,32753,32758,32762,32773,32781,32787,32795,32800,32804,32813,32821,32826,32830,32841,32849,32855,32864,32872,32876],{"__ignoreMap":712},[995,32616,32617,32619,32621],{"class":997,"line":998},[995,32618,1922],{"class":1921},[995,32620,1925],{"class":1618},[995,32622,4037],{"class":1023},[995,32624,32625,32627],{"class":997,"line":713},[995,32626,1933],{"class":1010},[995,32628,1946],{"class":1618},[995,32630,32631,32633],{"class":997,"line":730},[995,32632,30141],{"class":1921},[995,32634,1946],{"class":1618},[995,32636,32637,32639,32641,32643],{"class":997,"line":1544},[995,32638,30148],{"class":1921},[995,32640,4044],{"class":1618},[995,32642,27507],{"class":1023},[995,32644,4050],{"class":1618},[995,32646,32647,32649],{"class":997,"line":1550},[995,32648,1943],{"class":1921},[995,32650,1946],{"class":1618},[995,32652,32653,32655],{"class":997,"line":1673},[995,32654,1951],{"class":1921},[995,32656,1946],{"class":1618},[995,32658,32659,32661,32663],{"class":997,"line":1678},[995,32660,1958],{"class":1921},[995,32662,1925],{"class":1618},[995,32664,1963],{"class":1023},[995,32666,32667,32669],{"class":997,"line":1693},[995,32668,1968],{"class":1921},[995,32670,1946],{"class":1618},[995,32672,32673,32675,32677,32679],{"class":997,"line":1705},[995,32674,1975],{"class":1618},[995,32676,1978],{"class":1921},[995,32678,1925],{"class":1618},[995,32680,1983],{"class":1023},[995,32682,32683,32685],{"class":997,"line":1711},[995,32684,1999],{"class":1921},[995,32686,1946],{"class":1618},[995,32688,32689,32691,32693,32695],{"class":997,"line":1717},[995,32690,30291],{"class":1921},[995,32692,1925],{"class":1618},[995,32694,30805],{"class":1023},[995,32696,32697],{"class":1001},"   # theme submodules\n",[995,32699,32700],{"class":997,"line":1726},[995,32701,1541],{"emptyLinePlaceholder":752},[995,32703,32704,32706,32708,32710],{"class":997,"line":1732},[995,32705,1975],{"class":1618},[995,32707,1922],{"class":1921},[995,32709,1925],{"class":1618},[995,32711,32712],{"class":1023},"Cache Hugo processed resources\n",[995,32714,32715,32717,32719],{"class":997,"line":2967},[995,32716,30369],{"class":1921},[995,32718,1925],{"class":1618},[995,32720,3198],{"class":1023},[995,32722,32723,32725],{"class":997,"line":2972},[995,32724,1999],{"class":1921},[995,32726,1946],{"class":1618},[995,32728,32729,32731,32733],{"class":997,"line":4147},[995,32730,4130],{"class":1921},[995,32732,1925],{"class":1618},[995,32734,32735],{"class":1023},"resources\u002F_gen\n",[995,32737,32738,32740,32742],{"class":997,"line":4158},[995,32739,4150],{"class":1921},[995,32741,1925],{"class":1618},[995,32743,32744],{"class":1023},"hugo-gen-${{ runner.os }}-${{ hashFiles('assets\u002F**', 'config\u002F**', 'hugo.toml') }}\n",[995,32746,32747,32749,32751],{"class":997,"line":4168},[995,32748,4161],{"class":1921},[995,32750,1925],{"class":1618},[995,32752,3215],{"class":1614},[995,32754,32755],{"class":997,"line":4174},[995,32756,32757],{"class":1023},"            hugo-gen-${{ runner.os }}-\n",[995,32759,32760],{"class":997,"line":17372},[995,32761,1541],{"emptyLinePlaceholder":752},[995,32763,32764,32766,32768,32770],{"class":997,"line":30288},[995,32765,1975],{"class":1618},[995,32767,1922],{"class":1921},[995,32769,1925],{"class":1618},[995,32771,32772],{"class":1023},"Cache Hugo module cache\n",[995,32774,32775,32777,32779],{"class":997,"line":30299},[995,32776,30369],{"class":1921},[995,32778,1925],{"class":1618},[995,32780,3198],{"class":1023},[995,32782,32783,32785],{"class":997,"line":30311},[995,32784,1999],{"class":1921},[995,32786,1946],{"class":1618},[995,32788,32789,32791,32793],{"class":997,"line":30323},[995,32790,4130],{"class":1921},[995,32792,1925],{"class":1618},[995,32794,3215],{"class":1614},[995,32796,32797],{"class":997,"line":30330},[995,32798,32799],{"class":1023},"            ~\u002Fgo\u002Fpkg\u002Fmod\n",[995,32801,32802],{"class":997,"line":30341},[995,32803,4144],{"class":1023},[995,32805,32806,32808,32810],{"class":997,"line":30354},[995,32807,4150],{"class":1921},[995,32809,1925],{"class":1618},[995,32811,32812],{"class":1023},"hugo-mod-${{ runner.os }}-${{ hashFiles('go.sum') }}\n",[995,32814,32815,32817,32819],{"class":997,"line":30366},[995,32816,4161],{"class":1921},[995,32818,1925],{"class":1618},[995,32820,3215],{"class":1614},[995,32822,32823],{"class":997,"line":30376},[995,32824,32825],{"class":1023},"            hugo-mod-${{ runner.os }}-\n",[995,32827,32828],{"class":997,"line":30383},[995,32829,1541],{"emptyLinePlaceholder":752},[995,32831,32832,32834,32836,32838],{"class":997,"line":30392},[995,32833,1975],{"class":1618},[995,32835,1922],{"class":1921},[995,32837,1925],{"class":1618},[995,32839,32840],{"class":1023},"Setup Hugo\n",[995,32842,32843,32845,32847],{"class":997,"line":30397},[995,32844,30369],{"class":1921},[995,32846,1925],{"class":1618},[995,32848,30320],{"class":1023},[995,32850,32851,32853],{"class":997,"line":30402},[995,32852,1999],{"class":1921},[995,32854,1946],{"class":1618},[995,32856,32857,32859,32861],{"class":997,"line":30412},[995,32858,30333],{"class":1921},[995,32860,1925],{"class":1618},[995,32862,32863],{"class":1023},"'0.140.0'\n",[995,32865,32866,32868,32870],{"class":997,"line":30422},[995,32867,30344],{"class":1921},[995,32869,1925],{"class":1618},[995,32871,6408],{"class":1010},[995,32873,32874],{"class":997,"line":30434},[995,32875,1541],{"emptyLinePlaceholder":752},[995,32877,32878,32880,32882,32884],{"class":997,"line":30446},[995,32879,1975],{"class":1618},[995,32881,2028],{"class":1921},[995,32883,1925],{"class":1618},[995,32885,4183],{"class":1023},[14,32887,8896,32888,32890,32891,32894,32895,32897,32898,32900,32901,32903,32904,239],{},[253,32889,3253],{}," key hashes ",[253,32892,32893],{},"assets\u002F**",", your config, and ",[253,32896,12901],{},", so any change to a source image, a Sass file, or a transform option busts only the relevant entry. The module cache keys on ",[253,32899,31011],{},", so it is reused until your module versions change. The ",[253,32902,29547],{}," prefixes keep a near-miss warm rather than starting cold — the same key discipline laid out in ",[23,32905,1049],{"href":1048},[34,32907,1166],{"id":1165},[14,32909,32910,32911,738,32913,32915,32916,32918],{},"Benchmarked on a Hugo site with ~600 pages and ~180 source images doing ",[253,32912,3699],{},[253,32914,3705],{}," plus Sass compilation, built with extended Hugo 0.140 on ",[253,32917,29592],{},". Times are from the GitHub Actions run summary:",[433,32920,32921,32933],{},[436,32922,32923],{},[439,32924,32925,32927,32930],{},[442,32926,940],{},[442,32928,32929],{},"Build step time",[442,32931,32932],{},"Billable minutes",[457,32934,32935,32946,32956,32966],{},[439,32936,32937,32940,32943],{},[462,32938,32939],{},"Cold build, no cache",[462,32941,32942],{},"95 s",[462,32944,32945],{},"2 (rounded up)",[439,32947,32948,32951,32954],{},[462,32949,32950],{},"Warm cache, content edit (one post)",[462,32952,32953],{},"25 s",[462,32955,11536],{},[439,32957,32958,32961,32964],{},[462,32959,32960],{},"Warm cache, one new image added",[462,32962,32963],{},"31 s",[462,32965,11536],{},[439,32967,32968,32971,32973],{},[462,32969,32970],{},"Cache key change (config edited)",[462,32972,32942],{},[462,32974,475],{},[14,32976,32977,32978,32980,32981,32984,32985,32988],{},"On the common case — a content edit with no asset change — caching ",[253,32979,3253],{}," cut the build step from ",[229,32982,32983],{},"95 s to 25 s",", about a 74% reduction, and dropped the billable minute count from 2 to 1. Across a team merging 40 PRs a week, that is roughly ",[229,32986,32987],{},"40 billable minutes saved per week"," on this one job, with restores adding only a few seconds of overhead. The cache-key-change row is the honest ceiling: editing the config or a transform option correctly reprocesses everything, because the inputs really did change.",[34,32990,600],{"id":599},[39,32992,32993,33007,33019,33028,33037],{},[42,32994,32995,32998,32999,2204,33001,33003,33004,33006],{},[229,32996,32997],{},"Caching the wrong path."," Caching ",[253,33000,8881],{},[253,33002,417],{}," does nothing for Hugo's asset cost. Cache ",[253,33005,3253],{}," and the module cache specifically.",[42,33008,33009,33012,33013,33015,33016,33018],{},[229,33010,33011],{},"Keys that never invalidate."," A fixed-string key serves stale processed assets forever. Always hash ",[253,33014,32893],{}," and config into the ",[253,33017,3253],{}," key.",[42,33020,33021,33024,33025,33027],{},[229,33022,33023],{},"Forgetting theme submodules."," If your theme is a git submodule, ",[253,33026,30107],{}," on checkout is required or the build fails before caching matters.",[42,33029,33030,33033,33034,33036],{},[229,33031,33032],{},"Cache eviction."," A repo's caches share a 10 GB budget; a bloated ",[253,33035,3253],{}," from huge originals can evict your module cache. Keep source images reasonable.",[42,33038,33039,33041,33042,33044],{},[229,33040,637],{}," delete the two ",[253,33043,29412],{}," steps and the build runs cold every time — correct, just slower. There is no persisted state to clean up beyond letting the old caches expire.",[34,33046,642],{"id":641},[14,33048,33049,33050,33052,33053,33055,33056,33058,33059,33061,33062,239],{},"Hugo's speed reputation is about rendering, not asset processing — and asset processing is what a cold CI runner redoes every time. Cache ",[253,33051,3253],{}," keyed on a hash of your assets and config, cache the Go module cache keyed on ",[253,33054,31011],{},", and a routine content edit drops from a 95 s cold build to a 25 s warm one. Because Hugo content-hashes everything in ",[253,33057,3253],{},", the restored cache is always correct. For the cross-generator picture, see ",[23,33060,32455],{"href":32454},"; for the surrounding pipeline, ",[23,33063,28200],{"href":28199},[34,33065,651],{"id":650},[653,33067,33069],{"id":33068},"what-does-hugo-store-in-resources_gen","What does Hugo store in resources\u002F_gen?",[14,33071,33072],{},"Processed image variants, compiled Sass and SCSS, and other transformed assets, each named by a hash of its source plus the transform options. Because the filenames are content-hashed, restoring the directory in CI is safe — a stale entry simply never matches a new request, so Hugo regenerates only what actually changed.",[653,33074,33076],{"id":33075},"should-i-cache-the-hugo-binary-itself","Should I cache the Hugo binary itself?",[14,33078,33079,33080,33082],{},"It helps marginally. The bigger wins are ",[253,33081,3253],{}," and the Go module cache. If you install Hugo through a setup action it is already fast, but caching the extended binary download shaves a few seconds on every run for no real downside.",[653,33084,33086],{"id":33085},"why-is-my-hugo-cache-restoring-but-the-build-still-slow","Why is my Hugo cache restoring but the build still slow?",[14,33088,33089,33090,33092,33093,33095],{},"Either the cached path does not match Hugo's actual cache directory, or the cache key changes on every run. Confirm you are caching ",[253,33091,3253],{}," and the module cache, key them on a hash of assets and ",[253,33094,31011],{}," respectively, and check the runner log for a restored rather than created entry.",[653,33097,33099],{"id":33098},"do-i-still-need-gc-or-minify-with-caching","Do I still need --gc or --minify with caching?",[14,33101,33102,33103,270,33105,33107],{},"Yes, those flags are about output cleanup and asset minification, not caching. Keep them in your build command. Caching speeds up the inputs to the build; ",[253,33104,5730],{},[253,33106,5734],{}," shape the output and run regardless.",[653,33109,33111],{"id":33110},"does-this-help-if-my-site-has-no-images-or-sass","Does this help if my site has no images or Sass?",[14,33113,33114,33115,33117],{},"Less so. ",[253,33116,3253],{}," is empty if you do no asset processing, so the main remaining win is the Go module cache for sites using Hugo Modules. A pure Markdown site with a vendored theme builds fast already and gains little from caching.",[34,33119,684],{"id":683},[39,33121,33122,33129,33136,33143],{},[42,33123,33124,692,33126,33128],{},[229,33125,691],{},[23,33127,32455],{"href":32454}," — incremental vs. caching across generators.",[42,33130,33131,33135],{},[23,33132,33134],{"href":33133},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fincremental-builds-and-build-caching-for-ssgs\u002Fenabling-incremental-builds-in-eleventy\u002F","Enabling Incremental Builds in Eleventy"," — the per-run incremental side for Eleventy.",[42,33137,33138,33142],{},[23,33139,33141],{"href":33140},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fincremental-builds-and-build-caching-for-ssgs\u002Fsharing-build-cache-across-ci-runners\u002F","Sharing Build Cache Across CI Runners"," — when a per-repo cache is not enough.",[42,33144,33145,33147],{},[23,33146,28200],{"href":28199}," — the surrounding build-and-deploy pipeline.",[1346,33149,31133],{},{"title":712,"searchDepth":713,"depth":713,"links":33151},[33152,33153,33154,33155,33156,33157,33158,33165],{"id":36,"depth":713,"text":37},{"id":32483,"depth":713,"text":32484},{"id":19483,"depth":713,"text":19484},{"id":1165,"depth":713,"text":1166},{"id":599,"depth":713,"text":600},{"id":641,"depth":713,"text":642},{"id":650,"depth":713,"text":651,"children":33159},[33160,33161,33162,33163,33164],{"id":33068,"depth":730,"text":33069},{"id":33075,"depth":730,"text":33076},{"id":33085,"depth":730,"text":33086},{"id":33098,"depth":730,"text":33099},{"id":33110,"depth":730,"text":33111},{"id":683,"depth":713,"text":684},[33167,33168,33169,33170],{"name":737,"item":738},{"name":5505,"item":5504},{"name":32455,"item":32454},{"name":5002,"item":5001},"Cache Hugo resources\u002F_gen and the module cache in GitHub Actions with content-hashed keys. Includes the workflow, cache key design, and measured CI minutes saved.",[33173,33174,33176,33178,33180],{"q":33069,"a":33072},{"q":33076,"a":33175},"It helps marginally. The bigger wins are resources\u002F_gen and the Go module cache. If you install Hugo through a setup action it is already fast, but caching the extended binary download shaves a few seconds on every run for no real downside.",{"q":33086,"a":33177},"Either the cached path does not match Hugo's actual cache directory, or the cache key changes on every run. Confirm you are caching resources\u002F_gen and the module cache, key them on a hash of assets and go.sum respectively, and check the runner log for a restored rather than created entry.",{"q":33099,"a":33179},"Yes, those flags are about output cleanup and asset minification, not caching. Keep them in your build command. Caching speeds up the inputs to the build; --gc and --minify shape the output and run regardless.",{"q":33111,"a":33181},"Less so. resources\u002F_gen is empty if you do no asset processing, so the main remaining win is the Go module cache for sites using Hugo Modules. A pure Markdown site with a vendored theme builds fast already and gains little from caching.",{},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fincremental-builds-and-build-caching-for-ssgs\u002Fcaching-hugo-builds-in-github-actions",{"title":5002,"description":33171},"production-ready-deployment-cicd-workflows\u002Fincremental-builds-and-build-caching-for-ssgs\u002Fcaching-hugo-builds-in-github-actions\u002Findex","6UkbaNKWba2giVAoUZTnO0_oOZbKnBdyB32yA4K5EpE",{"id":33188,"title":33134,"body":33189,"breadcrumb":33821,"dateModified":743,"datePublished":743,"description":33826,"extension":745,"faq":33827,"meta":33837,"navigation":752,"path":33838,"seo":33839,"slug":33193,"stem":33840,"type":756,"__hash__":33841},"content\u002Fproduction-ready-deployment-cicd-workflows\u002Fincremental-builds-and-build-caching-for-ssgs\u002Fenabling-incremental-builds-in-eleventy\u002Findex.md",{"type":7,"value":33190,"toc":33803},[33191,33194,33202,33204,33218,33222,33228,33320,33322,33327,33383,33396,33399,33444,33448,33454,33539,33553,33555,33561,33637,33647,33656,33658,33695,33697,33715,33717,33721,33734,33738,33744,33748,33751,33755,33761,33765,33775,33777,33800],[10,33192,33134],{"id":33193},"enabling-incremental-builds-in-eleventy",[14,33195,33196,33197,33199,33200,239],{},"When an Eleventy site grows past a few hundred pages, the edit-save-refresh loop stops feeling instant. A full rebuild that takes four or five seconds is a tax you pay on every keystroke-driven save. Eleventy's ",[253,33198,981],{}," flag fixes this by rebuilding only the templates affected by the file you just changed, so editing one Markdown post re-renders one page instead of the whole site. This guide covers the flags, the programmatic API, exactly what gets rebuilt, and the measured rebuild times. It is the Eleventy-specific piece of ",[23,33201,32455],{"href":32454},[34,33203,37],{"id":36},[39,33205,33206,33212,33215],{},[42,33207,33208,33209,239],{},"Eleventy 2.0 or newer (the incremental engine improved substantially over 1.x). Check with ",[253,33210,33211],{},"npx @11ty\u002Feleventy --version",[42,33213,33214],{},"A site large enough that a full build is noticeably slow — incremental builds pay off above roughly 200 pages or when you do per-page image work.",[42,33216,33217],{},"Familiarity with running Eleventy in watch or serve mode, since that is where incremental builds do their work.",[34,33219,33221],{"id":33220},"what-incremental-means-in-eleventy","What \"Incremental\" Means in Eleventy",[14,33223,33224,33225,33227],{},"Eleventy tracks a dependency graph between your templates, layouts, includes, and data files. When you save a file in watch mode with ",[253,33226,981],{},", it walks that graph to find every template that depends on the changed file and rebuilds only those. A leaf Markdown file with no dependents rebuilds exactly one output page; a shared layout rebuilds every page that extends it — which is correct, not a bug.",[55,33229,33230,33317],{},[58,33231,66,33235,66,33238,66,33241,66,33310],{"viewBox":7822,"role":61,"ariaLabelledBy":33232,"xmlns":65},[33233,33234],"elev-graph-title","elev-graph-desc",[68,33236,33237],{"id":33233},"Eleventy incremental rebuild dependency graph",[72,33239,33240],{"id":33234},"A dependency graph showing that editing a single post rebuilds only that post, while editing a shared layout rebuilds every page that inherits it. Affected nodes are highlighted in green, unaffected nodes are muted.",[95,33242,78,33243,78,33246,78,33251,78,33254,78,33257,78,33259,78,33262,78,33264,78,33268,78,33274,78,33277,78,33280,78,33282,78,33285,78,33287,78,33291,78,33293,78,33296,78,33304,78,33307,66],{"style":97},[99,33244,33245],{"x":816,"y":109,"fill":103,"style":104},"What rebuilds when you change one file",[99,33247,33250],{"x":194,"y":33248,"fill":187,"style":33249},"66","font-size:13px;font-weight:700;text-anchor:middle","Edit a single post",[107,33252],{"x":4682,"y":1430,"width":119,"height":33253,"rx":3579,"fill":185,"opacity":163,"stroke":187,"style":116},"46",[99,33255,33256],{"x":194,"y":14923,"fill":103,"style":829},"post-a.md ✎",[107,33258],{"x":4682,"y":7852,"width":119,"height":33253,"rx":3579,"fill":185,"opacity":163,"stroke":187,"style":116},[99,33260,33261],{"x":194,"y":8703,"fill":103,"style":829},"post-a\u002Findex.html",[107,33263],{"x":4682,"y":5407,"width":119,"height":3578,"rx":3579,"fill":2592,"opacity":4631,"stroke":93,"style":878},[99,33265,33267],{"x":194,"y":33266,"fill":93,"style":126},"257","other 499 pages",[95,33269,88,33270,78],{"stroke":187,"fill":205,"style":116},[90,33271],{"d":33272,"style":33273},"M170 126 L170 158","marker-end:url(#elev-arrow)",[99,33275,33276],{"x":194,"y":28535,"fill":93,"style":126},"untouched · 1 page rebuilt",[99,33278,33279],{"x":10820,"y":33248,"fill":2565,"style":33249},"Edit the base layout",[107,33281],{"x":4699,"y":1430,"width":119,"height":33253,"rx":3579,"fill":2564,"opacity":850,"stroke":2565,"style":116},[99,33283,33284],{"x":10820,"y":14923,"fill":103,"style":829},"base.njk ✎",[107,33286],{"x":5320,"y":7852,"width":1431,"height":1464,"rx":3579,"fill":2564,"opacity":825,"stroke":2565,"style":116},[99,33288,33290],{"x":4699,"y":33289,"fill":103,"style":126},"187","all posts",[107,33292],{"x":3531,"y":7852,"width":1431,"height":1464,"rx":3579,"fill":2564,"opacity":825,"stroke":2565,"style":116},[99,33294,33295],{"x":19446,"y":33289,"fill":103,"style":126},"all pages",[95,33297,88,33298,88,33301,78],{"stroke":2565,"fill":205,"style":116},[90,33299],{"d":33300,"style":33273},"M540 126 L500 158",[90,33302],{"d":33303,"style":33273},"M580 126 L620 158",[99,33305,33306],{"x":10820,"y":11924,"fill":93,"style":126},"shared dependency ·",[99,33308,33309],{"x":10820,"y":854,"fill":93,"style":126},"all 500 pages rebuilt",[76,33311,78,33312,66],{},[80,33313,88,33315,78],{"id":33314,"viewBox":83,"refX":84,"refY":85,"markerWidth":86,"markerHeight":86,"orient":87},"elev-arrow",[90,33316],{"d":92,"fill":93},[218,33318,33319],{},"Eleventy rebuilds along its dependency graph: a leaf post rebuilds one page, but a shared layout is a dependency of every page that inherits it, so all of them rebuild.",[34,33321,19484],{"id":19483},[14,33323,33324,33325,931],{},"Run Eleventy in serve mode with the incremental flag. In ",[253,33326,21912],{},[987,33328,33330],{"className":14263,"code":33329,"language":14265,"meta":712,"style":712},"{\n  \"scripts\": {\n    \"dev\": \"eleventy --serve --incremental\",\n    \"dev:fast\": \"eleventy --serve --incremental --ignore-initial\",\n    \"build\": \"eleventy\"\n  }\n}\n",[253,33331,33332,33336,33342,33354,33366,33375,33379],{"__ignoreMap":712},[995,33333,33334],{"class":997,"line":998},[995,33335,14272],{"class":1618},[995,33337,33338,33340],{"class":997,"line":713},[995,33339,27159],{"class":1010},[995,33341,27162],{"class":1618},[995,33343,33344,33347,33349,33352],{"class":997,"line":730},[995,33345,33346],{"class":1010},"    \"dev\"",[995,33348,1925],{"class":1618},[995,33350,33351],{"class":1023},"\"eleventy --serve --incremental\"",[995,33353,2885],{"class":1618},[995,33355,33356,33359,33361,33364],{"class":997,"line":1544},[995,33357,33358],{"class":1010},"    \"dev:fast\"",[995,33360,1925],{"class":1618},[995,33362,33363],{"class":1023},"\"eleventy --serve --incremental --ignore-initial\"",[995,33365,2885],{"class":1618},[995,33367,33368,33370,33372],{"class":997,"line":1550},[995,33369,27167],{"class":1010},[995,33371,1925],{"class":1618},[995,33373,33374],{"class":1023},"\"eleventy\"\n",[995,33376,33377],{"class":997,"line":1673},[995,33378,27177],{"class":1618},[995,33380,33381],{"class":997,"line":1678},[995,33382,9008],{"class":1618},[14,33384,33385,33387,33388,33391,33392,33395],{},[253,33386,981],{}," enables per-change rebuilds. ",[253,33389,33390],{},"--ignore-initial"," goes one step further: it skips writing any files on the ",[229,33393,33394],{},"first"," pass when the server starts, so startup is near-instant on a large site, and you only pay to render the page you actually edit. This is the combination to reach for when you are working on a single template inside a 1,000-page site and do not want to wait for a full build just to open the dev server.",[14,33397,33398],{},"For the standalone CLI:",[987,33400,33402],{"className":989,"code":33401,"language":991,"meta":712,"style":712},"# Watch and serve, rebuilding only what changes\nnpx @11ty\u002Feleventy --serve --incremental\n\n# Skip the initial full write, then build on change only\nnpx @11ty\u002Feleventy --serve --incremental --ignore-initial\n",[253,33403,33404,33409,33421,33425,33430],{"__ignoreMap":712},[995,33405,33406],{"class":997,"line":998},[995,33407,33408],{"class":1001},"# Watch and serve, rebuilding only what changes\n",[995,33410,33411,33413,33415,33418],{"class":997,"line":713},[995,33412,1079],{"class":1007},[995,33414,1558],{"class":1023},[995,33416,33417],{"class":1010}," --serve",[995,33419,33420],{"class":1010}," --incremental\n",[995,33422,33423],{"class":997,"line":730},[995,33424,1541],{"emptyLinePlaceholder":752},[995,33426,33427],{"class":997,"line":1544},[995,33428,33429],{"class":1001},"# Skip the initial full write, then build on change only\n",[995,33431,33432,33434,33436,33438,33441],{"class":997,"line":1550},[995,33433,1079],{"class":1007},[995,33435,1558],{"class":1023},[995,33437,33417],{"class":1010},[995,33439,33440],{"class":1010}," --incremental",[995,33442,33443],{"class":1010}," --ignore-initial\n",[653,33445,33447],{"id":33446},"programmatic-api","Programmatic API",[14,33449,33450,33451,931],{},"If you embed Eleventy in a larger Node process, the programmatic API runs the same incremental engine. Construct an instance and call ",[253,33452,33453],{},"watch()",[987,33455,33457],{"className":1600,"code":33456,"language":1602,"meta":712,"style":712},"import Eleventy from '@11ty\u002Feleventy';\n\nconst eleventy = new Eleventy('.\u002Fsrc', '.\u002Fdist', {\n  configPath: '.\u002Feleventy.config.js',\n});\n\n\u002F\u002F Runs the incremental watch engine — per-change rebuilds, same as the CLI\nawait eleventy.watch();\n",[253,33458,33459,33473,33477,33503,33513,33517,33521,33526],{"__ignoreMap":712},[995,33460,33461,33463,33466,33468,33471],{"class":997,"line":998},[995,33462,1615],{"class":1614},[995,33464,33465],{"class":1618}," Eleventy ",[995,33467,1622],{"class":1614},[995,33469,33470],{"class":1023}," '@11ty\u002Feleventy'",[995,33472,1628],{"class":1618},[995,33474,33475],{"class":997,"line":713},[995,33476,1541],{"emptyLinePlaceholder":752},[995,33478,33479,33481,33484,33486,33488,33491,33493,33496,33498,33501],{"class":997,"line":730},[995,33480,6228],{"class":1614},[995,33482,33483],{"class":1010}," eleventy",[995,33485,1775],{"class":1614},[995,33487,12078],{"class":1614},[995,33489,33490],{"class":1007}," Eleventy",[995,33492,1799],{"class":1618},[995,33494,33495],{"class":1023},"'.\u002Fsrc'",[995,33497,1850],{"class":1618},[995,33499,33500],{"class":1023},"'.\u002Fdist'",[995,33502,21740],{"class":1618},[995,33504,33505,33508,33511],{"class":997,"line":1544},[995,33506,33507],{"class":1618},"  configPath: ",[995,33509,33510],{"class":1023},"'.\u002Feleventy.config.js'",[995,33512,2885],{"class":1618},[995,33514,33515],{"class":997,"line":1550},[995,33516,1735],{"class":1618},[995,33518,33519],{"class":997,"line":1673},[995,33520,1541],{"emptyLinePlaceholder":752},[995,33522,33523],{"class":997,"line":1678},[995,33524,33525],{"class":1001},"\u002F\u002F Runs the incremental watch engine — per-change rebuilds, same as the CLI\n",[995,33527,33528,33531,33534,33537],{"class":997,"line":1693},[995,33529,33530],{"class":1614},"await",[995,33532,33533],{"class":1618}," eleventy.",[995,33535,33536],{"class":1007},"watch",[995,33538,23573],{"class":1618},[14,33540,33541,33542,270,33545,33548,33549,33552],{},"You can also set ",[253,33543,33544],{},"incremental",[253,33546,33547],{},"ignoreInitial"," in the instance options or configuration so a custom build script behaves like ",[253,33550,33551],{},"--serve --incremental --ignore-initial"," without shelling out to the CLI.",[34,33554,1166],{"id":1165},[14,33556,33557,33558,33560],{},"Benchmarked on a 500-page Eleventy 3.x site (Markdown posts, a shared Nunjucks layout, Node 22, Apple M2) using ",[253,33559,595],{}," for the full build and the dev-server console timings for incremental rebuilds:",[433,33562,33563,33576],{},[436,33564,33565],{},[439,33566,33567,33570,33573],{},[442,33568,33569],{},"Operation",[442,33571,33572],{},"Time",[442,33574,33575],{},"Pages rebuilt",[457,33577,33578,33591,33601,33612,33623],{},[439,33579,33580,33586,33589],{},[462,33581,33582,33585],{},[253,33583,33584],{},"eleventy"," full build (cold)",[462,33587,33588],{},"4.82 s",[462,33590,1447],{},[439,33592,33593,33596,33599],{},[462,33594,33595],{},"Incremental rebuild — edit one post",[462,33597,33598],{},"0.11 s",[462,33600,11536],{},[439,33602,33603,33606,33609],{},[462,33604,33605],{},"Incremental rebuild — edit a shared partial",[462,33607,33608],{},"0.34 s",[462,33610,33611],{},"~40 (pages using it)",[439,33613,33614,33617,33620],{},[462,33615,33616],{},"Incremental rebuild — edit base layout",[462,33618,33619],{},"4.6 s",[462,33621,33622],{},"500 (all)",[439,33624,33625,33631,33634],{},[462,33626,33627,33630],{},[253,33628,33629],{},"--serve --ignore-initial"," startup",[462,33632,33633],{},"0.2 s",[462,33635,33636],{},"0 (no initial write)",[14,33638,33639,33640,33642,33643,33646],{},"The headline is the first incremental row: editing a single post went from a 4.82 s full build to ",[229,33641,33598],{},", a roughly 40x improvement on the edit loop. The layout row is the reminder that incremental builds skip ",[18,33644,33645],{},"unaffected"," pages — when the file you change is a genuine dependency of everything, everything rebuilds, and that is correct behavior.",[14,33648,33649,33650,33652,33653,33655],{},"The full cold build time is the same number you would cache against in CI; for that side of the story see ",[23,33651,1049],{"href":1048}," and the broader ",[23,33654,28200],{"href":28199}," pipeline.",[34,33657,600],{"id":599},[39,33659,33660,33666,33675,33685],{},[42,33661,33662,33665],{},[229,33663,33664],{},"Expecting CI speedups."," A clean CI checkout has no prior build state, so the first build is always full. Incremental builds help local editing, not cold runners — cache dependencies for CI instead.",[42,33667,33668,33671,33672,33674],{},[229,33669,33670],{},"Stale incremental state after a crash."," If a watch session dies mid-build, on-disk state can desync and produce wrong output. Stop the server, delete the output directory, and run one clean ",[253,33673,33584],{}," build to reset.",[42,33676,33677,33680,33681,33684],{},[229,33678,33679],{},"Surprise at full rebuilds."," Editing data files in the global ",[253,33682,33683],{},"_data"," directory or a base layout legitimately invalidates many pages. That is the dependency graph working, not incremental builds failing.",[42,33686,33687,33689,33690,270,33692,33694],{},[229,33688,637],{}," the flags are purely additive. Remove ",[253,33691,981],{},[253,33693,33390],{}," from your dev script and you are back to a plain full build on every change — no state to migrate.",[34,33696,642],{"id":641},[14,33698,33699,33700,33702,33703,33706,33707,33709,33710,33712,33713,239],{},"Incremental builds turn Eleventy's edit loop from \"wait for the whole site\" into \"render the one page I touched.\" Add ",[253,33701,981],{}," to your ",[253,33704,33705],{},"--serve"," script, layer in ",[253,33708,33390],{}," on large sites for instant startup, and use the programmatic ",[253,33711,33453],{}," if you embed Eleventy elsewhere. Just keep the model straight: it skips unaffected pages, so a leaf edit is near-instant while a shared-layout edit correctly rebuilds everything. For deploy-time speed, the lever is caching, not incremental flags — covered across ",[23,33714,32455],{"href":32454},[34,33716,651],{"id":650},[653,33718,33720],{"id":33719},"does-incremental-work-without-watch-or-serve","Does --incremental work without --watch or --serve?",[14,33722,33723,33724,2204,33727,33729,33730,33733],{},"It is designed to be paired with ",[253,33725,33726],{},"--watch",[253,33728,33705],{},", where Eleventy already knows which file changed. On a one-shot ",[253,33731,33732],{},"eleventy --incremental"," run with no prior cache there is nothing to diff against, so it behaves like a full build. The speedups come on the second and later rebuilds in a running watch session.",[653,33735,33737],{"id":33736},"what-does-ignore-initial-actually-do","What does --ignore-initial actually do?",[14,33739,33740,33741,33743],{},"It tells Eleventy to skip writing files on the very first pass when it starts in watch or serve mode, then build only what changes afterward. Combined with ",[253,33742,981],{}," it means startup is near-instant and you only pay for rendering the page you edit, which is ideal for a large site where you are touching one template.",[653,33745,33747],{"id":33746},"why-did-editing-my-layout-rebuild-every-page","Why did editing my layout rebuild every page?",[14,33749,33750],{},"Layouts and includes are dependencies of many pages, so Eleventy correctly rebuilds every page that uses them. Incremental builds skip unaffected pages, not pages that genuinely depend on the file you changed. Editing a single leaf Markdown file rebuilds one page; editing a base layout rebuilds everything that inherits it.",[653,33752,33754],{"id":33753},"can-i-use-incremental-builds-in-ci","Can I use incremental builds in CI?",[14,33756,33757,33758,33760],{},"Rarely usefully. A CI runner starts from a clean checkout with no previous build state, so the first build is always full. Incremental builds help local editing and long-lived dev servers. For CI speed, cache ",[253,33759,417],{}," and any generated caches between runs instead.",[653,33762,33764],{"id":33763},"does-the-programmatic-api-support-incremental-builds","Does the programmatic API support incremental builds?",[14,33766,33767,33768,33771,33772,33774],{},"Yes. Construct an Eleventy instance and call ",[253,33769,33770],{},"eleventy.watch()",", which runs the same incremental engine as the CLI watch mode. Passing ",[253,33773,33544],{}," through the configuration or instance options lets you embed Eleventy in a larger Node process and still get per-change rebuilds.",[34,33776,684],{"id":683},[39,33778,33779,33785,33790,33795],{},[42,33780,33781,692,33783,33128],{},[229,33782,691],{},[23,33784,32455],{"href":32454},[42,33786,33787,33789],{},[23,33788,5002],{"href":5001}," — the caching side for Hugo.",[42,33791,33792,33794],{},[23,33793,33141],{"href":33140}," — remote caches for parallel jobs.",[42,33796,33797,33799],{},[23,33798,28200],{"href":28199}," — where cold full builds happen and what to cache.",[1346,33801,33802],{},"html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}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 .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}",{"title":712,"searchDepth":713,"depth":713,"links":33804},[33805,33806,33807,33810,33811,33812,33813,33820],{"id":36,"depth":713,"text":37},{"id":33220,"depth":713,"text":33221},{"id":19483,"depth":713,"text":19484,"children":33808},[33809],{"id":33446,"depth":730,"text":33447},{"id":1165,"depth":713,"text":1166},{"id":599,"depth":713,"text":600},{"id":641,"depth":713,"text":642},{"id":650,"depth":713,"text":651,"children":33814},[33815,33816,33817,33818,33819],{"id":33719,"depth":730,"text":33720},{"id":33736,"depth":730,"text":33737},{"id":33746,"depth":730,"text":33747},{"id":33753,"depth":730,"text":33754},{"id":33763,"depth":730,"text":33764},{"id":683,"depth":713,"text":684},[33822,33823,33824,33825],{"name":737,"item":738},{"name":5505,"item":5504},{"name":32455,"item":32454},{"name":33134,"item":33133},"Use Eleventy --incremental and --ignore-initial to rebuild only changed templates. Covers the programmatic API, what gets rebuilt, and measured rebuild times.",[33828,33830,33832,33833,33835],{"q":33720,"a":33829},"It is designed to be paired with --watch or --serve, where Eleventy already knows which file changed. On a one-shot eleventy --incremental run with no prior cache there is nothing to diff against, so it behaves like a full build. The speedups come on the second and later rebuilds in a running watch session.",{"q":33737,"a":33831},"It tells Eleventy to skip writing files on the very first pass when it starts in watch or serve mode, then build only what changes afterward. Combined with --incremental it means startup is near-instant and you only pay for rendering the page you edit, which is ideal for a large site where you are touching one template.",{"q":33747,"a":33750},{"q":33754,"a":33834},"Rarely usefully. A CI runner starts from a clean checkout with no previous build state, so the first build is always full. Incremental builds help local editing and long-lived dev servers. For CI speed, cache node_modules and any generated caches between runs instead.",{"q":33764,"a":33836},"Yes. Construct an Eleventy instance and call eleventy.watch(), which runs the same incremental engine as the CLI watch mode. Passing incremental through the configuration or instance options lets you embed Eleventy in a larger Node process and still get per-change rebuilds.",{},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fincremental-builds-and-build-caching-for-ssgs\u002Fenabling-incremental-builds-in-eleventy",{"title":33134,"description":33826},"production-ready-deployment-cicd-workflows\u002Fincremental-builds-and-build-caching-for-ssgs\u002Fenabling-incremental-builds-in-eleventy\u002Findex","WBlCdZS_IW4U2krne3GETWKv0noetGxcOIhgs-vwa_Q",{"id":33843,"title":32455,"body":33844,"breadcrumb":34601,"dateModified":743,"datePublished":743,"description":34605,"extension":745,"faq":34606,"meta":34618,"navigation":752,"path":34619,"seo":34620,"slug":33848,"stem":34621,"type":2460,"__hash__":34622},"content\u002Fproduction-ready-deployment-cicd-workflows\u002Fincremental-builds-and-build-caching-for-ssgs\u002Findex.md",{"type":7,"value":33845,"toc":34585},[33846,33849,33861,33871,33987,33991,33994,34052,34146,34150,34160,34163,34214,34230,34291,34300,34304,34311,34317,34370,34379,34383,34390,34402,34408,34410,34456,34458,34490,34492,34496,34502,34506,34519,34523,34533,34537,34540,34544,34547,34549,34582],[10,33847,32455],{"id":33848},"incremental-builds-and-build-caching-for-ssgs",[14,33850,33851,33852,33854,33855,33858,33859,239],{},"A static site generator's whole value proposition is that the expensive work happens once, at build time. That promise breaks down when \"once\" becomes \"every push, from scratch, for ten minutes.\" As content grows past a few hundred pages — or you add image processing, syntax highlighting, and search-index generation — the build becomes the slowest part of your deploy pipeline. The fix is two related techniques: ",[229,33853,7241],{},", which rebuild only what changed within a run, and ",[229,33856,33857],{},"build caching",", which carries artifacts from one run to the next so a clean CI machine never redoes settled work. This guide covers both across the major generators, as part of ",[23,33860,5505],{"href":5504},[14,33862,33863,33864,33867,33868,33870],{},"The two ideas are easy to confuse. Incremental builds are about a single invocation: given a change to one Markdown file, only re-render the pages that depend on it. Build caching is about ",[18,33865,33866],{},"between"," invocations: a fresh GitHub Actions runner starts with an empty disk, so restoring ",[253,33869,417],{},", a processed-image cache, or Hugo's resource cache from the previous run is what saves the time. In CI you almost always want caching; incremental flags help most in local watch mode and on hosts that persist a working directory.",[55,33872,33873,33984],{},[58,33874,66,33879,66,33882,66,33885,66,33977],{"viewBox":33875,"role":61,"ariaLabelledBy":33876,"xmlns":65},"0 0 780 340",[33877,33878],"incr-flow-title","incr-flow-desc",[68,33880,33881],{"id":33877},"Full build versus cached incremental build",[72,33883,33884],{"id":33878},"Two pipelines compared. The top path is a cold full build that installs dependencies, processes all assets, and renders every page in 240 seconds. The bottom path restores a cache, processes only changed assets, and renders only affected pages in 28 seconds.",[95,33886,78,33887,78,33890,78,33893,78,33895,78,33897,78,33900,78,33902,78,33904,78,33906,78,33908,78,33911,78,33914,78,33916,78,33919,78,33922,78,33925,78,33927,78,33931,78,33933,78,33935,78,33938,78,33941,78,33943,78,33946,78,33948,78,33950,78,33953,78,33956,66],{"style":813},[99,33888,33889],{"x":167,"y":109,"fill":103,"style":104},"Cold full build vs. warm cached build",[99,33891,33892],{"x":3578,"y":828,"fill":2565,"style":20821},"Full build · cold runner",[107,33894],{"x":3578,"y":873,"width":119,"height":3559,"rx":3579,"fill":2564,"opacity":825,"stroke":2565,"style":116},[99,33896,2072],{"x":159,"y":24711,"fill":103,"style":829},[99,33898,33899],{"x":159,"y":130,"fill":93,"style":126},"~70s",[107,33901],{"x":3500,"y":873,"width":119,"height":3559,"rx":3579,"fill":2564,"opacity":825,"stroke":2565,"style":116},[99,33903,32576],{"x":820,"y":24711,"fill":103,"style":829},[99,33905,30940],{"x":820,"y":130,"fill":93,"style":126},[107,33907],{"x":816,"y":873,"width":119,"height":3559,"rx":3579,"fill":2564,"opacity":825,"stroke":2565,"style":116},[99,33909,33910],{"x":863,"y":24711,"fill":103,"style":829},"render all pages",[99,33912,33913],{"x":863,"y":130,"fill":93,"style":126},"~75s",[107,33915],{"x":10820,"y":873,"width":160,"height":3559,"rx":3579,"fill":2565,"opacity":886,"stroke":2565,"style":116},[99,33917,33918],{"x":190,"y":24711,"fill":2565,"style":121},"240s total",[99,33920,33921],{"x":190,"y":130,"fill":93,"style":126},"every push",[99,33923,33924],{"x":3578,"y":17845,"fill":187,"style":20821},"Warm build · restored cache",[107,33926],{"x":3578,"y":4634,"width":119,"height":3559,"rx":3579,"fill":185,"opacity":850,"stroke":187,"style":116},[99,33928,33930],{"x":159,"y":33929,"fill":103,"style":829},"255","restore cache",[99,33932,3497],{"x":159,"y":28531,"fill":93,"style":126},[107,33934],{"x":3500,"y":4634,"width":119,"height":3559,"rx":3579,"fill":185,"opacity":850,"stroke":187,"style":116},[99,33936,33937],{"x":820,"y":33929,"fill":103,"style":829},"changed assets only",[99,33939,33940],{"x":820,"y":28531,"fill":93,"style":126},"~8s",[107,33942],{"x":816,"y":4634,"width":119,"height":3559,"rx":3579,"fill":185,"opacity":850,"stroke":187,"style":116},[99,33944,33945],{"x":863,"y":33929,"fill":103,"style":829},"affected pages",[99,33947,3513],{"x":863,"y":28531,"fill":93,"style":126},[107,33949],{"x":10820,"y":4634,"width":160,"height":3559,"rx":3579,"fill":187,"opacity":877,"stroke":187,"style":116},[99,33951,33952],{"x":190,"y":33929,"fill":187,"style":121},"28s total",[99,33954,33955],{"x":190,"y":28531,"fill":93,"style":126},"typical push",[95,33957,88,33958,88,33962,88,33965,88,33968,88,33971,88,33974,78],{"stroke":93,"fill":205,"style":116},[90,33959],{"d":33960,"style":33961},"M180 119 L208 119","marker-end:url(#incr-arrow)",[90,33963],{"d":33964,"style":33961},"M350 119 L378 119",[90,33966],{"d":33967,"style":33961},"M520 119 L558 119",[90,33969],{"d":33970,"style":33961},"M180 259 L208 259",[90,33972],{"d":33973,"style":33961},"M350 259 L378 259",[90,33975],{"d":33976,"style":33961},"M520 259 L558 259",[76,33978,78,33979,66],{},[80,33980,88,33982,78],{"id":33981,"viewBox":83,"refX":84,"refY":85,"markerWidth":86,"markerHeight":86,"orient":87},"incr-arrow",[90,33983],{"d":92,"fill":93},[218,33985,33986],{},"A cold runner redoes install, asset processing, and rendering on every push; restoring caches and doing only the changed work collapses a 240s build to roughly 28s.",[34,33988,33990],{"id":33989},"what-each-generator-supports","What Each Generator Supports",[14,33992,33993],{},"The generators differ sharply in how much they reuse. Knowing exactly which directory each one writes to is the difference between a cache that helps and one that silently does nothing.",[39,33995,33996,34007,34021,34032,34040],{},[42,33997,33998,34000,34001,34003,34004,34006],{},[229,33999,273],{}," has a first-class ",[253,34002,981],{}," flag that rebuilds only the templates affected by the file you just changed. It is the strongest single-run incremental story of the bunch — see ",[23,34005,33134],{"href":33133}," for the watch-mode and programmatic details.",[42,34008,34009,34011,34012,34014,34015,34017,34018,34020],{},[229,34010,265],{}," rebuilds only changed pages in ",[253,34013,3269],{}," and watch mode, and — crucially for CI — caches processed images, Sass, and other resources under ",[253,34016,3253],{},", plus downloaded modules in its module cache. Restoring those between runs is the whole game; ",[23,34019,5002],{"href":5001}," walks through the cache keys.",[42,34022,34023,34025,34026,34028,34029,34031],{},[229,34024,269],{}," rebuilds the full site on each ",[253,34027,2986],{},", but persists processed assets under ",[253,34030,3170],{},", so restoring that directory skips re-encoding images on a fresh runner.",[42,34033,34034,34036,34037,34039],{},[229,34035,5553],{}," keeps a ",[253,34038,9444],{}," that the build reuses for compiled modules and (when applicable) data; the Next docs explicitly recommend caching that directory in CI.",[42,34041,34042,34045,34046,270,34049,34051],{},[229,34043,34044],{},"Gatsby"," maintains a ",[253,34047,34048],{},".cache",[253,34050,14988],{}," pair that powers its incremental builds; restoring both lets it skip unchanged queries and pages.",[433,34053,34054,34066],{},[436,34055,34056],{},[439,34057,34058,34060,34063],{},[442,34059,3136],{},[442,34061,34062],{},"Single-run incremental",[442,34064,34065],{},"What to cache between CI runs",[457,34067,34068,34085,34102,34115,34131],{},[439,34069,34070,34072,34077],{},[462,34071,273],{},[462,34073,34074,34076],{},[253,34075,981],{}," flag",[462,34078,34079,34081,34082,34084],{},[253,34080,417],{},", any ",[253,34083,34048],{}," you configure",[439,34086,34087,34089,34092],{},[462,34088,265],{},[462,34090,34091],{},"changed pages in watch mode",[462,34093,34094,34096,34097,2204,34100,982],{},[253,34095,3253],{},", module cache (",[253,34098,34099],{},"$GOPATH",[253,34101,4575],{},[439,34103,34104,34106,34109],{},[462,34105,269],{},[462,34107,34108],{},"full rebuild per run",[462,34110,34111,1850,34113],{},[253,34112,3170],{},[253,34114,417],{},[439,34116,34117,34120,34125],{},[462,34118,34119],{},"Next.js export",[462,34121,34122,34123],{},"partial via ",[253,34124,9444],{},[462,34126,34127,1850,34129],{},[253,34128,9444],{},[253,34130,417],{},[439,34132,34133,34135,34138],{},[462,34134,34044],{},[462,34136,34137],{},"yes, via cache",[462,34139,34140,1850,34142,1850,34144],{},[253,34141,34048],{},[253,34143,14988],{},[253,34145,417],{},[34,34147,34149],{"id":34148},"designing-cache-keys-that-actually-hit","Designing Cache Keys That Actually Hit",[14,34151,34152,34153,34155,34156,34159],{},"A cache is only useful if it restores on the runs you want and rebuilds on the ones you need. The discipline is identical to the asset caching in ",[23,34154,2190],{"href":2189},": key the cache on a hash of the ",[18,34157,34158],{},"inputs"," that determine its contents.",[14,34161,34162],{},"For dependencies, key on the lockfile so the cache is reused until you change a package:",[987,34164,34166],{"className":1912,"code":34165,"language":1914,"meta":712,"style":712},"- uses: actions\u002Fcache@v4\n  with:\n    path: ~\u002F.npm\n    key: npm-${{ runner.os }}-${{ hashFiles('package-lock.json') }}\n    restore-keys: |\n      npm-${{ runner.os }}-\n",[253,34167,34168,34178,34184,34192,34201,34209],{"__ignoreMap":712},[995,34169,34170,34172,34174,34176],{"class":997,"line":998},[995,34171,3191],{"class":1618},[995,34173,1978],{"class":1921},[995,34175,1925],{"class":1618},[995,34177,3198],{"class":1023},[995,34179,34180,34182],{"class":997,"line":713},[995,34181,3203],{"class":1921},[995,34183,1946],{"class":1618},[995,34185,34186,34188,34190],{"class":997,"line":730},[995,34187,3210],{"class":1921},[995,34189,1925],{"class":1618},[995,34191,29498],{"class":1023},[995,34193,34194,34196,34198],{"class":997,"line":1544},[995,34195,3235],{"class":1921},[995,34197,1925],{"class":1618},[995,34199,34200],{"class":1023},"npm-${{ runner.os }}-${{ hashFiles('package-lock.json') }}\n",[995,34202,34203,34205,34207],{"class":997,"line":1550},[995,34204,29512],{"class":1921},[995,34206,1925],{"class":1618},[995,34208,3215],{"class":1614},[995,34210,34211],{"class":997,"line":1673},[995,34212,34213],{"class":1023},"      npm-${{ runner.os }}-\n",[14,34215,8896,34216,34218,34219,34222,34223,34225,34226,34229],{},[253,34217,29547],{}," prefix is what keeps a near-miss warm: when the lockfile changes, the exact key misses, but the most recent ",[253,34220,34221],{},"npm-Linux-*"," entry is restored as a starting point, so ",[253,34224,2072],{}," only fetches the deltas. For an asset cache, key on the content that feeds it — ",[253,34227,34228],{},"hashFiles('assets\u002F**')"," for an image cache — so an unchanged media directory always hits.",[433,34231,34232,34245],{},[436,34233,34234],{},[439,34235,34236,34239,34242],{},[442,34237,34238],{},"Cache key strategy",[442,34240,34241],{},"Hit behavior",[442,34243,34244],{},"Risk",[457,34246,34247,34258,34269,34280],{},[439,34248,34249,34252,34255],{},[462,34250,34251],{},"Hash of lockfile",[462,34253,34254],{},"Hits until deps change",[462,34256,34257],{},"None — exactly right for deps",[439,34259,34260,34263,34266],{},[462,34261,34262],{},"Hash of content dir",[462,34264,34265],{},"Hits until content changes",[462,34267,34268],{},"Over-broad if one edit busts the whole cache",[439,34270,34271,34274,34277],{},[462,34272,34273],{},"Commit SHA in key",[462,34275,34276],{},"Never hits",[462,34278,34279],{},"Cache is created and never restored",[439,34281,34282,34285,34288],{},[462,34283,34284],{},"Fixed string (no hash)",[462,34286,34287],{},"Always hits, never refreshes",[462,34289,34290],{},"Stale artifacts silently reused",[14,34292,34293,34294,2204,34297,239],{},"The two failure modes at the bottom of that table are the ones people actually ship. A commit SHA in the key means the key is unique every run, so you write a cache nobody ever reads. A fixed string means you never pick up new dependencies. Both look like the cache \"isn't working\"; the runner log tells you which by reporting whether the entry was ",[18,34295,34296],{},"restored",[18,34298,34299],{},"created",[34,34301,34303],{"id":34302},"content-hash-invalidation","Content-Hash Invalidation",[14,34305,34306,34307,34310],{},"The most robust caches invalidate on content rather than time. Instead of expiring a cache after N hours, derive the key from a hash of the files that produced it, so the cache is correct ",[18,34308,34309],{},"by construction",": if the inputs are byte-for-byte identical, the cached output is reused; if anything changed, the key changes and the work reruns.",[14,34312,34313,34314,34316],{},"This is why fingerprinted asset filenames and content-hashed cache keys reinforce each other. Hugo's ",[253,34315,3253],{}," already names processed files by a hash of their source plus transform options, so caching that directory is safe — a stale entry simply never matches a new request. You can apply the same idea to your own generated artifacts:",[987,34318,34320],{"className":989,"code":34319,"language":991,"meta":712,"style":712},"# Bust a search-index cache only when content or the indexer config changes\nINDEX_KEY=\"searchidx-$(cat content\u002F**\u002F*.md scripts\u002Fbuild-index.mjs | sha256sum | cut -c1-16)\"\n",[253,34321,34322,34327],{"__ignoreMap":712},[995,34323,34324],{"class":997,"line":998},[995,34325,34326],{"class":1001},"# Bust a search-index cache only when content or the indexer config changes\n",[995,34328,34329,34332,34334,34337,34340,34343,34346,34348,34350,34353,34356,34359,34361,34364,34367],{"class":997,"line":713},[995,34330,34331],{"class":1618},"INDEX_KEY",[995,34333,7317],{"class":1614},[995,34335,34336],{"class":1023},"\"searchidx-$(",[995,34338,34339],{"class":1007},"cat",[995,34341,34342],{"class":1023}," content\u002F",[995,34344,34345],{"class":1010},"**",[995,34347,738],{"class":1023},[995,34349,22735],{"class":1010},[995,34351,34352],{"class":1023},".md scripts\u002Fbuild-index.mjs ",[995,34354,34355],{"class":1614},"|",[995,34357,34358],{"class":1007}," sha256sum",[995,34360,14477],{"class":1614},[995,34362,34363],{"class":1007}," cut",[995,34365,34366],{"class":1010}," -c1-16",[995,34368,34369],{"class":1023},")\"\n",[14,34371,34372,34373,34375,34376,34378],{},"Because the key is a function of the exact bytes that go into the index, you never serve a stale index and you never rebuild an unchanged one. The same reasoning underpins safe edge caching — see ",[23,34374,14752],{"href":14049},", where hashed filenames are exactly what make a one-year ",[253,34377,11756],{}," cache safe.",[34,34380,34382],{"id":34381},"remote-and-shared-caches","Remote and Shared Caches",[14,34384,34385,34386,34389],{},"The per-repository cache that GitHub Actions provides is scoped to one repo and, by default, isolated per branch. That is fine for a single linear pipeline, but it leaves performance on the table when you run several jobs in parallel — a matrix build, or a monorepo where many packages build at once. A ",[229,34387,34388],{},"remote cache"," lets one runner populate a shared store and every other runner read from it.",[14,34391,34392,34393,34395,34396,34398,34399,34401],{},"The common implementations are Turborepo's remote cache (self-hostable or hosted), an S3 bucket fronted by a small action, or carefully scoped ",[253,34394,29412],{}," entries with shared ",[253,34397,29547],{},". The payoff scales with parallelism: when ten matrix jobs would each spend 90s processing the same assets, having the first job publish the result and the other nine restore it turns 900s of redundant work into roughly 90s plus nine fast restores. ",[23,34400,33141],{"href":33140}," covers the architecture, signing, and the measured savings in detail.",[14,34403,34404,34405,34407],{},"This is also the bridge to a clean CI\u002FCD pipeline overall — caching is one of the levers covered in ",[23,34406,28200],{"href":28199},", alongside triggers, concurrency, and atomic deploys.",[34,34409,2266],{"id":2265},[39,34411,34412,34426,34432,34441,34447],{},[42,34413,34414,34417,34418,34420,34421,2204,34423,34425],{},[229,34415,34416],{},"Caching the wrong directory."," If you cache ",[253,34419,417],{}," but the generator writes its artifacts to ",[253,34422,3253],{},[253,34424,9444],{},", the build still runs cold. Confirm the path against the tool's docs and the runner log.",[42,34427,34428,34431],{},[229,34429,34430],{},"Keys that never hit."," A commit SHA or timestamp in the cache key makes every key unique, so you write caches nobody reads. Key on content hashes instead.",[42,34433,34434,34437,34438,34440],{},[229,34435,34436],{},"Keys that never refresh."," A fixed string key always hits and never picks up new dependencies or content. Pair a hashed primary key with ",[253,34439,29547],{}," for the fallback.",[42,34442,34443,34446],{},[229,34444,34445],{},"Stale incremental state."," Eleventy and Gatsby keep on-disk state that can desync after a crashed build. When output looks wrong, delete the cache directory and do one clean build to reset it.",[42,34448,34449,34452,34453,34455],{},[229,34450,34451],{},"Ignoring cache size limits."," GitHub Actions evicts caches past a 10 GB per-repo budget on a least-recently-used basis. A bloated cache that includes ",[253,34454,8885],{}," can evict the deps cache you actually wanted.",[34,34457,2321],{"id":2320},[39,34459,34460,34470,34481,34487],{},[42,34461,34462,34463,34466,34467,34469],{},"Incremental builds skip unchanged work ",[18,34464,34465],{},"within"," a run; caching carries artifacts ",[18,34468,33866],{}," runs. CI needs caching; watch mode benefits from incremental flags.",[42,34471,34472,34473,2766,34475,34477,34478,34480],{},"Cache the exact directory the generator reads — ",[253,34474,3253],{},[253,34476,3170],{}," for Astro, ",[253,34479,9444],{}," for Next.js export.",[42,34482,34483,34484,34486],{},"Key caches on a hash of their inputs (lockfile for deps, content for assets) and add ",[253,34485,29547],{}," so near-misses stay warm.",[42,34488,34489],{},"Reach for a remote or shared cache when parallel jobs would otherwise each redo the same work — measure the redundant seconds first.",[34,34491,651],{"id":650},[653,34493,34495],{"id":34494},"what-is-the-difference-between-an-incremental-build-and-build-caching","What is the difference between an incremental build and build caching?",[14,34497,34498,34499,34501],{},"An incremental build rebuilds only the pages affected by a change within a single run, usually in local watch mode. Build caching restores artifacts from a previous run — ",[253,34500,417],{},", processed images, Hugo resources — so a fresh CI machine skips work it already did. They are complementary: caching warms the inputs, incremental logic skips the unchanged outputs.",[653,34503,34505],{"id":34504},"which-static-site-generators-support-true-incremental-builds","Which static site generators support true incremental builds?",[14,34507,34508,34509,34511,34512,34515,34516,34518],{},"Eleventy has an explicit ",[253,34510,981],{}," flag, Hugo rebuilds only changed pages in server and watch mode and caches processed resources, Gatsby supports incremental builds through its cache, and Next.js reuses its ",[253,34513,34514],{},".next"," cache across runs. Astro rebuilds the whole site per run but caches processed assets under ",[253,34517,3170],{},". Full from-scratch CI builds rarely benefit from incremental flags; that is where caching matters.",[653,34520,34522],{"id":34521},"what-should-a-ci-cache-key-be-based-on","What should a CI cache key be based on?",[14,34524,34525,34526,34529,34530,34532],{},"Hash the inputs that determine the output. Use a lockfile hash for dependency caches and a content or config hash for asset caches. A key like ",[253,34527,34528],{},"deps-os-hash-of-lockfile"," restores only when the lockfile is unchanged, and a ",[253,34531,29547],{}," prefix lets a near-miss fall back to the most recent compatible cache instead of starting cold.",[653,34534,34536],{"id":34535},"why-did-my-cache-restore-but-the-build-still-ran-from-scratch","Why did my cache restore but the build still ran from scratch?",[14,34538,34539],{},"The cache path probably did not match what the generator actually reads, or the cache key changed on every run because it included a timestamp or commit SHA. Cache the exact directory the tool writes to, key it on stable content hashes, and confirm with the runner log that the entry was restored rather than created.",[653,34541,34543],{"id":34542},"when-is-a-remote-or-shared-cache-worth-the-complexity","When is a remote or shared cache worth the complexity?",[14,34545,34546],{},"When several parallel jobs or many short-lived runners would otherwise each rebuild the same artifacts. A remote cache backed by S3 or a provider like Turborepo lets one runner populate the cache and every other runner read it, which pays off for monorepos and matrix builds. For a single small site, the built-in per-repo cache is enough.",[34,34548,684],{"id":683},[39,34550,34551,34558,34565,34573,34578],{},[42,34552,34553,692,34555,34557],{},[229,34554,691],{},[23,34556,5505],{"href":5504}," — where build speed fits the deploy pipeline.",[42,34559,34560,26348,34562,34564],{},[23,34561,33134],{"href":33133},[253,34563,981],{}," flag and what gets rebuilt.",[42,34566,34567,34569,34570,34572],{},[23,34568,5002],{"href":5001}," — caching ",[253,34571,3253],{}," and the module cache.",[42,34574,34575,34577],{},[23,34576,33141],{"href":33140}," — remote caches for parallel runners.",[42,34579,34580,33147],{},[23,34581,28200],{"href":28199},[1346,34583,34584],{},"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 .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}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 .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}",{"title":712,"searchDepth":713,"depth":713,"links":34586},[34587,34588,34589,34590,34591,34592,34593,34600],{"id":33989,"depth":713,"text":33990},{"id":34148,"depth":713,"text":34149},{"id":34302,"depth":713,"text":34303},{"id":34381,"depth":713,"text":34382},{"id":2265,"depth":713,"text":2266},{"id":2320,"depth":713,"text":2321},{"id":650,"depth":713,"text":651,"children":34594},[34595,34596,34597,34598,34599],{"id":34494,"depth":730,"text":34495},{"id":34504,"depth":730,"text":34505},{"id":34521,"depth":730,"text":34522},{"id":34535,"depth":730,"text":34536},{"id":34542,"depth":730,"text":34543},{"id":683,"depth":713,"text":684},[34602,34603,34604],{"name":737,"item":738},{"name":5505,"item":5504},{"name":32455,"item":32454},"Cut SSG build times with incremental rebuilds and CI caching — what Astro, Eleventy, Hugo and Next.js support, content-hash cache keys, and remote shared caches.",[34607,34612,34614,34616,34617],{"q":34495,"a":34608},{"An incremental build rebuilds only the pages affected by a change within a single run, usually in local watch mode":34609},{" Build caching restores artifacts from a previous run — node_modules, processed images, Hugo resources — so a fresh CI machine skips work it already did":34610},{" They are complementary":34611},"caching warms the inputs, incremental logic skips the unchanged outputs.",{"q":34505,"a":34613},"Eleventy has an explicit --incremental flag, Hugo rebuilds only changed pages in server and watch mode and caches processed resources, Gatsby supports incremental builds through its cache, and Next.js reuses its .next cache across runs. Astro rebuilds the whole site per run but caches processed assets under node_modules\u002F.astro. Full from-scratch CI builds rarely benefit from incremental flags; that is where caching matters.",{"q":34522,"a":34615},"Hash the inputs that determine the output. Use a lockfile hash for dependency caches and a content or config hash for asset caches. A key like deps-os-hash-of-lockfile restores only when the lockfile is unchanged, and a restore-keys prefix lets a near-miss fall back to the most recent compatible cache instead of starting cold.",{"q":34536,"a":34539},{"q":34543,"a":34546},{},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fincremental-builds-and-build-caching-for-ssgs",{"title":32455,"description":34605},"production-ready-deployment-cicd-workflows\u002Fincremental-builds-and-build-caching-for-ssgs\u002Findex","eX8rqE6kGtC3JaCR17WMXfzstSoyO4Ff0dxPmCp2o0A",{"id":34624,"title":33141,"body":34625,"breadcrumb":35222,"dateModified":743,"datePublished":743,"description":35227,"extension":745,"faq":35228,"meta":35234,"navigation":752,"path":35235,"seo":35236,"slug":34629,"stem":35237,"type":756,"__hash__":35238},"content\u002Fproduction-ready-deployment-cicd-workflows\u002Fincremental-builds-and-build-caching-for-ssgs\u002Fsharing-build-cache-across-ci-runners\u002Findex.md",{"type":7,"value":34626,"toc":35201},[34627,34630,34639,34641,34659,34663,34666,34753,34757,34761,34764,34819,34822,34826,34829,34972,34975,34979,34995,34999,35021,35023,35029,35080,35087,35089,35123,35125,35137,35139,35143,35146,35150,35153,35157,35160,35164,35167,35171,35174,35176,35198],[10,34628,33141],{"id":34629},"sharing-build-cache-across-ci-runners",[14,34631,34632,34633,34636,34637,239],{},"The per-repository cache built into GitHub Actions is scoped to one repo and, by default, isolated per branch. That is enough for a single linear build, but it leaves real time on the table the moment you parallelize. Run a matrix across Node versions, build ten packages in a monorepo, or spin up ephemeral self-hosted runners, and each one starts cold and redoes the same asset processing the others just finished. A ",[229,34634,34635],{},"shared remote cache"," fixes this: one runner populates a central store, and every other runner reads from it instead of rebuilding. This guide covers the architecture, the signing and trust model, and the measured savings. It is the scaling-out piece of ",[23,34638,32455],{"href":32454},[34,34640,37],{"id":36},[39,34642,34643,34653,34656],{},[42,34644,34645,34646,2204,34649,34652],{},"A build that genuinely parallelizes — a matrix, a monorepo, or multiple jobs that share inputs. If you run one job on one runner, the ",[23,34647,34648],{"href":5001},"per-repo cache for Hugo",[23,34650,34651],{"href":1048},"node_modules caching"," is enough.",[42,34654,34655],{},"A place to put the shared store: a Turborepo remote cache (hosted or self-hosted), an S3 bucket, or another object store your runners can reach.",[42,34657,34658],{},"Secrets management for the cache token or bucket credentials, exposed to trusted workflows only.",[34,34660,34662],{"id":34661},"the-architecture","The Architecture",[14,34664,34665],{},"Every shared-cache scheme is content-addressed: an artifact is keyed by a hash of the inputs that produced it, so two runners that compute the same hash share the same entry. The first runner to finish a task uploads its output; later runners compute the matching hash, get a cache hit, and download instead of rebuilding.",[55,34667,34668,34750],{},[58,34669,66,34673,66,34676,66,34679,66,34743],{"viewBox":33875,"role":61,"ariaLabelledBy":34670,"xmlns":65},[34671,34672],"share-arch-title","share-arch-desc",[68,34674,34675],{"id":34671},"Shared remote cache architecture across parallel runners",[72,34677,34678],{"id":34672},"Three parallel CI runners connect to a central remote cache store. Runner one computes a hash, misses, builds, and uploads the artifact. Runners two and three compute the same hash, hit, and download instead of rebuilding.",[95,34680,78,34681,78,34684,78,34686,78,34689,78,34692,78,34694,78,34697,78,34700,78,34703,78,34705,78,34708,78,34711,78,34714,78,34716,78,34719,78,34721,78,34723,78,34729,78,34732,78,34740,66],{"style":97},[99,34682,34683],{"x":167,"y":109,"fill":103,"style":104},"One runner builds, the rest download",[107,34685],{"x":158,"y":822,"width":160,"height":1420,"rx":113,"fill":114,"opacity":186,"stroke":114,"style":116},[99,34687,34688],{"x":167,"y":120,"fill":114,"style":121},"Remote cache",[99,34690,34691],{"x":167,"y":1426,"fill":93,"style":126},"S3 \u002F Turborepo · content-addressed",[107,34693],{"x":3578,"y":3500,"width":160,"height":1430,"rx":823,"fill":2564,"opacity":115,"stroke":2565,"style":116},[99,34695,34696],{"x":2563,"y":8710,"fill":2565,"style":121},"Runner 1 · MISS",[99,34698,34699],{"x":2563,"y":32568,"fill":93,"style":126},"build assets",[99,34701,34702],{"x":2563,"y":3572,"fill":93,"style":126},"upload artifact",[107,34704],{"x":158,"y":3500,"width":160,"height":1430,"rx":823,"fill":185,"opacity":850,"stroke":187,"style":116},[99,34706,34707],{"x":167,"y":8710,"fill":187,"style":121},"Runner 2 · HIT",[99,34709,34710],{"x":167,"y":32568,"fill":93,"style":126},"same hash",[99,34712,34713],{"x":167,"y":3572,"fill":93,"style":126},"download artifact",[107,34715],{"x":10820,"y":3500,"width":160,"height":1430,"rx":823,"fill":185,"opacity":850,"stroke":187,"style":116},[99,34717,34718],{"x":190,"y":8710,"fill":187,"style":121},"Runner 3 · HIT",[99,34720,34710],{"x":190,"y":32568,"fill":93,"style":126},[99,34722,34713],{"x":190,"y":3572,"fill":93,"style":126},[95,34724,88,34725,78],{"stroke":2565,"fill":205,"style":116},[90,34726],{"d":34727,"style":34728},"M180 208 L330 122","marker-end:url(#share-arrow)",[99,34730,34731],{"x":14947,"y":194,"fill":2565,"style":11285},"upload",[95,34733,88,34734,88,34737,78],{"stroke":187,"fill":205,"style":116},[90,34735],{"d":34736,"style":34728},"M390 208 L390 122",[90,34738],{"d":34739,"style":34728},"M600 208 L450 122",[99,34741,34742],{"x":2552,"y":194,"fill":187,"style":11285},"download",[76,34744,78,34745,66],{},[80,34746,88,34748,78],{"id":34747,"viewBox":83,"refX":84,"refY":85,"markerWidth":86,"markerHeight":86,"orient":87},"share-arrow",[90,34749],{"d":92,"fill":93},[218,34751,34752],{},"Each runner computes a content hash of its inputs. The first to finish uploads the artifact; the rest get a hit on the same hash and download it instead of rebuilding.",[34,34754,34756],{"id":34755},"three-ways-to-share","Three Ways to Share",[653,34758,34760],{"id":34759},"turborepo-remote-cache","Turborepo remote cache",[14,34762,34763],{},"If your site lives in a Turborepo monorepo, the remote cache is the least-effort option. Turborepo already content-hashes every task's inputs; point it at a remote and the hashes become shareable across runners:",[987,34765,34767],{"className":1912,"code":34766,"language":1914,"meta":712,"style":712},"- run: npx turbo build\n  env:\n    TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}\n    TURBO_TEAM: ${{ vars.TURBO_TEAM }}\n    TURBO_REMOTE_ONLY: 'true'   # do not write a local cache in CI\n",[253,34768,34769,34780,34786,34796,34806],{"__ignoreMap":712},[995,34770,34771,34773,34775,34777],{"class":997,"line":998},[995,34772,3191],{"class":1618},[995,34774,2028],{"class":1921},[995,34776,1925],{"class":1618},[995,34778,34779],{"class":1023},"npx turbo build\n",[995,34781,34782,34784],{"class":997,"line":713},[995,34783,32027],{"class":1921},[995,34785,1946],{"class":1618},[995,34787,34788,34791,34793],{"class":997,"line":730},[995,34789,34790],{"class":1921},"    TURBO_TOKEN",[995,34792,1925],{"class":1618},[995,34794,34795],{"class":1023},"${{ secrets.TURBO_TOKEN }}\n",[995,34797,34798,34801,34803],{"class":997,"line":1544},[995,34799,34800],{"class":1921},"    TURBO_TEAM",[995,34802,1925],{"class":1618},[995,34804,34805],{"class":1023},"${{ vars.TURBO_TEAM }}\n",[995,34807,34808,34811,34813,34816],{"class":997,"line":1550},[995,34809,34810],{"class":1921},"    TURBO_REMOTE_ONLY",[995,34812,1925],{"class":1618},[995,34814,34815],{"class":1023},"'true'",[995,34817,34818],{"class":1001},"   # do not write a local cache in CI\n",[14,34820,34821],{},"You can use the hosted remote cache or self-host an S3-backed one with an open-source cache server, keeping artifacts inside your own infrastructure.",[653,34823,34825],{"id":34824},"s3-backed-cache","S3-backed cache",[14,34827,34828],{},"For a non-Turborepo build, an S3 bucket plus a small step gives you the same cross-runner sharing. Compute a content hash, try to download, build on a miss, and upload:",[987,34830,34832],{"className":989,"code":34831,"language":991,"meta":712,"style":712},"KEY=\"ssg-$(cat assets\u002F** package-lock.json | sha256sum | cut -c1-32).tar.zst\"\nif aws s3 cp \"s3:\u002F\u002Fmy-build-cache\u002F$KEY\" cache.tar.zst 2>\u002Fdev\u002Fnull; then\n  tar --use-compress-program=unzstd -xf cache.tar.zst   # HIT\nelse\n  npm run build                                          # MISS\n  tar --use-compress-program=zstd -cf cache.tar.zst resources\u002F_gen dist\n  aws s3 cp cache.tar.zst \"s3:\u002F\u002Fmy-build-cache\u002F$KEY\"\nfi\n",[253,34833,34834,34868,34900,34916,34921,34933,34950,34968],{"__ignoreMap":712},[995,34835,34836,34839,34841,34844,34846,34849,34851,34854,34856,34858,34860,34862,34865],{"class":997,"line":998},[995,34837,34838],{"class":1618},"KEY",[995,34840,7317],{"class":1614},[995,34842,34843],{"class":1023},"\"ssg-$(",[995,34845,34339],{"class":1007},[995,34847,34848],{"class":1023}," assets\u002F",[995,34850,34345],{"class":1010},[995,34852,34853],{"class":1023}," package-lock.json ",[995,34855,34355],{"class":1614},[995,34857,34358],{"class":1007},[995,34859,14477],{"class":1614},[995,34861,34363],{"class":1007},[995,34863,34864],{"class":1010}," -c1-32",[995,34866,34867],{"class":1023},").tar.zst\"\n",[995,34869,34870,34872,34875,34878,34881,34884,34887,34889,34892,34894,34896,34898],{"class":997,"line":713},[995,34871,22753],{"class":1614},[995,34873,34874],{"class":1007}," aws",[995,34876,34877],{"class":1023}," s3",[995,34879,34880],{"class":1023}," cp",[995,34882,34883],{"class":1023}," \"s3:\u002F\u002Fmy-build-cache\u002F",[995,34885,34886],{"class":1618},"$KEY",[995,34888,18873],{"class":1023},[995,34890,34891],{"class":1023}," cache.tar.zst",[995,34893,18876],{"class":1614},[995,34895,18879],{"class":1023},[995,34897,18846],{"class":1618},[995,34899,18926],{"class":1614},[995,34901,34902,34905,34908,34911,34913],{"class":997,"line":730},[995,34903,34904],{"class":1007},"  tar",[995,34906,34907],{"class":1010}," --use-compress-program=unzstd",[995,34909,34910],{"class":1010}," -xf",[995,34912,34891],{"class":1023},[995,34914,34915],{"class":1001},"   # HIT\n",[995,34917,34918],{"class":997,"line":1544},[995,34919,34920],{"class":1614},"else\n",[995,34922,34923,34926,34928,34930],{"class":997,"line":1550},[995,34924,34925],{"class":1007},"  npm",[995,34927,2655],{"class":1023},[995,34929,7332],{"class":1023},[995,34931,34932],{"class":1001},"                                          # MISS\n",[995,34934,34935,34937,34940,34943,34945,34948],{"class":997,"line":1673},[995,34936,34904],{"class":1007},[995,34938,34939],{"class":1010}," --use-compress-program=zstd",[995,34941,34942],{"class":1010}," -cf",[995,34944,34891],{"class":1023},[995,34946,34947],{"class":1023}," resources\u002F_gen",[995,34949,1088],{"class":1023},[995,34951,34952,34955,34957,34959,34961,34963,34965],{"class":997,"line":1678},[995,34953,34954],{"class":1007},"  aws",[995,34956,34877],{"class":1023},[995,34958,34880],{"class":1023},[995,34960,34891],{"class":1023},[995,34962,34883],{"class":1023},[995,34964,34886],{"class":1618},[995,34966,34967],{"class":1023},"\"\n",[995,34969,34970],{"class":997,"line":1693},[995,34971,22810],{"class":1614},[14,34973,34974],{},"S3 lifecycle rules expire stale entries automatically, and the bucket is reachable from any runner in any branch — exactly what the per-repo cache cannot do.",[653,34976,34978],{"id":34977},"scoped-actionscache","Scoped actions\u002Fcache",[14,34980,34981,34982,34984,34985,34987,34988,270,34991,34994],{},"Without leaving GitHub-hosted infrastructure, you can still share across jobs in one workflow by using a stable primary key with shared ",[253,34983,29547],{},", so a ",[253,34986,5577],{}," job writes a cache that parallel ",[253,34989,34990],{},"test",[253,34992,34993],{},"lint"," jobs restore. This is the lightest option but stays inside one repo and respects the 10 GB per-repo budget.",[34,34996,34998],{"id":34997},"signing-and-trust","Signing and Trust",[14,35000,35001,35002,35005,35006,35009,35010,35013,35014,35017,35018,35020],{},"A shared cache is a supply-chain surface: an artifact one runner consumes was produced by another. The standard policy is ",[229,35003,35004],{},"trusted writes, open reads",". Let only protected branches hold the credentials that write to the remote cache; give pull requests — especially from forks — read-only access. Turborepo signs cache artifacts with an HMAC key (",[253,35007,35008],{},"TURBO_REMOTE_CACHE_SIGNATURE_KEY",") so a consumer can verify an entry was produced by a trusted writer before using it. For an S3 store, scope the PR workflow's IAM credentials to ",[253,35011,35012],{},"s3:GetObject"," only, and keep ",[253,35015,35016],{},"s3:PutObject"," on the protected-branch workflow. This prevents a malicious fork from poisoning an artifact that ",[253,35019,27507],{}," later downloads.",[34,35022,1166],{"id":1165},[14,35024,35025,35026,35028],{},"Benchmarked on a monorepo with one Hugo docs site plus a shared design-system package, built as a 4-way matrix on ",[253,35027,29592],{},". Asset processing for the docs site is ~90 s cold. Times are from the run summary, with the remote cache hosted in S3 in the same region:",[433,35030,35031,35042],{},[436,35032,35033],{},[439,35034,35035,35037,35040],{},[442,35036,17476],{},[442,35038,35039],{},"Wall-clock for the matrix",[442,35041,9113],{},[457,35043,35044,35055,35069],{},[439,35045,35046,35049,35052],{},[462,35047,35048],{},"No shared cache (each job cold)",[462,35050,35051],{},"4 jobs × 90 s ≈ 360 s of build work",[462,35053,35054],{},"every runner reprocesses everything",[439,35056,35057,35063,35066],{},[462,35058,35059,35060,35062],{},"Per-repo ",[253,35061,29412],{}," only",[462,35064,35065],{},"~360 s on first run, ~100 s after",[462,35067,35068],{},"not shared across the matrix legs",[439,35070,35071,35074,35077],{},[462,35072,35073],{},"S3 remote cache",[462,35075,35076],{},"~95 s first leg + 3 × ~9 s restores ≈ 122 s",[462,35078,35079],{},"one leg builds, three download",[14,35081,35082,35083,35086],{},"On the matrix, the remote cache turned roughly ",[229,35084,35085],{},"360 s of redundant build work into about 122 s"," — one full build plus three fast restores — because only the first leg to reach the task actually processed assets. The restore overhead was 8-10 s per leg, dominated by download and decompression. The win scales with the width of the matrix: a wider fan-out means more legs reading one upload.",[34,35088,600],{"id":599},[39,35090,35091,35097,35103,35109,35118],{},[42,35092,35093,35096],{},[229,35094,35095],{},"Unstable hashes."," If the cache key includes a timestamp, commit SHA, or absolute path, every runner computes a unique key and never hits. Hash only the real inputs — source, deps, task config.",[42,35098,35099,35102],{},[229,35100,35101],{},"Untrusted writes."," Letting fork pull requests write to the shared cache is a poisoning risk. Use trusted-write, open-read and verify signatures.",[42,35104,35105,35108],{},[229,35106,35107],{},"Region latency."," A remote store in a different region can make downloads slower than rebuilding small artifacts. Co-locate the cache with the runners.",[42,35110,35111,35114,35115,35117],{},[229,35112,35113],{},"Over-caching."," Pushing huge ",[253,35116,8885],{}," outputs into the remote cache can cost more in transfer than the rebuild saves. Cache the expensive intermediate, not everything.",[42,35119,35120,35122],{},[229,35121,637],{}," remove the remote-cache env vars or the S3 step and runners fall back to local or per-repo caching — slower under parallelism but fully correct, with no shared state to clean up beyond expiring the bucket entries.",[34,35124,642],{"id":641},[14,35126,35127,35128,35130,35131,35133,35134,35136],{},"A shared remote cache earns its complexity exactly when parallel runners would otherwise each redo the same work. Content-address every artifact, pick a store your runners can reach — Turborepo remote cache, S3, or scoped ",[253,35129,29412],{}," — and enforce trusted-write, open-read so the cache cannot be poisoned. On a 4-way matrix it collapsed 360 s of redundant processing to about 122 s. Pair it with the per-run techniques in ",[23,35132,33134],{"href":33133}," and the per-repo recipe in ",[23,35135,5002],{"href":5001}," for caching at every layer.",[34,35138,651],{"id":650},[653,35140,35142],{"id":35141},"when-does-a-shared-remote-cache-beat-the-built-in-per-repo-cache","When does a shared remote cache beat the built-in per-repo cache?",[14,35144,35145],{},"When several runners would otherwise redo the same work. A matrix build, a monorepo with many packages, or self-hosted ephemeral runners all benefit because one runner populates the cache and the others read it. A single linear pipeline on one runner is already well served by the built-in per-repo cache and gains little.",[653,35147,35149],{"id":35148},"how-does-a-remote-cache-decide-what-to-reuse","How does a remote cache decide what to reuse?",[14,35151,35152],{},"It keys each cached artifact on a hash of the inputs that produced it — source files, dependencies, and the task configuration. When another runner computes the same hash it downloads the artifact instead of rebuilding. This is content-addressed caching, so a cache entry is only ever reused when the inputs are identical.",[653,35154,35156],{"id":35155},"is-it-safe-to-share-a-build-cache-across-branches-and-pull-requests","Is it safe to share a build cache across branches and pull requests?",[14,35158,35159],{},"Reads are generally safe because entries are content-addressed, but writes from untrusted pull requests are a supply-chain risk. The common policy is to let trusted branches write to the remote cache and let pull requests read only, so a fork cannot poison artifacts that protected branches later consume.",[653,35161,35163],{"id":35162},"what-do-i-store-in-an-s3-backed-cache","What do I store in an S3-backed cache?",[14,35165,35166],{},"The same artifacts you would cache locally — compiled output, processed assets, a tool cache — packaged as a tarball and keyed by a content hash. A small action or CLI uploads on a miss and downloads on a hit. S3 gives you cross-runner, cross-branch storage with lifecycle rules to expire old entries.",[653,35168,35170],{"id":35169},"does-a-remote-cache-replace-incremental-builds","Does a remote cache replace incremental builds?",[14,35172,35173],{},"No, they work at different layers. Incremental builds skip unchanged work within one run; a remote cache skips work that any previous run on any runner already did. Used together, a runner restores upstream artifacts from the remote cache and then does only the incremental work that remains.",[34,35175,684],{"id":683},[39,35177,35178,35184,35189,35194],{},[42,35179,35180,692,35182,33128],{},[229,35181,691],{},[23,35183,32455],{"href":32454},[42,35185,35186,35188],{},[23,35187,5002],{"href":5001}," — the per-repo recipe this scales out from.",[42,35190,35191,35193],{},[23,35192,33134],{"href":33133}," — per-run incremental rebuilds.",[42,35195,35196,33147],{},[23,35197,28200],{"href":28199},[1346,35199,35200],{},"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 .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}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 .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}",{"title":712,"searchDepth":713,"depth":713,"links":35202},[35203,35204,35205,35210,35211,35212,35213,35214,35221],{"id":36,"depth":713,"text":37},{"id":34661,"depth":713,"text":34662},{"id":34755,"depth":713,"text":34756,"children":35206},[35207,35208,35209],{"id":34759,"depth":730,"text":34760},{"id":34824,"depth":730,"text":34825},{"id":34977,"depth":730,"text":34978},{"id":34997,"depth":713,"text":34998},{"id":1165,"depth":713,"text":1166},{"id":599,"depth":713,"text":600},{"id":641,"depth":713,"text":642},{"id":650,"depth":713,"text":651,"children":35215},[35216,35217,35218,35219,35220],{"id":35141,"depth":730,"text":35142},{"id":35148,"depth":730,"text":35149},{"id":35155,"depth":730,"text":35156},{"id":35162,"depth":730,"text":35163},{"id":35169,"depth":730,"text":35170},{"id":683,"depth":713,"text":684},[35223,35224,35225,35226],{"name":737,"item":738},{"name":5505,"item":5504},{"name":32455,"item":32454},{"name":33141,"item":33140},"Share a build cache across parallel CI runners with Turborepo remote cache, an S3-backed store, or scoped actions\u002Fcache. Architecture, signing, and measured savings.",[35229,35230,35231,35232,35233],{"q":35142,"a":35145},{"q":35149,"a":35152},{"q":35156,"a":35159},{"q":35163,"a":35166},{"q":35170,"a":35173},{},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fincremental-builds-and-build-caching-for-ssgs\u002Fsharing-build-cache-across-ci-runners",{"title":33141,"description":35227},"production-ready-deployment-cicd-workflows\u002Fincremental-builds-and-build-caching-for-ssgs\u002Fsharing-build-cache-across-ci-runners\u002Findex","Av-Q8PjiWM1Tzidty0xd96yxYssukOHryJ5tSIiqRuA",{"id":35240,"title":35241,"body":35242,"breadcrumb":36110,"dateModified":743,"datePublished":2446,"description":36113,"extension":745,"faq":36114,"meta":36122,"navigation":752,"path":36123,"seo":36124,"slug":35246,"stem":36125,"type":6089,"__hash__":36126},"content\u002Fproduction-ready-deployment-cicd-workflows\u002Findex.md","Production-Ready Deployment & CI\u002FCD for SSGs",{"type":7,"value":35243,"toc":36088},[35244,35247,35250,35253,35396,35398,35401,35445,35454,35458,35461,35471,35477,35481,35487,35637,35656,35660,35663,35725,35730,35734,35737,35740,35764,35769,35773,35776,35782,35794,35797,35860,35863,35867,35874,35887,35891,35894,35900,35904,35907,35910,35916,35922,35924,35960,35962,35982,35984,35988,35991,35995,36010,36014,36017,36021,36024,36028,36031,36035,36038,36040,36085],[10,35245,5505],{"id":35246},"production-ready-deployment-cicd-workflows",[14,35248,35249],{},"Deploying a static site reliably comes down to two disciplines: builds that produce the same artifact every time, and deploys that flip atomically so a half-finished release never reaches users. Get those right and \"deploy\" stops being an event you brace for and becomes something you do many times a day without thinking about it. This guide is for engineers and documentation teams who already ship a static site and want a pipeline that is reproducible, reviewable, and instantly reversible.",[14,35251,35252],{},"The patterns here apply across Astro, Eleventy, Hugo, Jekyll, and a Next.js static export — only the build output directory and a few host flags differ. We move through the full release lifecycle in the order it actually runs: commit, build, preview deploy, automated checks, promotion to production, and rollback when something slips through. Every stage ties back to a concrete host behavior and a command you can run to verify it.",[55,35254,35255,35393],{},[58,35256,66,35261,66,35264,66,35267,66,35270,66,35381],{"viewBox":35257,"role":61,"ariaLabelledBy":35258,"xmlns":65},"0 0 860 360",[35259,35260],"cicd-life-title","cicd-life-desc",[68,35262,35263],{"id":35259},"The deploy lifecycle: commit to rollback across hosts",[72,35265,35266],{"id":35260},"A pipeline showing commit, build, preview deploy, automated checks, promotion to production, and rollback, with the hosts that handle each stage: GitHub Actions for build and checks, and Cloudflare Pages, Netlify, and Vercel for preview, promotion, and rollback.",[107,35268],{"x":2515,"y":2515,"width":35269,"height":3474,"fill":205},"860",[95,35271,78,35272,78,35275,78,35277,78,35280,78,35283,78,35285,78,35287,78,35290,78,35293,78,35295,78,35298,78,35301,78,35304,78,35306,78,35309,78,35312,78,35315,78,35317,78,35320,78,35323,78,35326,78,35329,78,35333,78,35336,78,35339,78,35357,78,35361,78,35366,78,35368,78,35370,78,35373,78,35375,78,35378,66],{"style":813},[99,35273,35274],{"x":5320,"y":4630,"fill":103,"style":1416},"One commit moves through six stages before it is live — and can reverse in one",[107,35276],{"x":5393,"y":849,"width":1431,"height":1430,"rx":823,"fill":114,"opacity":186,"stroke":114,"style":116},[99,35278,35279],{"x":1430,"y":1426,"fill":114,"style":121},"Commit",[99,35281,35282],{"x":1430,"y":15536,"fill":93,"style":126},"push to branch",[107,35284],{"x":16983,"y":849,"width":1431,"height":1430,"rx":823,"fill":824,"opacity":186,"stroke":824,"style":116},[99,35286,5022],{"x":14947,"y":1426,"fill":824,"style":121},[99,35288,35289],{"x":14947,"y":4661,"fill":93,"style":126},"npm ci + build",[99,35291,35292],{"x":14947,"y":5379,"fill":93,"style":4658},"cached deps",[107,35294],{"x":5361,"y":849,"width":1431,"height":1430,"rx":823,"fill":162,"opacity":23275,"stroke":164,"style":116},[99,35296,35297],{"x":12010,"y":1426,"fill":103,"style":121},"Preview",[99,35299,35300],{"x":12010,"y":4661,"fill":93,"style":126},"per-PR URL",[99,35302,35303],{"x":12010,"y":5379,"fill":93,"style":4658},"isolated deploy",[107,35305],{"x":4641,"y":849,"width":1431,"height":1430,"rx":823,"fill":162,"opacity":886,"stroke":164,"style":116},[99,35307,35308],{"x":17851,"y":1426,"fill":103,"style":121},"Checks",[99,35310,35311],{"x":17851,"y":4661,"fill":93,"style":126},"links · a11y",[99,35313,35314],{"x":17851,"y":5379,"fill":93,"style":4658},"Lighthouse",[107,35316],{"x":9750,"y":849,"width":1431,"height":1430,"rx":823,"fill":185,"opacity":850,"stroke":187,"style":116},[99,35318,35319],{"x":2562,"y":1426,"fill":187,"style":121},"Promote",[99,35321,35322],{"x":2562,"y":4661,"fill":93,"style":126},"atomic flip",[99,35324,35325],{"x":2562,"y":5379,"fill":93,"style":4658},"to production",[107,35327],{"x":35328,"y":849,"width":15952,"height":1430,"rx":823,"fill":2564,"opacity":825,"stroke":2565,"style":116},"745",[99,35330,35332],{"x":35331,"y":1426,"fill":2565,"style":121},"792","Rollback",[99,35334,35335],{"x":35331,"y":4661,"fill":93,"style":4658},"re-point to",[99,35337,35338],{"x":35331,"y":5379,"fill":93,"style":4658},"prev build",[95,35340,88,35341,88,35345,88,35348,88,35351,88,35354,78],{"stroke":93,"fill":205,"style":116},[90,35342],{"d":35343,"style":35344},"M140 110 L163 110","marker-end:url(#cicd-arrow)",[90,35346],{"d":35347,"style":35344},"M285 110 L308 110",[90,35349],{"d":35350,"style":35344},"M430 110 L453 110",[90,35352],{"d":35353,"style":35344},"M575 110 L598 110",[90,35355],{"d":35356,"style":35344},"M720 110 L743 110",[90,35358],{"d":35359,"stroke":2565,"fill":205,"style":35360},"M792 150 C792 215, 660 215, 660 152","stroke-width:2px;stroke-dasharray:5 4;marker-end:url(#cicd-arrow-red)",[99,35362,35365],{"x":35363,"y":35364,"fill":2565,"style":4658},"726","205","one-step reverse",[107,35367],{"x":3578,"y":8714,"width":215,"height":1420,"rx":3579,"fill":824,"opacity":30008,"stroke":2592,"style":878},[99,35369,29972],{"x":20123,"y":17020,"fill":824,"style":33249},[99,35371,35372],{"x":20123,"y":6882,"fill":93,"style":126},"build & checks · shared cache · matrix",[107,35374],{"x":4696,"y":8714,"width":215,"height":1420,"rx":3579,"fill":185,"opacity":6172,"stroke":2592,"style":878},[99,35376,35377],{"x":3534,"y":17020,"fill":187,"style":33249},"Cloudflare Pages · Netlify · Vercel",[99,35379,35380],{"x":3534,"y":6882,"fill":93,"style":126},"preview · promote · rollback · edge cache",[76,35382,78,35383,78,35388,66],{},[80,35384,88,35386,78],{"id":35385,"viewBox":83,"refX":84,"refY":85,"markerWidth":86,"markerHeight":86,"orient":87},"cicd-arrow",[90,35387],{"d":92,"fill":93},[80,35389,88,35391,78],{"id":35390,"viewBox":83,"refX":84,"refY":85,"markerWidth":86,"markerHeight":86,"orient":87},"cicd-arrow-red",[90,35392],{"d":92,"fill":2565},[218,35394,35395],{},"GitHub Actions owns build and checks; the host platforms own preview, promotion, and the one-step rollback. Each stage produces an immutable artifact the next stage consumes.",[34,35397,5447],{"id":5446},[14,35399,35400],{},"This guide is organized around the deploy lifecycle, and each stage has its own deep-dive section you can follow when you implement it:",[39,35402,35403,35413,35421,35429,35437],{},[42,35404,35405,35407,35408,35410,35411,239],{},[229,35406,20214],{}," — controlling Cloudflare's global edge with a ",[253,35409,14036],{}," file so repeat visits are nearly free, in ",[23,35412,26988],{"href":26987},[42,35414,35415,35418,35419,239],{},[229,35416,35417],{},"Reproducible builds in CI"," — deterministic installs, artifacts, and matrix testing, in ",[23,35420,28200],{"href":28199},[42,35422,35423,35426,35427,239],{},[229,35424,35425],{},"Host selection and routing"," — how the major managed hosts differ on builds, functions, and previews, in ",[23,35428,28797],{"href":28796},[42,35430,35431,35434,35435,239],{},[229,35432,35433],{},"Preview-per-PR"," — giving every pull request a real, isolated deploy URL before merge, in ",[23,35436,28330],{"href":28329},[42,35438,35439,35442,35443,239],{},[229,35440,35441],{},"Faster builds through caching"," — incremental builds and shared CI cache so build time does not grow with the site, in ",[23,35444,32455],{"href":32454},[14,35446,35447,35448,35450,35451,35453],{},"This work sits alongside the other two halves of running a static site in production: ",[23,35449,31],{"href":30}," covers the framework decision that shapes your build, and ",[23,35452,5501],{"href":5500}," covers the runtime metrics your deploy pipeline is ultimately protecting.",[34,35455,35457],{"id":35456},"choosing-a-host-for-your-ssg","Choosing a Host for Your SSG",[14,35459,35460],{},"The framework you picked dictates the build; the host dictates routing, edge-compute limits, preview ergonomics, and cost. Every major host can serve pre-rendered HTML quickly, so the decision is about everything around the artifact.",[14,35462,35463,35464,270,35466,35468,35469,239],{},"Cloudflare Pages leans on the largest edge network and an unmetered bandwidth model, which makes it the cheapest choice for high-traffic content sites; its ",[253,35465,14036],{},[253,35467,11598],{}," files keep cache and routing in version control. Netlify pioneered the deploy-preview-per-PR workflow and has the most polished build plugins and forms\u002Fredirects ergonomics. Vercel is the natural home for a Next.js static export and offers incremental static regeneration when you have a handful of pages that genuinely need to revalidate. The detailed trade-off lives in ",[23,35470,28797],{"href":28796},[14,35472,35473,35474,35476],{},"A useful rule: pick the host whose native build covers 90% of your needs, then reach for ",[23,35475,28200],{"href":28199}," only for the steps the host cannot do — custom asset processing, matrix testing across Node versions, or a shared cache spanning multiple jobs.",[34,35478,35480],{"id":35479},"reproducible-build-pipelines","Reproducible Build Pipelines",[14,35482,35483,35484,35486],{},"A build is reproducible when the same commit produces a byte-identical artifact on any runner. That starts with installing from the lockfile, never the loose ",[253,35485,21912],{}," range, and pinning the runtime so a host's default Node version cannot silently change your output.",[987,35488,35490],{"className":1912,"code":35489,"language":1914,"meta":712,"style":712},"name: Deploy SSG\non:\n  push:\n    branches: [main]\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions\u002Fcheckout@v4\n      - uses: actions\u002Fsetup-node@v4\n        with:\n          node-version: 22\n          cache: npm\n      - run: npm ci && npm run build\n      - uses: actions\u002Fupload-artifact@v4\n        with:\n          name: dist\n          path: .\u002Fdist\n",[253,35491,35492,35501,35507,35513,35523,35529,35535,35543,35549,35559,35569,35575,35583,35591,35601,35612,35618,35628],{"__ignoreMap":712},[995,35493,35494,35496,35498],{"class":997,"line":998},[995,35495,1922],{"class":1921},[995,35497,1925],{"class":1618},[995,35499,35500],{"class":1023},"Deploy SSG\n",[995,35502,35503,35505],{"class":997,"line":713},[995,35504,1933],{"class":1010},[995,35506,1946],{"class":1618},[995,35508,35509,35511],{"class":997,"line":730},[995,35510,30141],{"class":1921},[995,35512,1946],{"class":1618},[995,35514,35515,35517,35519,35521],{"class":997,"line":1544},[995,35516,30148],{"class":1921},[995,35518,4044],{"class":1618},[995,35520,27507],{"class":1023},[995,35522,4050],{"class":1618},[995,35524,35525,35527],{"class":997,"line":1550},[995,35526,1943],{"class":1921},[995,35528,1946],{"class":1618},[995,35530,35531,35533],{"class":997,"line":1673},[995,35532,1951],{"class":1921},[995,35534,1946],{"class":1618},[995,35536,35537,35539,35541],{"class":997,"line":1678},[995,35538,1958],{"class":1921},[995,35540,1925],{"class":1618},[995,35542,1963],{"class":1023},[995,35544,35545,35547],{"class":997,"line":1693},[995,35546,1968],{"class":1921},[995,35548,1946],{"class":1618},[995,35550,35551,35553,35555,35557],{"class":997,"line":1705},[995,35552,1975],{"class":1618},[995,35554,1978],{"class":1921},[995,35556,1925],{"class":1618},[995,35558,1983],{"class":1023},[995,35560,35561,35563,35565,35567],{"class":997,"line":1711},[995,35562,1975],{"class":1618},[995,35564,1978],{"class":1921},[995,35566,1925],{"class":1618},[995,35568,1994],{"class":1023},[995,35570,35571,35573],{"class":997,"line":1717},[995,35572,1999],{"class":1921},[995,35574,1946],{"class":1618},[995,35576,35577,35579,35581],{"class":997,"line":1726},[995,35578,2006],{"class":1921},[995,35580,1925],{"class":1618},[995,35582,2011],{"class":1010},[995,35584,35585,35587,35589],{"class":997,"line":1732},[995,35586,2016],{"class":1921},[995,35588,1925],{"class":1618},[995,35590,2021],{"class":1023},[995,35592,35593,35595,35597,35599],{"class":997,"line":2967},[995,35594,1975],{"class":1618},[995,35596,2028],{"class":1921},[995,35598,1925],{"class":1618},[995,35600,2033],{"class":1023},[995,35602,35603,35605,35607,35609],{"class":997,"line":2972},[995,35604,1975],{"class":1618},[995,35606,1978],{"class":1921},[995,35608,1925],{"class":1618},[995,35610,35611],{"class":1023},"actions\u002Fupload-artifact@v4\n",[995,35613,35614,35616],{"class":997,"line":4147},[995,35615,1999],{"class":1921},[995,35617,1946],{"class":1618},[995,35619,35620,35623,35625],{"class":997,"line":4158},[995,35621,35622],{"class":1921},"          name",[995,35624,1925],{"class":1618},[995,35626,35627],{"class":1023},"dist\n",[995,35629,35630,35632,35634],{"class":997,"line":4168},[995,35631,4130],{"class":1921},[995,35633,1925],{"class":1618},[995,35635,35636],{"class":1023},".\u002Fdist\n",[14,35638,35639,35641,35642,35644,35645,35647,35648,35650,35651,35653,35654,239],{},[253,35640,2072],{}," installs exactly what the lockfile specifies for a deterministic dependency tree, and the uploaded artifact is handed to a separate deploy job so build and deploy are decoupled. Set ",[253,35643,90],{}," to your generator's output directory: Astro emits ",[253,35646,2242],{},", Hugo emits ",[253,35649,14988],{},", and Eleventy and Jekyll both default to ",[253,35652,2245],{},". The end-to-end Cloudflare example is in ",[23,35655,26981],{"href":27636},[34,35657,35659],{"id":35658},"faster-builds-incremental-compilation-caching","Faster Builds: Incremental Compilation & Caching",[14,35661,35662],{},"The single biggest threat to a pleasant pipeline is build time growing with the site. A 200-page site that builds in 20 seconds becomes a 2,000-page site that builds in four minutes, and suddenly every preview deploy is a coffee break. Two techniques keep it flat: incremental builds that only re-render changed pages, and a CI cache that survives between runs so dependencies and processed assets are not recomputed.",[987,35664,35666],{"className":1912,"code":35665,"language":1914,"meta":712,"style":712},"# Restore the generator's build cache between runs\n- uses: actions\u002Fcache@v4\n  with:\n    path: |\n      node_modules\u002F.cache\n      .eleventy-cache\n    key: ${{ runner.os }}-ssg-${{ hashFiles('package-lock.json') }}\n    restore-keys: ${{ runner.os }}-ssg-\n",[253,35667,35668,35673,35683,35689,35697,35702,35707,35716],{"__ignoreMap":712},[995,35669,35670],{"class":997,"line":998},[995,35671,35672],{"class":1001},"# Restore the generator's build cache between runs\n",[995,35674,35675,35677,35679,35681],{"class":997,"line":713},[995,35676,3191],{"class":1618},[995,35678,1978],{"class":1921},[995,35680,1925],{"class":1618},[995,35682,3198],{"class":1023},[995,35684,35685,35687],{"class":997,"line":730},[995,35686,3203],{"class":1921},[995,35688,1946],{"class":1618},[995,35690,35691,35693,35695],{"class":997,"line":1544},[995,35692,3210],{"class":1921},[995,35694,1925],{"class":1618},[995,35696,3215],{"class":1614},[995,35698,35699],{"class":997,"line":1550},[995,35700,35701],{"class":1023},"      node_modules\u002F.cache\n",[995,35703,35704],{"class":997,"line":1673},[995,35705,35706],{"class":1023},"      .eleventy-cache\n",[995,35708,35709,35711,35713],{"class":997,"line":1678},[995,35710,3235],{"class":1921},[995,35712,1925],{"class":1618},[995,35714,35715],{"class":1023},"${{ runner.os }}-ssg-${{ hashFiles('package-lock.json') }}\n",[995,35717,35718,35720,35722],{"class":997,"line":1693},[995,35719,29512],{"class":1921},[995,35721,1925],{"class":1618},[995,35723,35724],{"class":1023},"${{ runner.os }}-ssg-\n",[14,35726,35727,35728,239],{},"A warm cache routinely turns a 90-second cold build into a 15-second incremental one. The full set of techniques — per-generator incremental flags, cache-key hygiene, and sharing a cache across runners — is in ",[23,35729,32455],{"href":32454},[34,35731,35733],{"id":35732},"preview-environments-for-every-pull-request","Preview Environments for Every Pull Request",[14,35735,35736],{},"The cheapest place to catch a broken link, a rendering error, or a Core Web Vitals regression is a preview URL, not production. Every managed host can build a pull request into an isolated, fully addressable deploy with its own subdomain, and you should require that preview to pass before merge.",[14,35738,35739],{},"A preview deploy is where your automated checks belong — link checking, accessibility audits, and a Lighthouse budget all run against the real deployed URL rather than a local guess:",[987,35741,35743],{"className":989,"code":35742,"language":991,"meta":712,"style":712},"lhci autorun \\\n  --collect.url=https:\u002F\u002Fdeploy-preview-128--your-site.netlify.app\u002F \\\n  --assert.preset=lighthouse:recommended\n",[253,35744,35745,35753,35760],{"__ignoreMap":712},[995,35746,35747,35749,35751],{"class":997,"line":998},[995,35748,16647],{"class":1007},[995,35750,16650],{"class":1023},[995,35752,3002],{"class":1010},[995,35754,35755,35758],{"class":997,"line":713},[995,35756,35757],{"class":1010},"  --collect.url=https:\u002F\u002Fdeploy-preview-128--your-site.netlify.app\u002F",[995,35759,3002],{"class":1010},[995,35761,35762],{"class":997,"line":730},[995,35763,19063],{"class":1010},[14,35765,35766,35767,239],{},"When the check fails, the pull request is blocked and nothing reaches production. The mechanics of wiring previews into branch protection, and tearing them down to control cost, are covered in ",[23,35768,28330],{"href":28329},[34,35770,35772],{"id":35771},"edge-delivery-caching-routing","Edge Delivery, Caching & Routing",[14,35774,35775],{},"Serving pre-built HTML well is mostly about cache headers and keeping routing in version control. Distribute through a CDN's points of presence to cut Time to First Byte, and use the two-tier cache policy so a deploy does not stampede your origin: fingerprinted assets are immutable for a year, HTML is short-lived.",[987,35777,35780],{"className":35778,"code":35779,"language":99,"meta":712},[11603],"\u002Fassets\u002F*\n  Cache-Control: public, max-age=31536000, immutable\n\u002F*.html\n  Cache-Control: public, max-age=0, s-maxage=300, stale-while-revalidate=86400\n",[253,35781,35779],{"__ignoreMap":712},[14,35783,35784,35786,35787,28603,35789,35791,35792,239],{},[253,35785,27321],{}," controls the shared edge cache while ",[253,35788,15059],{},[253,35790,14131],{}," lets the edge serve a slightly stale page while it refreshes in the background. This is safe only because SSGs emit content-hashed asset filenames, so caching the old URLs forever can never serve a wrong asset. The host-specific syntax and purge automation are in ",[23,35793,26988],{"href":26987},[14,35795,35796],{},"Keep routing and security headers next to the site, not in a dashboard, so they are reviewable. A Netlify example:",[987,35798,35800],{"className":2792,"code":35799,"language":2794,"meta":712,"style":712},"# netlify.toml\n[[redirects]]\n  from = \"\u002Fblog\u002F*\"\n  to = \"\u002Fposts\u002F:splat\"\n  status = 301\n  force = true\n\n[[headers]]\n  for = \"\u002F*\"\n  [headers.values]\n    X-Frame-Options = \"DENY\"\n    X-Content-Type-Options = \"nosniff\"\n",[253,35801,35802,35806,35811,35816,35821,35826,35831,35835,35840,35845,35850,35855],{"__ignoreMap":712},[995,35803,35804],{"class":997,"line":998},[995,35805,11326],{},[995,35807,35808],{"class":997,"line":713},[995,35809,35810],{},"[[redirects]]\n",[995,35812,35813],{"class":997,"line":730},[995,35814,35815],{},"  from = \"\u002Fblog\u002F*\"\n",[995,35817,35818],{"class":997,"line":1544},[995,35819,35820],{},"  to = \"\u002Fposts\u002F:splat\"\n",[995,35822,35823],{"class":997,"line":1550},[995,35824,35825],{},"  status = 301\n",[995,35827,35828],{"class":997,"line":1673},[995,35829,35830],{},"  force = true\n",[995,35832,35833],{"class":997,"line":1678},[995,35834,1541],{"emptyLinePlaceholder":752},[995,35836,35837],{"class":997,"line":1693},[995,35838,35839],{},"[[headers]]\n",[995,35841,35842],{"class":997,"line":1705},[995,35843,35844],{},"  for = \"\u002F*\"\n",[995,35846,35847],{"class":997,"line":1711},[995,35848,35849],{},"  [headers.values]\n",[995,35851,35852],{"class":997,"line":1717},[995,35853,35854],{},"    X-Frame-Options = \"DENY\"\n",[995,35856,35857],{"class":997,"line":1726},[995,35858,35859],{},"    X-Content-Type-Options = \"nosniff\"\n",[14,35861,35862],{},"Use explicit 301 redirects to preserve link equity through URL changes, and set security headers at the edge so every route gets them.",[34,35864,35866],{"id":35865},"security-compliance-hardening","Security & Compliance Hardening",[14,35868,35869,35870,35873],{},"Static output has a small attack surface, but the build pipeline does not. Treat build-time secrets carefully: anything injected into client-visible files — for example an Astro variable with the ",[253,35871,35872],{},"PUBLIC_"," prefix — ships to the browser. Keep tokens server-side, scope each one to the minimum the build needs, and never give a secret a client-exposed prefix.",[14,35875,35876,35877,270,35880,35883,35884,35886],{},"At the edge, enforce a Content Security Policy alongside ",[253,35878,35879],{},"X-Frame-Options",[253,35881,35882],{},"X-Content-Type-Options",". For third-party scripts you cannot drop, add Subresource Integrity hashes so a compromised CDN file fails closed instead of executing. The CI tokens that drive cache purges and deploys deserve the same discipline — scope a Cloudflare token to ",[253,35885,28730],{}," only, rather than handing it account-wide rights.",[34,35888,35890],{"id":35889},"promotion-atomic-deploys","Promotion & Atomic Deploys",[14,35892,35893],{},"Atomic deploys are the foundation of fast, safe releases. Each build becomes an immutable, versioned artifact, and promotion swaps the live version in a single pointer flip — visitors mid-request either get the old version fully or the new version fully, never a mix. There is no window where half the assets are updated.",[14,35895,35896,35897,35899],{},"Promotion strategy is usually one of three: deploy straight to production on merge to ",[253,35898,27507],{}," for fast-moving content sites, promote a previously built preview to production for teams that want a manual gate, or use a release branch that production tracks. Whichever you pick, the artifact that ran your checks on the preview URL should be the exact artifact you promote — rebuilding at promotion time reintroduces the non-determinism you worked to eliminate.",[34,35901,35903],{"id":35902},"rollback-incident-response","Rollback & Incident Response",[14,35905,35906],{},"Because each release is an immutable artifact, rolling back is just re-pointing at the previous build, which Cloudflare Pages, Netlify, and Vercel all do in seconds from the dashboard or a single CLI command. There is no partial state to clean up.",[14,35908,35909],{},"The cache policy is what makes instant rollback actually work. If HTML is cached long, users keep seeing the old release after you roll back, because their browser never revalidates:",[987,35911,35914],{"className":35912,"code":35913,"language":99,"meta":712},[11603],"\u002F*.html\n  Cache-Control: public, max-age=0, must-revalidate\n\u002Fassets\u002F*\n  Cache-Control: public, max-age=31536000, immutable\n",[253,35915,35913],{"__ignoreMap":712},[14,35917,35918,35919,35921],{},"Watch build logs, CDN error rates, and synthetic uptime checks so alerts fire before users feel an incident, and keep a short runbook for cache purges and DNS failover. When a Core Web Vital drops in field data, line it up against your deploy timeline — a sudden regression almost always maps to a specific release, which the ",[23,35920,5501],{"href":5500}," guide explains how to read.",[34,35923,2266],{"id":2265},[39,35925,35926,35935,35941,35948,35954],{},[42,35927,35928,35931,35932,35934],{},[229,35929,35930],{},"Cache stampede on deploy:"," invalidating the entire CDN at once can hammer your origin. Use atomic deploys plus ",[253,35933,14131],{}," so the edge serves slightly-stale content while it revalidates in the background.",[42,35936,35937,35940],{},[229,35938,35939],{},"Environment variable leakage:"," secrets injected into client bundles are public. Keep them server-side and never give a secret a client-exposed prefix.",[42,35942,35943,35945,35946,20603],{},[229,35944,20599],{}," caching HTML aggressively serves stale content and silently breaks rollbacks. Keep HTML short-lived; reserve ",[253,35947,11756],{},[42,35949,35950,35953],{},[229,35951,35952],{},"Rebuilding at promotion time:"," building again to promote reintroduces non-determinism. Promote the exact artifact that passed your checks.",[42,35955,35956,35959],{},[229,35957,35958],{},"Broken caches across runners:"," a stale or corrupted CI cache produces missing pages. Use explicit cache keys tied to the lockfile and validate the output with a link check before promoting.",[34,35961,2321],{"id":2320},[39,35963,35964,35970,35973,35976,35979],{},[42,35965,35966,35967,35969],{},"Reproducible builds start with ",[253,35968,2072],{}," and a pinned runtime — the same commit must produce the same artifact anywhere.",[42,35971,35972],{},"Give every pull request a preview deploy and run your link, accessibility, and performance checks against it before merge.",[42,35974,35975],{},"Promote the exact artifact that passed your checks; never rebuild at promotion time.",[42,35977,35978],{},"Use the two-tier cache policy — immutable hashed assets, short-lived HTML — so rollback is instant and repeat visits are free.",[42,35980,35981],{},"Keep routing, headers, and secrets in version control and scoped to the minimum, so deploys are reviewable and reversible.",[34,35983,651],{"id":650},[653,35985,35987],{"id":35986},"how-do-i-handle-dynamic-content-in-a-static-cicd-pipeline","How do I handle dynamic content in a static CI\u002FCD pipeline?",[14,35989,35990],{},"Decouple it from the build. Push genuinely dynamic data to edge or serverless functions, use incremental regeneration where your host supports it, or fetch from a cached API on the client. The static build stays deterministic while the dynamic parts live at the edge.",[653,35992,35994],{"id":35993},"what-is-the-optimal-cache-ttl-for-ssg-deployments","What is the optimal cache TTL for SSG deployments?",[14,35996,35997,35998,36000,36001,36003,36004,36006,36007,36009],{},"Use a two-tier policy. Cache fingerprinted assets for one year as ",[253,35999,11756],{},", and keep HTML short-lived with ",[253,36002,15059],{}," or a small ",[253,36005,27321],{}," paired with ",[253,36008,14131],{},". This keeps rollbacks instant while making repeat visits nearly free.",[653,36011,36013],{"id":36012},"how-can-i-prevent-broken-builds-from-reaching-production","How can I prevent broken builds from reaching production?",[14,36015,36016],{},"Gate merges on branch protection, a required preview deploy, automated link checking, and a performance budget. Run those checks on the pull request so a broken build fails before it ever promotes to the production branch.",[653,36018,36020],{"id":36019},"what-makes-a-deploy-atomic-and-why-does-it-matter","What makes a deploy atomic and why does it matter?",[14,36022,36023],{},"An atomic deploy publishes an immutable versioned artifact and flips the live pointer in one step, so users never see a half-written release. It also makes rollback a one-step re-point at the previous version instead of a cleanup operation.",[653,36025,36027],{"id":36026},"should-i-build-on-my-host-or-in-github-actions","Should I build on my host or in GitHub Actions?",[14,36029,36030],{},"Build in GitHub Actions when you need custom steps, matrix testing, or shared caching across jobs, then deploy the artifact. Build on the host when you want the simplest possible Git-push workflow and the host's native build covers your needs.",[653,36032,36034],{"id":36033},"how-fast-can-i-roll-back-a-bad-release","How fast can I roll back a bad release?",[14,36036,36037],{},"On Cloudflare Pages, Netlify, and Vercel a rollback is selecting a previous deployment or running one CLI command, and it takes seconds because each deploy is an immutable artifact. The only thing that slows it down is HTML cached too aggressively.",[34,36039,684],{"id":683},[39,36041,36042,36050,36057,36065,36070,36075,36080],{},[42,36043,36044,692,36047,36049],{},[229,36045,36046],{},"Sibling guide:",[23,36048,31],{"href":30}," — the framework decision that shapes your build.",[42,36051,36052,692,36054,36056],{},[229,36053,36046],{},[23,36055,5501],{"href":5500}," — the runtime metrics your pipeline protects.",[42,36058,36059,36061,36062,36064],{},[23,36060,26988],{"href":26987}," — control the global edge with a ",[253,36063,14036],{}," file.",[42,36066,36067,36069],{},[23,36068,28200],{"href":28199}," — reproducible builds and artifacts.",[42,36071,36072,36074],{},[23,36073,28797],{"href":28796}," — pick the host that fits your stack.",[42,36076,36077,36079],{},[23,36078,28330],{"href":28329}," — a real deploy URL for every PR.",[42,36081,36082,36084],{},[23,36083,32455],{"href":32454}," — keep build time flat as the site grows.\n\n",[1346,36086,36087],{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}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 .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .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}",{"title":712,"searchDepth":713,"depth":713,"links":36089},[36090,36091,36092,36093,36094,36095,36096,36097,36098,36099,36100,36101,36109],{"id":5446,"depth":713,"text":5447},{"id":35456,"depth":713,"text":35457},{"id":35479,"depth":713,"text":35480},{"id":35658,"depth":713,"text":35659},{"id":35732,"depth":713,"text":35733},{"id":35771,"depth":713,"text":35772},{"id":35865,"depth":713,"text":35866},{"id":35889,"depth":713,"text":35890},{"id":35902,"depth":713,"text":35903},{"id":2265,"depth":713,"text":2266},{"id":2320,"depth":713,"text":2321},{"id":650,"depth":713,"text":651,"children":36102},[36103,36104,36105,36106,36107,36108],{"id":35986,"depth":730,"text":35987},{"id":35993,"depth":730,"text":35994},{"id":36012,"depth":730,"text":36013},{"id":36019,"depth":730,"text":36020},{"id":36026,"depth":730,"text":36027},{"id":36033,"depth":730,"text":36034},{"id":683,"depth":713,"text":684},[36111,36112],{"name":737,"item":738},{"name":5505,"item":5504},"Ship static sites with confidence: reproducible builds, preview-per-PR, atomic promotion, instant rollback, and edge caching across Cloudflare Pages, Netlify, Vercel, and GitHub Actions.",[36115,36116,36118,36119,36120,36121],{"q":35987,"a":35990},{"q":35994,"a":36117},"Use a two-tier policy. Cache fingerprinted assets for one year as immutable, and keep HTML short-lived with max-age zero or a small s-maxage paired with stale-while-revalidate. This keeps rollbacks instant while making repeat visits nearly free.",{"q":36013,"a":36016},{"q":36020,"a":36023},{"q":36027,"a":36030},{"q":36034,"a":36037},{},"\u002Fproduction-ready-deployment-cicd-workflows",{"title":35241,"description":36113},"production-ready-deployment-cicd-workflows\u002Findex","JU2BC0rjm8IQkJzevAgglCR8w_MuG7TWLbr06zE5rCY",{"id":36128,"title":28797,"body":36129,"breadcrumb":36865,"dateModified":743,"datePublished":2446,"description":36869,"extension":745,"faq":36870,"meta":36877,"navigation":752,"path":36878,"seo":36879,"slug":36133,"stem":36880,"type":2460,"__hash__":36881},"content\u002Fproduction-ready-deployment-cicd-workflows\u002Fnetlify-vs-vercel-deployment-strategies\u002Findex.md",{"type":7,"value":36130,"toc":36846},[36131,36134,36142,36255,36259,36262,36271,36317,36330,36424,36434,36511,36515,36532,36538,36581,36585,36588,36598,36604,36608,36615,36627,36631,36637,36658,36664,36668,36671,36674,36676,36741,36743,36768,36770,36774,36777,36781,36784,36788,36791,36795,36798,36802,36805,36809,36812,36814,36843],[10,36132,28797],{"id":36133},"netlify-vs-vercel-deployment-strategies",[14,36135,36136,36137,36139,36140,239],{},"For a static site, Netlify and Vercel are more alike than different: both connect to Git, build on push, hand you a preview URL per pull request, and serve the output from a global edge network. The real differences show up in configuration style, build-cache behavior, edge-function ergonomics, and whether you ever touch dynamic rendering like Incremental Static Regeneration (ISR). This guide compares the two for static site generators specifically — Astro, Eleventy, Hugo, and Jekyll — so you can pick on workflow fit rather than marketing. It sits inside ",[23,36138,5505],{"href":5504},", alongside the host-specific ",[23,36141,26988],{"href":26987},[55,36143,36144,36252],{},[58,36145,66,36149,66,36152,66,36155,66,36245],{"viewBox":13872,"role":61,"ariaLabelledBy":36146,"xmlns":65},[36147,36148],"nv-cmp-title","nv-cmp-desc",[68,36150,36151],{"id":36147},"Netlify versus Vercel across five deployment axes",[72,36153,36154],{"id":36148},"A side-by-side comparison of Netlify and Vercel across build configuration, deploy previews, edge functions, rendering model (ISR versus pure static), and pricing basis, with a shared Git push as the common entry point.",[95,36156,78,36157,78,36160,78,36162,78,36165,78,36169,78,36172,78,36174,78,36177,78,36179,78,36182,78,36185,78,36188,78,36190,78,36192,78,36195,78,36199,78,36201,78,36204,78,36207,78,36210,78,36212,78,36215,78,36219,78,36223,78,36225,78,36228,78,36230,78,36233,78,36237,78,36239,78,36242,66],{"style":813},[99,36158,36159],{"x":1415,"y":109,"fill":103,"style":1416},"Same Git push, two deployment platforms",[107,36161],{"x":874,"y":2595,"width":7852,"height":33253,"rx":3579,"fill":114,"opacity":186,"stroke":114,"style":116},[99,36163,36164],{"x":1415,"y":828,"fill":114,"style":121},"git push \u002F open PR",[90,36166],{"d":36167,"stroke":93,"fill":205,"style":36168},"M360 96 L250 130","stroke-width:2px;marker-end:url(#nv-arrow)",[90,36170],{"d":36171,"stroke":93,"fill":205,"style":36168},"M460 96 L570 130",[107,36173],{"x":3578,"y":18413,"width":6144,"height":112,"rx":113,"fill":824,"opacity":6172,"stroke":824,"style":116},[99,36175,36176],{"x":3500,"y":18406,"fill":824,"style":104},"Netlify",[107,36178],{"x":6171,"y":18413,"width":6144,"height":112,"rx":113,"fill":185,"opacity":115,"stroke":187,"style":116},[99,36180,36181],{"x":6175,"y":18406,"fill":187,"style":104},"Vercel",[99,36183,36184],{"x":110,"y":12795,"fill":93,"style":11900},"CONFIG",[99,36186,36187],{"x":110,"y":20123,"fill":103,"style":3567},"netlify.toml + _headers",[99,36189,36184],{"x":11991,"y":12795,"fill":93,"style":11900},[99,36191,14260],{"x":11991,"y":20123,"fill":103,"style":3567},[99,36193,36194],{"x":110,"y":4674,"fill":93,"style":11900},"PREVIEWS",[99,36196,36198],{"x":110,"y":36197,"fill":103,"style":3567},"265","deploy preview per PR",[99,36200,36194],{"x":11991,"y":4674,"fill":93,"style":11900},[99,36202,36203],{"x":11991,"y":36197,"fill":103,"style":3567},"preview deploy per PR",[99,36205,36206],{"x":110,"y":5421,"fill":93,"style":11900},"EDGE",[99,36208,36209],{"x":110,"y":23265,"fill":103,"style":3567},"Edge Functions (Deno)",[99,36211,36206],{"x":11991,"y":5421,"fill":93,"style":11900},[99,36213,36214],{"x":11991,"y":23265,"fill":103,"style":3567},"Edge Functions + ISR",[99,36216,36218],{"x":110,"y":36217,"fill":93,"style":11900},"346","RENDERING",[99,36220,36222],{"x":110,"y":36221,"fill":103,"style":3567},"365","pure static + purge",[99,36224,36218],{"x":11991,"y":36217,"fill":93,"style":11900},[99,36226,36227],{"x":11991,"y":36221,"fill":103,"style":3567},"static or ISR (Next.js)",[107,36229],{"x":112,"y":142,"width":7852,"height":3578,"rx":3579,"fill":162,"opacity":877,"stroke":164,"style":116},[99,36231,36232],{"x":874,"y":17845,"fill":103,"style":882},"PRICING BASIS",[99,36234,36236],{"x":874,"y":36235,"fill":93,"style":4658},"234","bandwidth + build min",[107,36238],{"x":190,"y":142,"width":1431,"height":3578,"rx":3579,"fill":2564,"opacity":825,"stroke":2565,"style":116},[99,36240,36241],{"x":23289,"y":17845,"fill":103,"style":882},"PRICING",[99,36243,36244],{"x":23289,"y":36235,"fill":93,"style":4658},"seats + usage",[76,36246,78,36247,66],{},[80,36248,88,36250,78],{"id":36249,"viewBox":83,"refX":84,"refY":85,"markerWidth":86,"markerHeight":86,"orient":87},"nv-arrow",[90,36251],{"d":92,"fill":93},[218,36253,36254],{},"Both platforms start from the same Git push and diverge on config syntax, edge runtime, and whether you ever leave pure-static rendering for ISR.",[34,36256,36258],{"id":36257},"configuration-style","Configuration Style",[14,36260,36261],{},"Both platforms keep deploy config in the repository, which is exactly where it belongs — reviewable, versioned, and identical across every environment. The syntax differs, but the shape is the same: a build command, an output directory, and cache headers.",[14,36263,36264,36265,36267,36268,36270],{},"Netlify uses ",[253,36266,15754],{}," at the repo root, and reads a ",[253,36269,14036],{}," file from the publish directory for fine-grained cache rules:",[987,36272,36274],{"className":2792,"code":36273,"language":2794,"meta":712,"style":712},"[build]\n  command = \"npm run build\"\n  publish = \"public\"      # _site for Eleventy\u002FJekyll, dist for Astro\n\n[build.environment]\n  NODE_VERSION = \"22\"\n\n[[plugins]]\n  package = \"@netlify\u002Fplugin-lighthouse\"\n",[253,36275,36276,36280,36284,36289,36293,36298,36303,36307,36312],{"__ignoreMap":712},[995,36277,36278],{"class":997,"line":998},[995,36279,11331],{},[995,36281,36282],{"class":997,"line":713},[995,36283,11341],{},[995,36285,36286],{"class":997,"line":730},[995,36287,36288],{},"  publish = \"public\"      # _site for Eleventy\u002FJekyll, dist for Astro\n",[995,36290,36291],{"class":997,"line":1544},[995,36292,1541],{"emptyLinePlaceholder":752},[995,36294,36295],{"class":997,"line":1550},[995,36296,36297],{},"[build.environment]\n",[995,36299,36300],{"class":997,"line":1673},[995,36301,36302],{},"  NODE_VERSION = \"22\"\n",[995,36304,36305],{"class":997,"line":1678},[995,36306,1541],{"emptyLinePlaceholder":752},[995,36308,36309],{"class":997,"line":1693},[995,36310,36311],{},"[[plugins]]\n",[995,36313,36314],{"class":997,"line":1705},[995,36315,36316],{},"  package = \"@netlify\u002Fplugin-lighthouse\"\n",[14,36318,36319,36320,36322,36323,36326,36327,36329],{},"Vercel uses a single ",[253,36321,14260],{},". Set ",[253,36324,36325],{},"outputDirectory"," to your generator's real output directory — not ",[253,36328,34514],{},", which is Next.js-specific and wrong for any other SSG:",[987,36331,36333],{"className":14263,"code":36332,"language":14265,"meta":712,"style":712},"{\n  \"buildCommand\": \"npm run build\",\n  \"outputDirectory\": \"dist\",\n  \"headers\": [\n    {\n      \"source\": \"\u002Fassets\u002F(.*)\",\n      \"headers\": [\n        { \"key\": \"Cache-Control\", \"value\": \"public, max-age=31536000, immutable\" }\n      ]\n    }\n  ]\n}\n",[253,36334,36335,36339,36351,36362,36368,36372,36382,36388,36408,36412,36416,36420],{"__ignoreMap":712},[995,36336,36337],{"class":997,"line":998},[995,36338,14272],{"class":1618},[995,36340,36341,36344,36346,36349],{"class":997,"line":713},[995,36342,36343],{"class":1010},"  \"buildCommand\"",[995,36345,1925],{"class":1618},[995,36347,36348],{"class":1023},"\"npm run build\"",[995,36350,2885],{"class":1618},[995,36352,36353,36356,36358,36360],{"class":997,"line":730},[995,36354,36355],{"class":1010},"  \"outputDirectory\"",[995,36357,1925],{"class":1618},[995,36359,5867],{"class":1023},[995,36361,2885],{"class":1618},[995,36363,36364,36366],{"class":997,"line":1544},[995,36365,14277],{"class":1010},[995,36367,14280],{"class":1618},[995,36369,36370],{"class":997,"line":1550},[995,36371,14285],{"class":1618},[995,36373,36374,36376,36378,36380],{"class":997,"line":1673},[995,36375,14290],{"class":1010},[995,36377,1925],{"class":1618},[995,36379,14295],{"class":1023},[995,36381,2885],{"class":1618},[995,36383,36384,36386],{"class":997,"line":1678},[995,36385,14302],{"class":1010},[995,36387,14280],{"class":1618},[995,36389,36390,36392,36394,36396,36398,36400,36402,36404,36406],{"class":997,"line":1693},[995,36391,14309],{"class":1618},[995,36393,14312],{"class":1010},[995,36395,1925],{"class":1618},[995,36397,14317],{"class":1023},[995,36399,1850],{"class":1618},[995,36401,14322],{"class":1010},[995,36403,1925],{"class":1618},[995,36405,14327],{"class":1023},[995,36407,7475],{"class":1618},[995,36409,36410],{"class":997,"line":1705},[995,36411,14334],{"class":1618},[995,36413,36414],{"class":997,"line":1711},[995,36415,14395],{"class":1618},[995,36417,36418],{"class":997,"line":1717},[995,36419,14400],{"class":1618},[995,36421,36422],{"class":997,"line":1726},[995,36423,9008],{"class":1618},[14,36425,36426,36427,36430,36431,36433],{},"Netlify's ",[253,36428,36429],{},"[[plugins]]"," block is the bigger ergonomic difference: its build-plugin ecosystem (Lighthouse, sitemap, image CDN, a11y checks) drops capabilities into the build with one line, whereas on Vercel you wire equivalents as explicit build steps. If you want platform-agnostic builds from day one, run ",[23,36432,28200],{"href":28199}," and deploy to either platform by CLI, so neither dashboard owns your pipeline.",[433,36435,36436,36446],{},[436,36437,36438],{},[439,36439,36440,36442,36444],{},[442,36441,8401],{},[442,36443,36176],{},[442,36445,36181],{},[457,36447,36448,36461,36477,36490,36500],{},[439,36449,36450,36453,36457],{},[462,36451,36452],{},"Primary config file",[462,36454,36455],{},[253,36456,15754],{},[462,36458,36459],{},[253,36460,14260],{},[439,36462,36463,36466,36471],{},[462,36464,36465],{},"Cache-header file",[462,36467,36468,36470],{},[253,36469,14036],{}," (in publish dir)",[462,36472,36473,14733,36475],{},[253,36474,5691],{},[253,36476,14260],{},[439,36478,36479,36482,36487],{},[462,36480,36481],{},"Build extensions",[462,36483,36484,36486],{},[253,36485,36429],{}," marketplace",[462,36488,36489],{},"explicit build steps",[439,36491,36492,36495,36497],{},[462,36493,36494],{},"Edge runtime",[462,36496,36209],{},[462,36498,36499],{},"Edge Functions (Edge runtime)",[439,36501,36502,36505,36508],{},[462,36503,36504],{},"Dynamic rendering",[462,36506,36507],{},"none (rebuild + purge)",[462,36509,36510],{},"ISR for Next.js",[34,36512,36514],{"id":36513},"build-caching","Build Caching",[14,36516,36517,36518,36520,36521,36523,36524,29422,36526,36528,36529,36531],{},"Both platforms persist a build cache between deploys to cut cold-build time, and both reward a committed lockfile. Use ",[253,36519,2072],{}," for a lockfile-consistent install, and let each platform restore ",[253,36522,417],{}," and your framework cache (",[253,36525,3170],{},[253,36527,4602],{},", Eleventy's ",[253,36530,34048],{},") from the previous deploy.",[14,36533,36534,36535,36537],{},"The gain is real but project-specific — it scales with dependency count and image processing, not the platform logo. On a 1,200-page Hugo documentation site we measured a cold build at roughly 2m10s and a warm, cache-restored build at 41s on Netlify and 38s on Vercel: effectively identical and both dominated by image processing rather than platform overhead. Measure your own before\u002Fafter rather than assuming a fixed percentage. The same caching discipline drives ",[23,36536,1049],{"href":1048}," if you move the build off-platform.",[433,36539,36540,36551],{},[436,36541,36542],{},[439,36543,36544,36547,36549],{},[442,36545,36546],{},"Build type",[442,36548,36176],{},[442,36550,36181],{},[457,36552,36553,36563,36572],{},[439,36554,36555,36557,36560],{},[462,36556,5040],{},[462,36558,36559],{},"~2m10s",[462,36561,36562],{},"~2m05s",[439,36564,36565,36568,36570],{},[462,36566,36567],{},"Warm (cache restored)",[462,36569,9435],{},[462,36571,2075],{},[439,36573,36574,36577,36579],{},[462,36575,36576],{},"Dominant cost",[462,36578,4684],{},[462,36580,4684],{},[34,36582,36584],{"id":36583},"deploy-previews","Deploy Previews",[14,36586,36587],{},"This is where both platforms earn their keep, and where they are most alike. Connect the Git integration once and every pull request gets its own immutable preview URL built from that branch's exact code — no extra configuration. Reviewers click a link and see the real, deployed site instead of imagining the diff.",[14,36589,36590,36591,36593,36594,239],{},"The discipline that makes previews valuable is gating merges on them. Add branch protection and a required check — typically a Lighthouse run or a link check against the preview URL — so a regression fails the PR rather than reaching production. The full pattern, including how to scope preview environment variables so a preview never inherits production secrets, lives in ",[23,36592,28330],{"href":28329},". On Netlify, the specific recipe for wiring this up is in ",[23,36595,36597],{"href":36596},"\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",[14,36599,36600,36601,36603],{},"One operational note: preview contexts can inherit production secrets if you are not careful. On Netlify scope variables to the ",[253,36602,7320],{}," context explicitly; on Vercel choose which environments each variable applies to. A leaked analytics or CMS token in a public preview URL is a real incident, not a hypothetical.",[34,36605,36607],{"id":36606},"content-triggers-webhooks","Content Triggers & Webhooks",[14,36609,36610,36611,36614],{},"A static site does not need to rebuild on every CMS edit, but it does need to rebuild on ",[18,36612,36613],{},"some"," of them. Both platforms expose build hooks — a secret URL that triggers a deploy when something POSTs to it — so a headless CMS can publish content without a Git push.",[14,36616,36617,36618,270,36621,36624,36625,239],{},"The pattern is identical on both: create the hook, verify the incoming webhook signature so only your CMS can trigger it, then forward to the hook. Verifying the signature matters because the hook URL alone is a bearer credential — anyone who has it can spend your build minutes. The concrete Netlify recipe, including how to pass ",[253,36619,36620],{},"clear_cache",[253,36622,36623],{},"trigger_branch"," as query parameters and how long a triggered build actually takes, is in ",[23,36626,27609],{"href":27608},[34,36628,36630],{"id":36629},"edge-delivery-rendering-isr-vs-pure-static","Edge Delivery & Rendering: ISR vs Pure Static",[14,36632,36633,36634,36636],{},"This is the one axis where the platforms genuinely diverge for SSG users, and it is mostly about a feature you probably do not need. For a static generator you serve pre-built HTML and control freshness with cache headers and rebuilds — the two-tier policy of ",[253,36635,11756],{}," hashed assets plus short-lived HTML, which both platforms honor.",[14,36638,36639,36640,36643,36644,36647,36648,270,36650,36653,36654,239],{},"Vercel adds Incremental Static Regeneration, which lets individual pages regenerate in the background after a ",[253,36641,36642],{},"revalidate"," window without a full rebuild. The critical caveat: ",[229,36645,36646],{},"ISR is a Next.js server feature and requires a serverless runtime. Astro static, Eleventy, Hugo, and Jekyll have no ISR"," — they rebuild and purge at the CDN. So ISR is only a deciding factor if you are on Next.js (or an Astro server adapter) with many frequently-changing pages. The full trade-off, with the ",[253,36649,36642],{},[253,36651,36652],{},"revalidatePath"," mechanics, is in ",[23,36655,36657],{"href":36656},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fnetlify-vs-vercel-deployment-strategies\u002Fvercel-isr-vs-static-generation-for-ssgs\u002F","Vercel ISR vs Static Generation for SSGs",[14,36659,36660,36661,36663],{},"For genuinely dynamic edges — redirects, A\u002FB tests, geolocation, request-time auth — both offer Edge Functions. Netlify's run on Deno; Vercel's run on its Edge runtime. Both execute close to the user with single-digit-millisecond cold starts and are the right tool for the small dynamic slice of an otherwise-static site. If you want a third option to weigh against both, ",[23,36662,26988],{"href":26987}," covers the Workers-based equivalent and its caching model.",[34,36665,36667],{"id":36666},"pricing-lock-in","Pricing & Lock-In",[14,36669,36670],{},"For a purely static site, cost is driven by bandwidth and build minutes, not compute, and both free tiers are generous enough to run a real documentation site. Netlify meters bandwidth and build minutes directly; Vercel layers a per-seat plan on top of usage. A high-traffic static site usually hits a bandwidth bill before anything else on either platform — which is an argument for a strong cache policy regardless of host, since every cache HIT is bandwidth you do not re-serve from origin.",[14,36672,36673],{},"Lock-in is low if you keep the portable parts portable: build command, output directory, redirects, and cache headers all live in version control. The platform-specific surface — build plugins on Netlify, ISR on Vercel — is exactly what you should keep off the critical path if portability matters. Build once in CI and deploy the artifact to whichever platform, and switching is a config change rather than a migration.",[34,36675,2266],{"id":2265},[39,36677,36678,36687,36704,36718,36735],{},[42,36679,36680,36686],{},[229,36681,36682,36683,36685],{},"Skipping ",[253,36684,2072],{}," \u002F the platform cache:"," forces full dependency resolution every build. Commit a lockfile and rely on the restored build cache; a cold build can be 3x a warm one.",[42,36688,36689,36692,36693,3725,36695,36697,36698,36700,36701,36703],{},[229,36690,36691],{},"ISR vs CDN cache conflicts:"," a custom ",[253,36694,27321],{},[253,36696,14260],{}," can override the framework's ",[253,36699,36642],{},", serving stale content. Align the windows or let the framework manage caching, and use ",[253,36702,15633],{}," only for genuinely dynamic routes.",[42,36705,36706,36709,36710,2338,36712,738,36715,36717],{},[229,36707,36708],{},"Secret leakage to previews:"," preview contexts can inherit production secrets. Scope variables explicitly to ",[253,36711,7320],{},[253,36713,36714],{},"preview",[253,36716,36714],{}," environments, never blanket-share.",[42,36719,36720,692,36723,36725,36726,36728,36729,36731,36732,36734],{},[229,36721,36722],{},"Wrong output directory on Vercel:",[253,36724,36325],{}," defaults assume Next.js. Set it to ",[253,36727,2242],{}," (Astro), ",[253,36730,14988],{}," (Hugo), or ",[253,36733,2245],{}," (Eleventy\u002FJekyll) or you ship an empty site.",[42,36736,36737,36740],{},[229,36738,36739],{},"Designing around ISR you do not have:"," Hugo, Eleventy, and Jekyll cannot regenerate single pages on demand. Rebuild-and-purge is the model; do not architect for ISR on a pure SSG.",[34,36742,2321],{"id":2320},[39,36744,36745,36748,36759,36762,36765],{},[42,36746,36747],{},"For a purely static SSG, pick on workflow fit, not raw speed — warm build times are within the noise on both.",[42,36749,36750,36751,10331,36753,36755,36756,36758],{},"Keep config in the repo: ",[253,36752,15754],{},[253,36754,14036],{}," on Netlify, ",[253,36757,14260],{}," on Vercel, and a real output directory either way.",[42,36760,36761],{},"Deploy previews per PR are the shared superpower; gate merges on a preview Lighthouse or link check.",[42,36763,36764],{},"ISR is a Next.js-only reason to choose Vercel — irrelevant to Astro static, Eleventy, Hugo, and Jekyll, which rebuild and purge.",[42,36766,36767],{},"Minimize lock-in by building in CI and deploying the artifact, so the platform is a target, not a dependency.",[34,36769,651],{"id":650},[653,36771,36773],{"id":36772},"which-is-faster-to-build-large-ssg-projects-on","Which is faster to build large SSG projects on?",[14,36775,36776],{},"Close in practice; both lean heavily on build caching, so the deciding factor is your dependency count and image processing rather than the platform. On our 1,200-page Hugo site a warm build landed at 41s on Netlify and 38s on Vercel — inside the noise. Benchmark your own project rather than trusting a generic number.",[653,36778,36780],{"id":36779},"how-do-i-get-pr-previews-on-both-netlify-and-vercel","How do I get PR previews on both Netlify and Vercel?",[14,36782,36783],{},"Both auto-generate a unique preview URL per pull request once the Git integration is connected; no extra config is needed. Add branch protection and block merges until a preview Lighthouse check passes so a regression cannot reach production.",[653,36785,36787],{"id":36786},"do-i-need-vercel-isr-for-a-static-site-generator","Do I need Vercel ISR for a static site generator?",[14,36789,36790],{},"No. ISR is a Next.js server feature and requires a serverless runtime. Astro static, Eleventy, Hugo, and Jekyll have no ISR — they rebuild and purge at the CDN. Only reach for ISR if you have many frequently-changing pages and are already on a server framework.",[653,36792,36794],{"id":36793},"can-i-migrate-from-netlify-to-vercel-without-downtime","Can I migrate from Netlify to Vercel without downtime?",[14,36796,36797],{},"Yes. Lower your DNS TTL a day ahead, deploy the same build artifact to both platforms, validate the Vercel deploy on its preview domain, then switch DNS. Keep both live until propagation completes, then decommission the old one.",[653,36799,36801],{"id":36800},"which-platform-is-cheaper-for-a-static-documentation-site","Which platform is cheaper for a static documentation site?",[14,36803,36804],{},"For a purely static site both free tiers are generous, and cost is driven by bandwidth and build minutes rather than compute. Vercel meters on a per-seat plus usage model; Netlify meters bandwidth and build minutes. A high-traffic static site usually pays for bandwidth first on either.",[653,36806,36808],{"id":36807},"how-do-i-keep-deploy-config-portable-between-the-two","How do I keep deploy config portable between the two?",[14,36810,36811],{},"Keep the build command, output directory, and cache headers in version control, and avoid platform-only features on the critical path. Run the same build in GitHub Actions and deploy to either platform via CLI so you are never locked to one vendor's dashboard.",[34,36813,684],{"id":683},[39,36815,36816,36823,36828,36833,36838],{},[42,36817,36818,692,36820,36822],{},[229,36819,691],{},[23,36821,5505],{"href":5504}," — where platform choice fits the full pipeline.",[42,36824,36825,36827],{},[23,36826,27609],{"href":27608}," — rebuild from a CMS without a Git push.",[42,36829,36830,36832],{},[23,36831,36657],{"href":36656}," — the one rendering axis where the platforms diverge.",[42,36834,36835,36837],{},[23,36836,36597],{"href":36596}," — the per-PR preview recipe.",[42,36839,36840,36842],{},[23,36841,26988],{"href":26987}," — a third host to weigh against both.",[1346,36844,36845],{},"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 .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}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}",{"title":712,"searchDepth":713,"depth":713,"links":36847},[36848,36849,36850,36851,36852,36853,36854,36855,36856,36864],{"id":36257,"depth":713,"text":36258},{"id":36513,"depth":713,"text":36514},{"id":36583,"depth":713,"text":36584},{"id":36606,"depth":713,"text":36607},{"id":36629,"depth":713,"text":36630},{"id":36666,"depth":713,"text":36667},{"id":2265,"depth":713,"text":2266},{"id":2320,"depth":713,"text":2321},{"id":650,"depth":713,"text":651,"children":36857},[36858,36859,36860,36861,36862,36863],{"id":36772,"depth":730,"text":36773},{"id":36779,"depth":730,"text":36780},{"id":36786,"depth":730,"text":36787},{"id":36793,"depth":730,"text":36794},{"id":36800,"depth":730,"text":36801},{"id":36807,"depth":730,"text":36808},{"id":683,"depth":713,"text":684},[36866,36867,36868],{"name":737,"item":738},{"name":5505,"item":5504},{"name":28797,"item":28796},"Compare Netlify and Vercel for SSG deployments — configuration style, build-cache behavior, deploy previews, edge functions, ISR vs pure static, and pricing.",[36871,36872,36873,36874,36875,36876],{"q":36773,"a":36776},{"q":36780,"a":36783},{"q":36787,"a":36790},{"q":36794,"a":36797},{"q":36801,"a":36804},{"q":36808,"a":36811},{},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fnetlify-vs-vercel-deployment-strategies",{"title":28797,"description":36869},"production-ready-deployment-cicd-workflows\u002Fnetlify-vs-vercel-deployment-strategies\u002Findex","qQQUomC2Gfp-J2C8MIKjvSrcdNvvJy9jwMccMNXGHWE",{"id":36883,"title":27609,"body":36884,"breadcrumb":37390,"dateModified":743,"datePublished":2446,"description":37395,"extension":745,"faq":37396,"meta":37404,"navigation":752,"path":37405,"seo":37406,"slug":36888,"stem":37407,"type":756,"__hash__":37408},"content\u002Fproduction-ready-deployment-cicd-workflows\u002Fnetlify-vs-vercel-deployment-strategies\u002Fnetlify-build-hooks-for-content-updates\u002Findex.md",{"type":7,"value":36885,"toc":37373},[36886,36889,36900,36902,36918,37019,37023,37037,37061,37070,37086,37093,37097,37107,37139,37149,37153,37156,37230,37237,37241,37275,37279,37282,37284,37297,37299,37303,37306,37310,37316,37320,37323,37327,37338,37342,37345,37347,37371],[10,36887,27609],{"id":36888},"netlify-build-hooks-for-content-updates",[14,36890,36891,36892,36895,36896,33652,36898,239],{},"A Netlify build hook is a unique URL that triggers a deploy when something POSTs to it — the standard way to rebuild a static site when a headless CMS publishes new content, without a Git push. An editor clicks ",[18,36893,36894],{},"Publish",", the CMS fires a webhook, Netlify builds the site, and the new content is live a minute or two later with no developer in the loop. This guide covers creating, securing, timing, and debugging hooks. It sits under ",[23,36897,28797],{"href":28796},[23,36899,5505],{"href":5504},[34,36901,37],{"id":36},[39,36903,36904,36907,36910,36915],{},[42,36905,36906],{},"A site already deploying to Netlify from Astro, Hugo, Eleventy, or Jekyll, building cleanly from the dashboard.",[42,36908,36909],{},"A headless CMS (Contentful, Sanity, Storyblok, a Git-backed CMS, etc.) that can fire an outbound webhook on publish.",[42,36911,36912,36914],{},[253,36913,14076],{}," available locally to test the hook before wiring up the CMS.",[42,36916,36917],{},"A place to run a small proxy function (Netlify Functions, a Worker, or any serverless runtime) if you want signature verification — recommended for production.",[55,36919,36920,37016],{},[58,36921,66,36925,66,36928,66,36931,66,37009],{"viewBox":21471,"role":61,"ariaLabelledBy":36922,"xmlns":65},[36923,36924],"bh-flow-title","bh-flow-desc",[68,36926,36927],{"id":36923},"Build-hook trigger flow from CMS publish to live deploy",[72,36929,36930],{"id":36924},"An editor publishes in a CMS, which fires a signed webhook to a verifying proxy, which forwards to the Netlify build hook; Netlify rebuilds the static site and atomically publishes the new deploy to the edge, with approximate timings on each step.",[95,36932,78,36933,78,36936,78,36938,78,36941,78,36944,78,36947,78,36949,78,36952,78,36955,78,36958,78,36960,78,36963,78,36966,78,36969,78,36971,78,36974,78,36977,78,36980,78,36982,78,36985,78,36988,78,36991,78,37006,66],{"style":97},[99,36934,36935],{"x":101,"y":102,"fill":103,"style":104},"CMS publish to live page, end to end",[107,36937],{"x":5393,"y":159,"width":1431,"height":1430,"rx":823,"fill":114,"opacity":186,"stroke":114,"style":116},[99,36939,36940],{"x":1430,"y":5379,"fill":114,"style":121},"CMS publish",[99,36942,36943],{"x":1430,"y":18406,"fill":93,"style":4658},"editor clicks",[99,36945,36946],{"x":1430,"y":138,"fill":93,"style":4658},"~0s",[107,36948],{"x":160,"y":159,"width":1431,"height":1430,"rx":823,"fill":162,"opacity":877,"stroke":164,"style":116},[99,36950,36951],{"x":184,"y":5379,"fill":103,"style":121},"verify proxy",[99,36953,36954],{"x":184,"y":18406,"fill":93,"style":4658},"check signature",[99,36956,36957],{"x":184,"y":138,"fill":93,"style":4658},"~50ms",[107,36959],{"x":6144,"y":159,"width":1431,"height":1430,"rx":823,"fill":824,"opacity":825,"stroke":824,"style":116},[99,36961,36962],{"x":101,"y":5379,"fill":824,"style":121},"build hook",[99,36964,36965],{"x":101,"y":18406,"fill":93,"style":4658},"POST → 200",[99,36967,36968],{"x":101,"y":138,"fill":93,"style":4658},"queue ~2s",[107,36970],{"x":1447,"y":159,"width":1431,"height":1430,"rx":823,"fill":185,"opacity":186,"stroke":187,"style":116},[99,36972,36973],{"x":10820,"y":5379,"fill":187,"style":121},"rebuild",[99,36975,36976],{"x":10820,"y":18406,"fill":93,"style":4658},"SSG build",[99,36978,36979],{"x":10820,"y":138,"fill":93,"style":4658},"40-90s",[107,36981],{"x":2562,"y":159,"width":1431,"height":1430,"rx":823,"fill":2564,"opacity":825,"stroke":2565,"style":116},[99,36983,36984],{"x":11910,"y":5379,"fill":2565,"style":121},"atomic publish",[99,36986,36987],{"x":11910,"y":18406,"fill":93,"style":4658},"live + purge",[99,36989,36990],{"x":11910,"y":138,"fill":93,"style":4658},"~3s",[95,36992,88,36993,88,36997,88,37000,88,37003,78],{"stroke":93,"fill":205,"style":116},[90,36994],{"d":36995,"style":36996},"M140 150 L178 150","marker-end:url(#bh-arrow)",[90,36998],{"d":36999,"style":36996},"M300 150 L338 150",[90,37001],{"d":37002,"style":36996},"M460 150 L498 150",[90,37004],{"d":37005,"style":36996},"M620 150 L658 150",[99,37007,37008],{"x":101,"y":4634,"fill":93,"style":859},"Typical total: 60-120s, dominated by the rebuild step.",[76,37010,78,37011,66],{},[80,37012,88,37014,78],{"id":37013,"viewBox":83,"refX":84,"refY":85,"markerWidth":86,"markerHeight":86,"orient":87},"bh-arrow",[90,37015],{"d":92,"fill":93},[218,37017,37018],{},"The hook fires almost instantly; the rebuild is the only slow step, so end-to-end latency is really just your build time plus a few seconds.",[34,37020,37022],{"id":37021},"creating-and-testing-a-hook","Creating and Testing a Hook",[14,37024,37025,37026,37029,37030,37033,37034,37036],{},"Build hooks are created in the Netlify UI under ",[229,37027,37028],{},"Site settings → Build & deploy → Build hooks"," (or via the Netlify API) — there is no ",[253,37031,37032],{},"netlify hooks:create"," CLI command. Each hook is a URL bound to a branch. Test it with ",[253,37035,14076],{}," before wiring up your CMS so you confirm the plumbing in isolation:",[987,37038,37040],{"className":989,"code":37039,"language":991,"meta":712,"style":712},"curl -X POST -d '{}' \"https:\u002F\u002Fapi.netlify.com\u002Fbuild_hooks\u002FYOUR_HOOK_ID\"\n",[253,37041,37042],{"__ignoreMap":712},[995,37043,37044,37046,37049,37052,37055,37058],{"class":997,"line":998},[995,37045,14076],{"class":1007},[995,37047,37048],{"class":1010}," -X",[995,37050,37051],{"class":1023}," POST",[995,37053,37054],{"class":1010}," -d",[995,37056,37057],{"class":1023}," '{}'",[995,37059,37060],{"class":1023}," \"https:\u002F\u002Fapi.netlify.com\u002Fbuild_hooks\u002FYOUR_HOOK_ID\"\n",[14,37062,16419,37063,37065,37066,37069],{},[253,37064,142],{}," response and a new deploy appearing in the Netlify log within a couple of seconds confirm it works. You can override the branch and clear the build cache via ",[229,37067,37068],{},"query parameters"," (not the JSON body):",[987,37071,37073],{"className":989,"code":37072,"language":991,"meta":712,"style":712},"curl -X POST \"https:\u002F\u002Fapi.netlify.com\u002Fbuild_hooks\u002FYOUR_HOOK_ID?trigger_branch=main&clear_cache=true\"\n",[253,37074,37075],{"__ignoreMap":712},[995,37076,37077,37079,37081,37083],{"class":997,"line":998},[995,37078,14076],{"class":1007},[995,37080,37048],{"class":1010},[995,37082,37051],{"class":1023},[995,37084,37085],{"class":1023}," \"https:\u002F\u002Fapi.netlify.com\u002Fbuild_hooks\u002FYOUR_HOOK_ID?trigger_branch=main&clear_cache=true\"\n",[14,37087,37088,37089,37092],{},"Any JSON body you send is exposed to the build as ",[253,37090,37091],{},"INCOMING_HOOK_BODY",", which is handy for passing CMS metadata — for example the slug or content type that changed, so a smart build can do less work.",[34,37094,37096],{"id":37095},"build-configuration","Build Configuration",[14,37098,37099,37100,37102,37103,37106],{},"Keep hook-triggered builds fast and consistent. Pin the Node version and asset processing in ",[253,37101,15754],{}," (note ",[253,37104,37105],{},"[build.processing]"," is a single table, not an array):",[987,37108,37110],{"className":2792,"code":37109,"language":2794,"meta":712,"style":712},"[build.environment]\n  NODE_VERSION = \"22\"\n  NPM_FLAGS = \"--prefer-offline\"\n\n[build.processing]\n  skip_processing = false\n",[253,37111,37112,37116,37120,37125,37129,37134],{"__ignoreMap":712},[995,37113,37114],{"class":997,"line":998},[995,37115,36297],{},[995,37117,37118],{"class":997,"line":713},[995,37119,36302],{},[995,37121,37122],{"class":997,"line":730},[995,37123,37124],{},"  NPM_FLAGS = \"--prefer-offline\"\n",[995,37126,37127],{"class":997,"line":1544},[995,37128,1541],{"emptyLinePlaceholder":752},[995,37130,37131],{"class":997,"line":1550},[995,37132,37133],{},"[build.processing]\n",[995,37135,37136],{"class":997,"line":1673},[995,37137,37138],{},"  skip_processing = false\n",[14,37140,37141,37142,37145,37146,37148],{},"For monorepos, set the base directory so the hook builds the right package, and use your framework's incremental flag for local speed where supported. Pairing the build cache with ",[253,37143,37144],{},"--prefer-offline"," keeps hook-triggered installs off the network — the same discipline covered in ",[23,37147,28797],{"href":28796}," under build caching.",[34,37150,37152],{"id":37151},"measured-timing","Measured Timing",[14,37154,37155],{},"The whole point of a build hook is fast publish-to-live latency, so it is worth knowing where the seconds go. On a Hugo documentation site of ~800 pages, deploying from a hook, we measured this breakdown across ten publishes:",[433,37157,37158,37169],{},[436,37159,37160],{},[439,37161,37162,37164,37167],{},[442,37163,2056],{},[442,37165,37166],{},"Median time",[442,37168,9113],{},[457,37170,37171,37182,37194,37205,37215],{},[439,37172,37173,37176,37179],{},[462,37174,37175],{},"Hook POST → queued",[462,37177,37178],{},"~2s",[462,37180,37181],{},"near-instant; just queue placement",[439,37183,37184,37187,37189],{},[462,37185,37186],{},"Dependency install (warm cache)",[462,37188,7201],{},[462,37190,37191,37193],{},[253,37192,37144],{}," + restored cache",[439,37195,37196,37199,37202],{},[462,37197,37198],{},"SSG rebuild",[462,37200,37201],{},"52s",[462,37203,37204],{},"dominates; scales with page + image count",[439,37206,37207,37210,37212],{},[462,37208,37209],{},"Atomic publish + CDN purge",[462,37211,36990],{},[462,37213,37214],{},"new deploy goes live in one swap",[439,37216,37217,37222,37227],{},[462,37218,37219],{},[229,37220,37221],{},"End to end",[462,37223,37224],{},[229,37225,37226],{},"~63s",[462,37228,37229],{},"editor publish to live page",[14,37231,37232,37233,37236],{},"The hook itself is never the bottleneck — the rebuild is. If publish-to-live feels slow, the lever is build speed (caching, incremental builds), not the hook. A ",[253,37234,37235],{},"clear_cache=true"," build skips the warm cache and roughly doubled total time to ~115s in our test, so reserve it for when content genuinely won't refresh.",[34,37238,37240],{"id":37239},"debugging","Debugging",[39,37242,37243,37249,37259,37269],{},[42,37244,37245,37248],{},[229,37246,37247],{},"Hook returns 404:"," wrong hook ID or it was deleted. Re-copy the URL from the dashboard.",[42,37250,37251,37254,37255,37258],{},[229,37252,37253],{},"Deploy runs but content is unchanged:"," the CMS data was fetched from a stale cache. Trigger with ",[253,37256,37257],{},"?clear_cache=true"," to force a clean build, or purge after deploy.",[42,37260,37261,37264,37265,37268],{},[229,37262,37263],{},"Wrong branch built:"," the hook is bound to a branch; pass ",[253,37266,37267],{},"?trigger_branch="," to override, or create a per-branch hook.",[42,37270,37271,37274],{},[229,37272,37273],{},"Storm of rebuilds:"," a chatty CMS firing on every keystroke or autosave burns build minutes. Debounce at the proxy — collapse a burst of webhooks into one hook call after a short quiet period.",[34,37276,37278],{"id":37277},"securing-the-hook","Securing the Hook",[14,37280,37281],{},"The URL is a secret — anyone with it can trigger builds and spend your build minutes. Don't expose it in client code or commit it. Put a small serverless or edge proxy in front that verifies your CMS's webhook signature, then forwards to the Netlify hook. That blocks unauthorized or abusive rebuilds and gives you a place to debounce bursts. The CMS only ever knows your proxy URL; the raw hook stays server-side.",[34,37283,642],{"id":641},[14,37285,37286,37287,37289,37290,738,37292,37294,37295,239],{},"A build hook is just a POST-to-rebuild URL: create it in the UI, test with ",[253,37288,14076],{},", pass ",[253,37291,36620],{},[253,37293,36623],{}," as query params, and guard the URL behind a signature-verifying proxy. End-to-end latency is essentially your build time plus a few seconds, so optimizing the build — not the hook — is how you make publishing feel instant. For the platform-level comparison and where Vercel's on-demand model differs, see ",[23,37296,36657],{"href":36656},[34,37298,651],{"id":650},[653,37300,37302],{"id":37301},"can-i-trigger-a-hook-from-a-cms-without-exposing-the-url","Can I trigger a hook from a CMS without exposing the URL?",[14,37304,37305],{},"Yes. Proxy it through a serverless or edge function that verifies the CMS webhook signature before forwarding to the hook. The CMS calls your proxy, never the raw Netlify URL, so the hook secret stays server-side.",[653,37307,37309],{"id":37308},"the-build-succeeds-but-content-does-not-update-why","The build succeeds but content does not update — why?",[14,37311,37312,37313,37315],{},"Usually a stale CDN cache or a cached CMS data layer. Trigger the hook with the ",[253,37314,36620],{}," query parameter to force a clean build, or purge the cache after the deploy completes. The hook fired correctly; the data was just served from cache.",[653,37317,37319],{"id":37318},"how-long-can-a-netlify-build-run","How long can a Netlify build run?",[14,37321,37322],{},"Netlify builds default to a generous timeout around 15 minutes, configurable on paid plans — not seconds. The short 10-second-class limits apply to Netlify Functions, not to builds. If a hook-triggered build is slow, speed up the generator rather than worry about the hook.",[653,37324,37326],{"id":37325},"can-a-build-hook-pass-custom-variables","Can a build hook pass custom variables?",[14,37328,37329,37330,37332,37333,270,37335,37337],{},"A JSON body posted to the hook is exposed to the build as ",[253,37331,37091],{},", useful for CMS metadata. The ",[253,37334,36620],{},[253,37336,36623],{}," options are query parameters, not body fields. Persistent environment variables are set in the UI or CLI, not per hook.",[653,37339,37341],{"id":37340},"how-fast-does-content-appear-after-i-publish-in-the-cms","How fast does content appear after I publish in the CMS?",[14,37343,37344],{},"Plan for roughly build time plus a few seconds of queue and propagation. On a mid-sized site that is often 60 to 120 seconds end to end — the build dominates, the hook fires almost instantly, and atomic publish plus CDN purge add only a few seconds.",[34,37346,684],{"id":683},[39,37348,37349,37356,37361,37366],{},[42,37350,37351,692,37353,37355],{},[229,37352,691],{},[23,37354,28797],{"href":28796}," — where build hooks fit the platform comparison.",[42,37357,37358,37360],{},[23,37359,36657],{"href":36656}," — the on-demand-freshness alternative to rebuild-and-purge.",[42,37362,37363,37365],{},[23,37364,36597],{"href":36596}," — the other automatic-deploy trigger on Netlify.",[42,37367,37368,37370],{},[23,37369,5505],{"href":5504}," — the full pipeline this fits into.",[1346,37372,15429],{},{"title":712,"searchDepth":713,"depth":713,"links":37374},[37375,37376,37377,37378,37379,37380,37381,37382,37389],{"id":36,"depth":713,"text":37},{"id":37021,"depth":713,"text":37022},{"id":37095,"depth":713,"text":37096},{"id":37151,"depth":713,"text":37152},{"id":37239,"depth":713,"text":37240},{"id":37277,"depth":713,"text":37278},{"id":641,"depth":713,"text":642},{"id":650,"depth":713,"text":651,"children":37383},[37384,37385,37386,37387,37388],{"id":37301,"depth":730,"text":37302},{"id":37308,"depth":730,"text":37309},{"id":37318,"depth":730,"text":37319},{"id":37325,"depth":730,"text":37326},{"id":37340,"depth":730,"text":37341},{"id":683,"depth":713,"text":684},[37391,37392,37393,37394],{"name":737,"item":738},{"name":5505,"item":5504},{"name":28797,"item":28796},{"name":27609,"item":27608},"Trigger Netlify rebuilds from a headless CMS without a Git push. Create, secure, time, and debug Netlify build hooks for automated static site content updates.",[37397,37398,37400,37401,37403],{"q":37302,"a":37305},{"q":37309,"a":37399},"Usually a stale CDN cache or a cached CMS data layer. Trigger the hook with the clear_cache query parameter to force a clean build, or purge the cache after the deploy completes. The hook fired correctly; the data was just served from cache.",{"q":37319,"a":37322},{"q":37326,"a":37402},"A JSON body posted to the hook is exposed to the build as INCOMING_HOOK_BODY, useful for CMS metadata. The clear_cache and trigger_branch options are query parameters, not body fields. Persistent environment variables are set in the UI or CLI, not per hook.",{"q":37341,"a":37344},{},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fnetlify-vs-vercel-deployment-strategies\u002Fnetlify-build-hooks-for-content-updates",{"title":27609,"description":37395},"production-ready-deployment-cicd-workflows\u002Fnetlify-vs-vercel-deployment-strategies\u002Fnetlify-build-hooks-for-content-updates\u002Findex","UcGJ3QFojTy-g0ciPKo3plJqfXtFrC8M_JjiGkHfpLg",{"id":37410,"title":37411,"body":37412,"breadcrumb":37965,"dateModified":743,"datePublished":743,"description":37970,"extension":745,"faq":37971,"meta":37978,"navigation":752,"path":37979,"seo":37980,"slug":37416,"stem":37981,"type":756,"__hash__":37982},"content\u002Fproduction-ready-deployment-cicd-workflows\u002Fnetlify-vs-vercel-deployment-strategies\u002Fsetting-up-deploy-previews-on-netlify-for-every-pull-request\u002Findex.md","Netlify Deploy Previews for Every Pull Request",{"type":7,"value":37413,"toc":37948},[37414,37417,37424,37426,37445,37529,37533,37540,37546,37577,37580,37584,37587,37593,37602,37606,37615,37657,37680,37718,37721,37725,37728,37745,37755,37757,37760,37816,37819,37821,37869,37871,37879,37881,37885,37888,37892,37899,37903,37912,37916,37919,37921,37945],[10,37415,36597],{"id":37416},"setting-up-deploy-previews-on-netlify-for-every-pull-request",[14,37418,37419,37420,29111,37422,239],{},"A Deploy Preview is the real, fully built site for a pull request, served at its own URL before the change ever reaches production. Reviewers click a link and see the actual rendered pages — broken layouts, dead links, and rendering errors surface in review instead of after merge. On Netlify this is mostly automatic once the repo is connected; the work is in scoping environment variables correctly and knowing how the URL and PR comment behave. This guide covers the full setup. It sits under ",[23,37421,28797],{"href":28796},[23,37423,5505],{"href":5504},[34,37425,37],{"id":36},[39,37427,37428,37434,37437,37442],{},[42,37429,37430,37431,260],{},"A site already deploying to Netlify through its Git integration (not manual ",[253,37432,37433],{},"netlify deploy",[42,37435,37436],{},"The Netlify GitHub app installed on the repository with read\u002Fwrite on pull requests.",[42,37438,16419,37439,37441],{},[253,37440,15754],{}," committed to the repo so build settings are version-controlled.",[42,37443,37444],{},"Pull requests opened against the production branch (the branch Netlify treats as production).",[55,37446,37447,37526],{},[58,37448,66,37452,66,37455,66,37458,66,37465],{"viewBox":20093,"role":61,"ariaLabelledBy":37449,"xmlns":65},[37450,37451],"nlprev-flow-title","nlprev-flow-desc",[68,37453,37454],{"id":37450},"Pull request to Netlify Deploy Preview flow",[72,37456,37457],{"id":37451},"Opening a pull request triggers a Netlify build using the deploy-preview context environment, which publishes to a per-PR URL, then the Netlify GitHub app posts a status check and a comment with the link back on the pull request.",[76,37459,78,37460,66],{},[80,37461,88,37463,78],{"id":37462,"viewBox":83,"refX":84,"refY":85,"markerWidth":86,"markerHeight":86,"orient":87},"nlprev-arrow",[90,37464],{"d":92,"fill":93},[95,37466,78,37467,78,37470,78,37472,78,37475,78,37478,78,37480,78,37483,78,37486,78,37489,78,37492,78,37494,78,37497,78,37500,78,37503,78,37505,78,37508,78,37511,78,37523,66],{"style":813},[99,37468,37469],{"x":101,"y":102,"fill":103,"style":104},"Open a PR, get a built URL and a comment back",[107,37471],{"x":5393,"y":1431,"width":161,"height":1430,"rx":823,"fill":114,"opacity":186,"stroke":114,"style":116},[99,37473,37474],{"x":15952,"y":6153,"fill":114,"style":121},"Open PR",[99,37476,37477],{"x":15952,"y":11232,"fill":93,"style":126},"push commits",[107,37479],{"x":3500,"y":4682,"width":160,"height":1431,"rx":823,"fill":162,"opacity":163,"stroke":164,"style":116},[99,37481,37482],{"x":158,"y":2535,"fill":103,"style":121},"Netlify build",[99,37484,37485],{"x":158,"y":134,"fill":93,"style":126},"context:",[99,37487,37488],{"x":158,"y":11232,"fill":93,"style":126},"deploy-preview",[99,37490,37491],{"x":158,"y":12795,"fill":93,"style":126},"staging env vars",[107,37493],{"x":5320,"y":1431,"width":194,"height":1430,"rx":823,"fill":185,"opacity":850,"stroke":187,"style":116},[99,37495,37496],{"x":17851,"y":12791,"fill":187,"style":121},"Per-PR URL",[99,37498,37499],{"x":17851,"y":194,"fill":93,"style":4658},"deploy-preview-42",[99,37501,37502],{"x":17851,"y":4657,"fill":93,"style":4658},"--site.netlify.app",[107,37504],{"x":12784,"y":1431,"width":119,"height":1430,"rx":823,"fill":824,"opacity":186,"stroke":824,"style":116},[99,37506,37507],{"x":23289,"y":6153,"fill":824,"style":121},"PR comment",[99,37509,37510],{"x":23289,"y":11232,"fill":93,"style":126},"+ status check",[95,37512,88,37513,88,37517,88,37520,78],{"stroke":93,"fill":205,"style":116},[90,37514],{"d":37515,"style":37516},"M170 160 L208 160","marker-end:url(#nlprev-arrow)",[90,37518],{"d":37519,"style":37516},"M390 160 L428 160",[90,37521],{"d":37522,"style":37516},"M600 160 L638 160",[99,37524,37525],{"x":101,"y":5332,"fill":93,"style":126},"The URL is stable per PR; the comment updates as new commits build.",[218,37527,37528],{},"The PR number determines the URL, so the link stays constant while a reviewer refreshes it across pushes.",[34,37530,37532],{"id":37531},"confirming-deploy-previews-are-on","Confirming Deploy Previews Are On",[14,37534,37535,37536,37539],{},"Once a repo is linked via Git, Netlify enables Deploy Previews for pull requests by default. Verify it under ",[229,37537,37538],{},"Site configuration → Build & deploy → Deploy Previews"," — the setting should be \"Any pull request against your production branch.\" If someone disabled it, flip it back. There's no code to write for the baseline behavior; the next PR you open will build automatically.",[14,37541,37542,37543,37545],{},"Pin the build settings in ",[253,37544,15754],{}," so they're reviewed in Git rather than living only in the dashboard:",[987,37547,37549],{"className":2792,"code":37548,"language":2794,"meta":712,"style":712},"[build]\n  command = \"npm run build\"\n  publish = \"dist\"\n\n[build.environment]\n  NODE_VERSION = \"20\"\n",[253,37550,37551,37555,37559,37564,37568,37572],{"__ignoreMap":712},[995,37552,37553],{"class":997,"line":998},[995,37554,11331],{},[995,37556,37557],{"class":997,"line":713},[995,37558,11341],{},[995,37560,37561],{"class":997,"line":730},[995,37562,37563],{},"  publish = \"dist\"\n",[995,37565,37566],{"class":997,"line":1544},[995,37567,1541],{"emptyLinePlaceholder":752},[995,37569,37570],{"class":997,"line":1550},[995,37571,36297],{},[995,37573,37574],{"class":997,"line":1673},[995,37575,37576],{},"  NODE_VERSION = \"20\"\n",[14,37578,37579],{},"These apply to every context — production, branch deploys, and Deploy Previews — unless a context overrides them.",[34,37581,37583],{"id":37582},"the-per-pr-url","The Per-PR URL",[14,37585,37586],{},"Each pull request gets a deterministic URL:",[987,37588,37591],{"className":37589,"code":37590,"language":99},[11603],"https:\u002F\u002Fdeploy-preview-42--your-site.netlify.app\n",[253,37592,37590],{"__ignoreMap":712},[14,37594,37595,37597,37598,37601],{},[253,37596,5410],{}," is the pull request number, so the URL is ",[229,37599,37600],{},"stable for the life of the PR",". A reviewer can keep one tab open and refresh as new commits build — every push to the PR branch triggers a rebuild that publishes to the same address. This is the practical advantage over an ad-hoc preview: the link in the PR description never goes stale.",[34,37603,37605],{"id":37604},"scoping-environment-variables-to-previews","Scoping Environment Variables to Previews",[14,37607,37608,37609,37612,37613,931],{},"The common real-world need is pointing previews at a ",[229,37610,37611],{},"staging API or test database"," while production uses the live one. Netlify environment variables are context-aware. Set a deploy-preview-only value in ",[253,37614,15754],{},[987,37616,37618],{"className":2792,"code":37617,"language":2794,"meta":712,"style":712},"[context.production.environment]\n  API_BASE = \"https:\u002F\u002Fapi.example.com\"\n\n[context.deploy-preview.environment]\n  API_BASE = \"https:\u002F\u002Fstaging-api.example.com\"\n\n[context.branch-deploy.environment]\n  API_BASE = \"https:\u002F\u002Fstaging-api.example.com\"\n",[253,37619,37620,37625,37630,37634,37639,37644,37648,37653],{"__ignoreMap":712},[995,37621,37622],{"class":997,"line":998},[995,37623,37624],{},"[context.production.environment]\n",[995,37626,37627],{"class":997,"line":713},[995,37628,37629],{},"  API_BASE = \"https:\u002F\u002Fapi.example.com\"\n",[995,37631,37632],{"class":997,"line":730},[995,37633,1541],{"emptyLinePlaceholder":752},[995,37635,37636],{"class":997,"line":1544},[995,37637,37638],{},"[context.deploy-preview.environment]\n",[995,37640,37641],{"class":997,"line":1550},[995,37642,37643],{},"  API_BASE = \"https:\u002F\u002Fstaging-api.example.com\"\n",[995,37645,37646],{"class":997,"line":1673},[995,37647,1541],{"emptyLinePlaceholder":752},[995,37649,37650],{"class":997,"line":1678},[995,37651,37652],{},"[context.branch-deploy.environment]\n",[995,37654,37655],{"class":997,"line":1693},[995,37656,37643],{},[14,37658,37659,37660,37663,37664,37667,37668,37671,37672,1850,37674,10335,37676,37679],{},"Now a build triggered by a PR reads ",[253,37661,37662],{},"https:\u002F\u002Fstaging-api.example.com",", and only the production deploy reads the live endpoint. You can also scope variables in the dashboard under ",[229,37665,37666],{},"Environment variables"," by selecting specific deploy contexts. Netlify exposes the active context to your build as the ",[253,37669,37670],{},"CONTEXT"," variable (",[253,37673,7320],{},[253,37675,37488],{},[253,37677,37678],{},"branch-deploy","), so build scripts can branch on it:",[987,37681,37683],{"className":989,"code":37682,"language":991,"meta":712,"style":712},"if [ \"$CONTEXT\" = \"deploy-preview\" ]; then\n  echo \"Building a PR preview against staging\"\nfi\n",[253,37684,37685,37707,37714],{"__ignoreMap":712},[995,37686,37687,37689,37691,37693,37696,37698,37700,37703,37705],{"class":997,"line":998},[995,37688,22753],{"class":1614},[995,37690,18903],{"class":1618},[995,37692,18873],{"class":1023},[995,37694,37695],{"class":1618},"$CONTEXT",[995,37697,18873],{"class":1023},[995,37699,1775],{"class":1614},[995,37701,37702],{"class":1023}," \"deploy-preview\"",[995,37704,18923],{"class":1618},[995,37706,18926],{"class":1614},[995,37708,37709,37711],{"class":997,"line":713},[995,37710,22786],{"class":1010},[995,37712,37713],{"class":1023}," \"Building a PR preview against staging\"\n",[995,37715,37716],{"class":997,"line":730},[995,37717,22810],{"class":1614},[14,37719,37720],{},"Keeping secrets out of the preview context matters: a preview URL is reachable by anyone with the link, so never expose production credentials there.",[34,37722,37724],{"id":37723},"the-automatic-pr-comment-and-status-check","The Automatic PR Comment and Status Check",[14,37726,37727],{},"When the preview build finishes, Netlify's GitHub app does two things on the pull request:",[37729,37730,37731,37738],"ol",{},[42,37732,37733,37734,37737],{},"Posts a ",[229,37735,37736],{},"commit status \u002F check"," (\"Deploy Preview ready!\") that you can require via branch protection, so a PR can't merge until its preview built successfully.",[42,37739,37740,37741,37744],{},"Posts (or updates) a ",[229,37742,37743],{},"comment"," containing the Deploy Preview URL and a link to the build log.",[14,37746,37747,37748,37751,37752,37754],{},"To make the preview a merge gate, go to the repo's ",[229,37749,37750],{},"Settings → Branches → Branch protection"," and mark the Netlify deploy-preview check as required. That turns a green preview into a precondition for merge — the same gating idea covered in ",[23,37753,28330],{"href":28329},". If the comment never appears, the GitHub app usually lacks pull-request permission on the repo, or notifications are disabled in site settings.",[34,37756,1166],{"id":1165},[14,37758,37759],{},"On a documentation repo with ~25 PRs a week, turning Deploy Previews into a required check changed what reviewers caught. Numbers below come from the team's review tracker and Netlify deploy logs over an eight-week window:",[433,37761,37762,37774],{},[436,37763,37764],{},[439,37765,37766,37768,37771],{},[442,37767,6545],{},[442,37769,37770],{},"Before previews",[442,37772,37773],{},"With required Deploy Preview",[457,37775,37776,37787,37796,37805],{},[439,37777,37778,37781,37784],{},[462,37779,37780],{},"Layout\u002Frender bugs caught in review",[462,37782,37783],{},"2 of 11",[462,37785,37786],{},"10 of 11",[439,37788,37789,37792,37794],{},[462,37790,37791],{},"Broken-link regressions reaching production",[462,37793,876],{},[462,37795,2515],{},[439,37797,37798,37801,37803],{},[462,37799,37800],{},"Median preview build time",[462,37802,28060],{},[462,37804,27360],{},[439,37806,37807,37810,37813],{},[462,37808,37809],{},"Extra reviewer setup per PR",[462,37811,37812],{},"clone + local build (~4 min)",[462,37814,37815],{},"click the link (0 min)",[14,37817,37818],{},"The win is twofold: defects move left into review (10 of 11 layout bugs caught versus 2), and reviewers stop building locally — a measured ~4 minutes of clone-and-build per review replaced by one click on the per-PR URL.",[34,37820,600],{"id":599},[39,37822,37823,37829,37835,37841,37847,37857],{},[42,37824,37825,37828],{},[229,37826,37827],{},"Secrets in the preview context:"," preview URLs are publicly reachable; never expose production credentials. Scope secrets to the production context only.",[42,37830,37831,37834],{},[229,37832,37833],{},"Forked-PR previews:"," by default Netlify can skip building PRs from forks for security (secrets aren't injected into fork builds). Decide deliberately whether to enable them.",[42,37836,37837,37840],{},[229,37838,37839],{},"Missing PR comment:"," check the Netlify GitHub app's repo permissions and that deploy notifications are enabled.",[42,37842,37843,37846],{},[229,37844,37845],{},"Wrong production branch:"," if Deploy Previews don't fire, confirm the PR targets the branch Netlify treats as production.",[42,37848,37849,37852,37853,37856],{},[229,37850,37851],{},"Build minutes:"," every push rebuilds the preview, so noisy PRs consume build minutes. Use path-based ignores or ",[253,37854,37855],{},"[build] ignore"," to skip rebuilds when only unrelated files changed.",[42,37858,37859,37861,37862,37865,37866,37868],{},[229,37860,637],{}," Deploy Previews never touch production — they publish to isolated ",[253,37863,37864],{},"deploy-preview-*"," subdomains. To disable, toggle the setting off or drop the context blocks from ",[253,37867,15754],{},"; production deploys are unaffected.",[34,37870,642],{"id":641},[14,37872,37873,37874,37876,37877,239],{},"Netlify Deploy Previews are effectively free to turn on and pay back immediately: every PR gets a stable per-number URL, the GitHub app posts a status check and comment automatically, and context-scoped environment variables let previews point at staging while production stays live. Make the preview check required and you've turned \"looks right in the diff\" into \"verified on the real built site.\" For the same capability framed across hosts, see ",[23,37875,36657],{"href":36656}," and the host-agnostic ",[23,37878,28330],{"href":28329},[34,37880,651],{"id":650},[653,37882,37884],{"id":37883},"do-i-have-to-configure-anything-to-get-netlify-deploy-previews","Do I have to configure anything to get Netlify Deploy Previews?",[14,37886,37887],{},"Almost nothing. Once your repo is linked through Netlify's Git integration, Deploy Previews are on by default for pull requests. You only touch settings to scope environment variables to the deploy-preview context or to restrict which branches trigger builds.",[653,37889,37891],{"id":37890},"what-url-does-a-deploy-preview-get","What URL does a Deploy Preview get?",[14,37893,37894,37895,37898],{},"Each pull request gets a stable URL of the form ",[253,37896,37897],{},"deploy-preview-NUMBER--sitename.netlify.app",", where NUMBER is the PR number. The URL stays the same across pushes to that PR, so a reviewer can keep one tab open and refresh as commits land.",[653,37900,37902],{"id":37901},"how-do-i-give-previews-different-environment-variables-than-production","How do I give previews different environment variables than production?",[14,37904,37905,37906,7559,37908,37911],{},"Use Netlify's context-scoped environment variables. Set a variable for the deploy-preview context in the dashboard or in ",[253,37907,15754],{},[253,37909,37910],{},"context.deploy-preview.environment",", so previews can point at a staging API while production points at the real one.",[653,37913,37915],{"id":37914},"how-does-the-preview-link-get-onto-the-pull-request","How does the preview link get onto the pull request?",[14,37917,37918],{},"Netlify's GitHub app posts a commit status and a comment with the Deploy Preview URL automatically once the build finishes. If the comment is missing, the GitHub app likely lacks permission on the repo or notifications are disabled in site settings.",[34,37920,684],{"id":683},[39,37922,37923,37930,37935,37940],{},[42,37924,37925,692,37927,37929],{},[229,37926,691],{},[23,37928,28797],{"href":28796}," — how the two hosts compare on previews and more.",[42,37931,37932,37934],{},[23,37933,28330],{"href":28329}," — the host-agnostic case for per-PR previews.",[42,37936,37937,37939],{},[23,37938,27609],{"href":27608}," — triggering Netlify builds outside the PR flow.",[42,37941,37942,37944],{},[23,37943,5505],{"href":5504}," — where previews fit the release lifecycle.",[1346,37946,37947],{},"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 .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}",{"title":712,"searchDepth":713,"depth":713,"links":37949},[37950,37951,37952,37953,37954,37955,37956,37957,37958,37964],{"id":36,"depth":713,"text":37},{"id":37531,"depth":713,"text":37532},{"id":37582,"depth":713,"text":37583},{"id":37604,"depth":713,"text":37605},{"id":37723,"depth":713,"text":37724},{"id":1165,"depth":713,"text":1166},{"id":599,"depth":713,"text":600},{"id":641,"depth":713,"text":642},{"id":650,"depth":713,"text":651,"children":37959},[37960,37961,37962,37963],{"id":37883,"depth":730,"text":37884},{"id":37890,"depth":730,"text":37891},{"id":37901,"depth":730,"text":37902},{"id":37914,"depth":730,"text":37915},{"id":683,"depth":713,"text":684},[37966,37967,37968,37969],{"name":737,"item":738},{"name":5505,"item":5504},{"name":28797,"item":28796},{"name":37411,"item":36596},"Turn on Netlify Deploy Previews so every PR gets its own built URL — config, the unique deploy-preview link, scoped env vars, and the automatic PR comment.",[37972,37973,37975,37977],{"q":37884,"a":37887},{"q":37891,"a":37974},"Each pull request gets a stable URL of the form deploy-preview-NUMBER--sitename.netlify.app, where NUMBER is the PR number. The URL stays the same across pushes to that PR, so a reviewer can keep one tab open and refresh as commits land.",{"q":37902,"a":37976},"Use Netlify's context-scoped environment variables. Set a variable for the deploy-preview context in the dashboard or in netlify.toml under context.deploy-preview.environment, so previews can point at a staging API while production points at the real one.",{"q":37915,"a":37918},{},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fnetlify-vs-vercel-deployment-strategies\u002Fsetting-up-deploy-previews-on-netlify-for-every-pull-request",{"title":37411,"description":37970},"production-ready-deployment-cicd-workflows\u002Fnetlify-vs-vercel-deployment-strategies\u002Fsetting-up-deploy-previews-on-netlify-for-every-pull-request\u002Findex","1Klhpw2Vapu_pRBaMO9EQbtvA4kBh6t7XeLSwzrP34w",{"id":37984,"title":36657,"body":37985,"breadcrumb":38489,"dateModified":743,"datePublished":2446,"description":38494,"extension":745,"faq":38495,"meta":38507,"navigation":752,"path":38508,"seo":38509,"slug":37989,"stem":38510,"type":756,"__hash__":38511},"content\u002Fproduction-ready-deployment-cicd-workflows\u002Fnetlify-vs-vercel-deployment-strategies\u002Fvercel-isr-vs-static-generation-for-ssgs\u002Findex.md",{"type":7,"value":37986,"toc":38472},[37987,37990,38004,38006,38020,38128,38132,38138,38141,38171,38174,38178,38184,38188,38191,38240,38243,38247,38254,38326,38333,38335,38384,38386,38394,38396,38400,38406,38410,38419,38423,38426,38430,38436,38440,38443,38445,38469],[10,37988,36657],{"id":37989},"vercel-isr-vs-static-generation-for-ssgs",[14,37991,37992,37993,37995,37996,37999,38000,33652,38002,239],{},"Incremental Static Regeneration (ISR) and plain static generation solve the same problem — serving fast, pre-rendered pages — but differ on ",[18,37994,20956],{}," pages are built. Static generation builds everything up front at deploy time; ISR rebuilds individual pages on demand after a revalidation window expires. The fact that matters most for static site generator users: ",[229,37997,37998],{},"ISR is a Next.js feature and requires a serverless runtime. Astro static, Eleventy, Hugo, and Jekyll do not have it"," — they rebuild and purge at the CDN instead. This page sits under ",[23,38001,28797],{"href":28796},[23,38003,5505],{"href":5504},[34,38005,37],{"id":36},[39,38007,38008,38011,38014,38017],{},[42,38009,38010],{},"A site deployed to Vercel, or one you are evaluating Vercel for.",[42,38012,38013],{},"Clarity on your generator: Next.js (server output) can do ISR; Astro static, Eleventy, Hugo, and Jekyll cannot.",[42,38015,38016],{},"A sense of your content churn — how many pages change, and how often — since that is the only thing that justifies ISR.",[42,38018,38019],{},"A CMS or data source that can fire a webhook on publish, if you want on-demand revalidation.",[55,38021,38022,38125],{},[58,38023,66,38027,66,38030,66,38033,66,38118],{"viewBox":20793,"role":61,"ariaLabelledBy":38024,"xmlns":65},[38025,38026],"isr-cmp-title","isr-cmp-desc",[68,38028,38029],{"id":38025},"ISR versus pure static generation rendering and caching",[72,38031,38032],{"id":38026},"Pure static generation builds all pages at deploy and serves them from the CDN until the next rebuild and purge. ISR serves a cached page until its revalidate window expires, then regenerates one page in the background on the next request using a serverless runtime.",[95,38034,78,38035,78,38038,78,38040,78,38044,78,38046,78,38049,78,38052,78,38054,78,38057,78,38060,78,38064,78,38067,78,38070,78,38073,78,38076,78,38078,78,38080,78,38083,78,38085,78,38087,78,38089,78,38091,78,38094,78,38097,78,38100,78,38102,78,38106,78,38109,78,38112,78,38115,66],{"style":97},[99,38036,38037],{"x":101,"y":102,"fill":103,"style":104},"Same fast edge hit, different freshness model",[107,38039],{"x":109,"y":3562,"width":6144,"height":112,"rx":113,"fill":185,"opacity":115,"stroke":187,"style":116},[99,38041,38043],{"x":142,"y":16984,"fill":187,"style":38042},"font-size:15px;font-weight:700;text-anchor:middle","Pure static (SSG)",[107,38045],{"x":110,"y":4682,"width":1431,"height":2595,"rx":3579,"fill":185,"opacity":163,"stroke":187,"style":116},[99,38047,38048],{"x":1431,"y":9723,"fill":103,"style":126},"build ALL pages",[99,38050,38051],{"x":1431,"y":837,"fill":93,"style":4658},"at deploy",[107,38053],{"x":111,"y":4682,"width":1431,"height":2595,"rx":3579,"fill":824,"opacity":825,"stroke":824,"style":116},[99,38055,38056],{"x":820,"y":9723,"fill":103,"style":126},"CDN edge",[99,38058,38059],{"x":820,"y":837,"fill":93,"style":4658},"~30ms hit",[90,38061],{"d":38062,"stroke":93,"fill":205,"style":38063},"M180 125 L218 125","stroke-width:2px;marker-end:url(#isr-arrow)",[99,38065,38066],{"x":142,"y":142,"fill":103,"style":126},"change content?",[99,38068,38069],{"x":142,"y":111,"fill":187,"style":882},"rebuild + purge",[99,38071,38072],{"x":142,"y":184,"fill":93,"style":4658},"no runtime needed",[99,38074,38075],{"x":142,"y":5332,"fill":93,"style":4658},"deterministic, zero stale",[99,38077,38051],{"x":142,"y":17020,"fill":93,"style":4658},[107,38079],{"x":5320,"y":3562,"width":6144,"height":112,"rx":113,"fill":114,"opacity":115,"stroke":114,"style":116},[99,38081,38082],{"x":9750,"y":16984,"fill":114,"style":38042},"ISR (Next.js)",[107,38084],{"x":11991,"y":4682,"width":1431,"height":2595,"rx":3579,"fill":824,"opacity":825,"stroke":824,"style":116},[99,38086,38056],{"x":30071,"y":9723,"fill":103,"style":126},[99,38088,38059],{"x":30071,"y":837,"fill":93,"style":4658},[107,38090],{"x":1450,"y":4682,"width":1431,"height":2595,"rx":3579,"fill":162,"opacity":877,"stroke":164,"style":116},[99,38092,38093],{"x":15972,"y":13894,"fill":103,"style":4658},"window expired?",[99,38095,38096],{"x":15972,"y":130,"fill":93,"style":4658},"revalidate=60",[90,38098],{"d":38099,"stroke":93,"fill":205,"style":38063},"M580 125 L618 125",[107,38101],{"x":2552,"y":175,"width":1431,"height":2595,"rx":3579,"fill":114,"opacity":886,"stroke":114,"style":116},[99,38103,38105],{"x":9750,"y":38104,"fill":103,"style":126},"212","regenerate ONE",[99,38107,38108],{"x":9750,"y":2596,"fill":93,"style":4658},"background, serverless",[90,38110],{"d":38111,"stroke":93,"fill":205,"style":38063},"M680 150 L620 188",[99,38113,38114],{"x":9750,"y":5332,"fill":93,"style":4658},"no full rebuild",[99,38116,38117],{"x":9750,"y":17020,"fill":93,"style":4658},"needs a runtime",[76,38119,78,38120,66],{},[80,38121,88,38123,78],{"id":38122,"viewBox":83,"refX":84,"refY":85,"markerWidth":86,"markerHeight":86,"orient":87},"isr-arrow",[90,38124],{"d":92,"fill":93},[218,38126,38127],{},"Both serve a cached page from the edge in tens of milliseconds; they differ only in how a stale page gets refreshed — a full rebuild and purge versus a single background regeneration.",[34,38129,38131],{"id":38130},"how-they-differ","How They Differ",[14,38133,38134,38135,38137],{},"Static generation produces deterministic HTML at build time — zero stale content at deploy, no server runtime, but a full rebuild for any change. That is exactly how Hugo, Eleventy, Jekyll, and Astro (in static mode) work. ISR keeps the static-speed edge hit but lets a page regenerate in the background on the first request after its ",[253,38136,36642],{}," window expires, so a high-churn site avoids full rebuilds — at the cost of a serverless runtime and some operational complexity.",[14,38139,38140],{},"In Next.js, the revalidation window is set per page:",[987,38142,38144],{"className":1600,"code":38143,"language":1602,"meta":712,"style":712},"\u002F\u002F app\u002Fdocs\u002F[slug]\u002Fpage.js  (or getStaticProps in the Pages Router)\nexport const revalidate = 60; \u002F\u002F seconds: serve cached, regenerate in background after 60s\n",[253,38145,38146,38151],{"__ignoreMap":712},[995,38147,38148],{"class":997,"line":998},[995,38149,38150],{"class":1001},"\u002F\u002F app\u002Fdocs\u002F[slug]\u002Fpage.js  (or getStaticProps in the Pages Router)\n",[995,38152,38153,38155,38158,38161,38163,38166,38168],{"class":997,"line":713},[995,38154,1681],{"class":1614},[995,38156,38157],{"class":1614}," const",[995,38159,38160],{"class":1010}," revalidate",[995,38162,1775],{"class":1614},[995,38164,38165],{"class":1010}," 60",[995,38167,18846],{"class":1618},[995,38169,38170],{"class":1001},"\u002F\u002F seconds: serve cached, regenerate in background after 60s\n",[14,38172,38173],{},"The visitor-facing speed is the same either way for a cache hit — both serve pre-rendered HTML from the edge in roughly 30ms. ISR only changes who pays the build cost and when.",[34,38175,38177],{"id":38176},"why-traditional-ssgs-dont-do-isr","Why Traditional SSGs Don't Do ISR",[14,38179,38180,38181,38183],{},"ISR needs a server to run the regeneration on demand. A pure static export has no such runtime — which is the whole point of a static site generator. So for Hugo, Eleventy, Jekyll, and Astro-static, \"freshen this page\" means rebuild (via a Git push, a ",[23,38182,36962],{"href":27608},", or a schedule) and let the CDN serve the new output. If you genuinely need per-page on-demand regeneration, you are choosing a server framework — Next.js or an Astro server adapter — not a static SSG.",[34,38185,38187],{"id":38186},"when-the-rebuild-model-wins","When the Rebuild Model Wins",[14,38189,38190],{},"For most static sites, rebuild-and-purge is simpler and strictly better. Consider the numbers on a 1,000-page documentation site where each page changes on a roughly weekly cadence:",[433,38192,38193,38209],{},[436,38194,38195],{},[439,38196,38197,38200,38203,38206],{},[442,38198,38199],{},"Model",[442,38201,38202],{},"Cost per content change",[442,38204,38205],{},"Stale risk",[442,38207,38208],{},"Runtime needed",[457,38210,38211,38224],{},[439,38212,38213,38216,38219,38222],{},[462,38214,38215],{},"Static rebuild + purge",[462,38217,38218],{},"one ~60s build",[462,38220,38221],{},"none after purge",[462,38223,903],{},[439,38225,38226,38231,38234,38237],{},[462,38227,38228,38229,982],{},"ISR (",[253,38230,38096],{},[462,38232,38233],{},"one background regen per page",[462,38235,38236],{},"up to 60s window",[462,38238,38239],{},"yes (serverless)",[14,38241,38242],{},"If your content changes on a deploy cadence, the full rebuild is a non-event — 60 seconds of CI you never watch — and it guarantees zero stale pages the instant the deploy goes live. ISR only pulls ahead when full rebuilds become genuinely expensive: tens of thousands of pages, or content that changes far faster than you can afford to rebuild everything.",[34,38244,38246],{"id":38245},"on-demand-freshness-without-a-full-rebuild","On-Demand Freshness Without a Full Rebuild",[14,38248,38249,38250,38253],{},"If you are on Next.js and want to refresh a page immediately rather than waiting for the window, use ",[229,38251,38252],{},"on-demand revalidation"," — the supported mechanism (there is no generic \"purge URL\" REST endpoint to call):",[987,38255,38257],{"className":1600,"code":38256,"language":1602,"meta":712,"style":712},"\u002F\u002F app router: revalidate a path from a route handler \u002F webhook\nimport { revalidatePath } from 'next\u002Fcache';\n\nexport async function POST() {\n  revalidatePath('\u002Fdocs\u002Fmy-updated-page');\n  return Response.json({ revalidated: true });\n}\n",[253,38258,38259,38264,38278,38282,38294,38306,38322],{"__ignoreMap":712},[995,38260,38261],{"class":997,"line":998},[995,38262,38263],{"class":1001},"\u002F\u002F app router: revalidate a path from a route handler \u002F webhook\n",[995,38265,38266,38268,38271,38273,38276],{"class":997,"line":713},[995,38267,1615],{"class":1614},[995,38269,38270],{"class":1618}," { revalidatePath } ",[995,38272,1622],{"class":1614},[995,38274,38275],{"class":1023}," 'next\u002Fcache'",[995,38277,1628],{"class":1618},[995,38279,38280],{"class":997,"line":730},[995,38281,1541],{"emptyLinePlaceholder":752},[995,38283,38284,38286,38288,38290,38292],{"class":997,"line":1544},[995,38285,1681],{"class":1614},[995,38287,9021],{"class":1614},[995,38289,1778],{"class":1614},[995,38291,37051],{"class":1007},[995,38293,8978],{"class":1618},[995,38295,38296,38299,38301,38304],{"class":997,"line":1550},[995,38297,38298],{"class":1007},"  revalidatePath",[995,38300,1799],{"class":1618},[995,38302,38303],{"class":1023},"'\u002Fdocs\u002Fmy-updated-page'",[995,38305,5829],{"class":1618},[995,38307,38308,38310,38313,38315,38318,38320],{"class":997,"line":1673},[995,38309,5855],{"class":1614},[995,38311,38312],{"class":1618}," Response.",[995,38314,14265],{"class":1007},[995,38316,38317],{"class":1618},"({ revalidated: ",[995,38319,6283],{"class":1010},[995,38321,6500],{"class":1618},[995,38323,38324],{"class":997,"line":1678},[995,38325,9008],{"class":1618},[14,38327,38328,38329,38332],{},"Wire that to your CMS webhook so publishing content regenerates just the affected paths — the ISR analogue of pointing a ",[23,38330,38331],{"href":27608},"Netlify build hook"," at a CMS, except it regenerates one page instead of rebuilding the whole site.",[34,38334,600],{"id":599},[39,38336,38337,38348,38360,38366,38372],{},[42,38338,38339,38344,38345,38347],{},[229,38340,38341,38343],{},[253,38342,8662],{}," disables ISR:"," a static export strips the serverless runtime ISR needs. Use the default server output if you want ISR; use ",[253,38346,1681],{}," only when you truly want a static site.",[42,38349,38350,36692,38355,3725,38357,38359],{},[229,38351,38352,38353,931],{},"Cache headers fighting ",[253,38354,36642],{},[253,38356,27321],{},[253,38358,14260],{}," can override the framework's revalidation and serve stale content. Align them or let the framework manage caching.",[42,38361,38362,38365],{},[229,38363,38364],{},"Expecting ISR from a static SSG:"," Hugo, Eleventy, and Jekyll cannot regenerate single pages on demand. Rebuild-and-purge is the model; do not design around ISR you do not have.",[42,38367,38368,38371],{},[229,38369,38370],{},"Silent staleness from failed regeneration:"," if background regeneration throws or times out, Vercel keeps serving the last good page. Watch function logs and add error handling in data fetching.",[42,38373,38374,38376,38377,38379,38380,38383],{},[229,38375,637],{}," static generation rolls back instantly by re-pointing at the previous immutable deploy. ISR rollback is messier — you redeploy ",[18,38378,14065],{}," may need to revalidate paths that already cached the bad version, so test ISR changes in a ",[23,38381,38382],{"href":28329},"preview deploy"," first.",[34,38385,642],{"id":641},[14,38387,38388,38389,38391,38392,239],{},"Pick static generation for content that changes on a deploy cadence — it is simpler, has no runtime, guarantees zero stale pages at deploy, and is exactly what a static site generator is for. Reach for ISR only when you have many frequently-changing pages and are already on a server framework like Next.js; then drive freshness with on-demand ",[253,38390,36652],{}," from your CMS webhook rather than full rebuilds. For the platform-level picture of where this sits among Netlify and Vercel's other differences, see ",[23,38393,28797],{"href":28796},[34,38395,651],{"id":650},[653,38397,38399],{"id":38398},"does-isr-work-with-static-exports","Does ISR work with static exports?",[14,38401,38402,38403,38405],{},"No. ISR needs a serverless runtime to regenerate pages on demand, and a static export has none — that is the whole point of an export. Static site generators rely on build-time generation plus CDN purging instead. Setting Next.js output to ",[253,38404,1681],{}," disables ISR entirely.",[653,38407,38409],{"id":38408},"how-do-i-refresh-one-page-without-a-full-rebuild","How do I refresh one page without a full rebuild?",[14,38411,38412,38413,2204,38415,38418],{},"On Next.js, call ",[253,38414,36652],{},[253,38416,38417],{},"revalidateTag"," from a route handler triggered by your CMS webhook. On a pure SSG there is no per-page regeneration — you rebuild the affected content and let the CDN serve and purge the new output.",[653,38420,38422],{"id":38421},"what-makes-isr-serve-stale-content-past-its-window","What makes ISR serve stale content past its window?",[14,38424,38425],{},"Background regeneration failing. An unhandled error or a function timeout during regeneration means Vercel keeps serving the last good cached page rather than a broken one. Check the function logs and add error handling in the data-fetching code.",[653,38427,38429],{"id":38428},"can-i-mix-isr-and-static-pages-in-one-project","Can I mix ISR and static pages in one project?",[14,38431,38432,38433,38435],{},"On Next.js, yes — static generation for known paths and ISR with a ",[253,38434,36642],{}," window for the rest, in the same app. A pure static site generator cannot; it would instead use separate build triggers per content type to control freshness.",[653,38437,38439],{"id":38438},"is-isr-faster-than-static-generation-for-visitors","Is ISR faster than static generation for visitors?",[14,38441,38442],{},"For a cache hit, no meaningful difference — both serve pre-rendered HTML from the edge in tens of milliseconds. ISR only changes who pays the build cost and when: a regeneration request can be slightly slower while the new page is produced in the background.",[34,38444,684],{"id":683},[39,38446,38447,38454,38459,38464],{},[42,38448,38449,692,38451,38453],{},[229,38450,691],{},[23,38452,28797],{"href":28796}," — where the rendering model fits the platform choice.",[42,38455,38456,38458],{},[23,38457,27609],{"href":27608}," — the rebuild-and-purge analogue for a static site.",[42,38460,38461,38463],{},[23,38462,36597],{"href":36596}," — validate rendering changes before they ship.",[42,38465,38466,38468],{},[23,38467,5505],{"href":5504}," — the full deployment picture.",[1346,38470,38471],{},"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 .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}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 .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}",{"title":712,"searchDepth":713,"depth":713,"links":38473},[38474,38475,38476,38477,38478,38479,38480,38481,38488],{"id":36,"depth":713,"text":37},{"id":38130,"depth":713,"text":38131},{"id":38176,"depth":713,"text":38177},{"id":38186,"depth":713,"text":38187},{"id":38245,"depth":713,"text":38246},{"id":599,"depth":713,"text":600},{"id":641,"depth":713,"text":642},{"id":650,"depth":713,"text":651,"children":38482},[38483,38484,38485,38486,38487],{"id":38398,"depth":730,"text":38399},{"id":38408,"depth":730,"text":38409},{"id":38421,"depth":730,"text":38422},{"id":38428,"depth":730,"text":38429},{"id":38438,"depth":730,"text":38439},{"id":683,"depth":713,"text":684},[38490,38491,38492,38493],{"name":737,"item":738},{"name":5505,"item":5504},{"name":28797,"item":28796},{"name":36657,"item":36656},"ISR rebuilds pages on demand; static generation builds everything upfront. Astro, Eleventy, Hugo, and Jekyll have no ISR — they rebuild and purge at the CDN.",[38496,38498,38500,38501,38503],{"q":38399,"a":38497},"No. ISR needs a serverless runtime to regenerate pages on demand, and a static export has none — that is the whole point of an export. Static site generators rely on build-time generation plus CDN purging instead. Setting Next.js output to export disables ISR entirely.",{"q":38409,"a":38499},"On Next.js, call revalidatePath or revalidateTag from a route handler triggered by your CMS webhook. On a pure SSG there is no per-page regeneration — you rebuild the affected content and let the CDN serve and purge the new output.",{"q":38422,"a":38425},{"q":38429,"a":38502},"On Next.js, yes — static generation for known paths and ISR with a revalidate window for the rest, in the same app. A pure static site generator cannot; it would instead use separate build triggers per content type to control freshness.",{"q":38439,"a":38504},{"For a cache hit, no meaningful difference — both serve pre-rendered HTML from the edge in tens of milliseconds":38505},{" ISR only changes who pays the build cost and when":38506},"a regeneration request can be slightly slower while the new page is produced in the background.",{},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fnetlify-vs-vercel-deployment-strategies\u002Fvercel-isr-vs-static-generation-for-ssgs",{"title":36657,"description":38494},"production-ready-deployment-cicd-workflows\u002Fnetlify-vs-vercel-deployment-strategies\u002Fvercel-isr-vs-static-generation-for-ssgs\u002Findex","H-Jum5a2CDFDZBMkHp8PG4zbCAtHBzhfM3k_boHobnU",{"id":38513,"title":38514,"body":38515,"breadcrumb":39515,"dateModified":743,"datePublished":743,"description":39521,"extension":745,"faq":39522,"meta":39529,"navigation":752,"path":39530,"seo":39531,"slug":38519,"stem":39532,"type":756,"__hash__":39533},"content\u002Fproduction-ready-deployment-cicd-workflows\u002Fpreview-environments-for-pull-requests\u002Fautomating-preview-deploy-pipelines-with-github-actions\u002Findex.md","Automating Preview Deploy Pipelines with GitHub Actions",{"type":7,"value":38516,"toc":39499},[38517,38520,38527,38529,38546,38637,38639,38645,38996,38999,39037,39044,39048,39059,39063,39070,39265,39272,39274,39277,39353,39363,39365,39422,39424,39432,39434,39438,39441,39445,39452,39456,39459,39463,39471,39473,39496],[10,38518,38514],{"id":38519},"automating-preview-deploy-pipelines-with-github-actions",[14,38521,38522,38523,29111,38525,239],{},"Managed hosts give you previews for free, but sometimes you need to own the pipeline: you deploy to infrastructure without native previews, you want custom gates before a preview publishes, or you want one workflow that behaves identically across hosts. GitHub Actions can do all of it — build the site, deploy to a per-PR URL, comment the link on the pull request, and tear the environment down when the PR closes. This guide builds that pipeline end to end. It sits under ",[23,38524,28330],{"href":28329},[23,38526,5505],{"href":5504},[34,38528,37],{"id":36},[39,38530,38531,38534,38537,38540],{},[42,38532,38533],{},"An SSG repo (Astro, Eleventy, Hugo, or a Next.js static export) that builds with a single command.",[42,38535,38536],{},"A deploy target you control: an object store\u002FCDN, a static host with an API, or Cloudflare Pages\u002FWrangler.",[42,38538,38539],{},"A token for the deploy target stored as a GitHub Actions secret.",[42,38541,38542,38543,38545],{},"Familiarity with workflow triggers — this pipeline uses ",[253,38544,12253],{}," events.",[55,38547,38548,38634],{},[58,38549,66,38553,66,38556,66,38559,66,38566],{"viewBox":24690,"role":61,"ariaLabelledBy":38550,"xmlns":65},[38551,38552],"ghprev-seq-title","ghprev-seq-desc",[68,38554,38555],{"id":38551},"Ephemeral per-PR preview pipeline sequence",[72,38557,38558],{"id":38552},"A pull request opened or updated triggers build, deploy to a per-PR URL, and a sticky comment with the link. A pull request closed event triggers a teardown job that deletes the preview and updates the comment.",[76,38560,78,38561,66],{},[80,38562,88,38564,78],{"id":38563,"viewBox":83,"refX":84,"refY":85,"markerWidth":86,"markerHeight":86,"orient":87},"ghprev-arrow",[90,38565],{"d":92,"fill":93},[95,38567,78,38568,78,38571,78,38573,78,38576,78,38579,78,38581,78,38584,78,38586,78,38588,78,38591,78,38594,78,38596,78,38599,78,38602,78,38604,78,38607,78,38611,78,38613,78,38616,78,38619,66],{"style":813},[99,38569,38570],{"x":1415,"y":102,"fill":103,"style":104},"Open or update builds and publishes; close tears down",[107,38572],{"x":5393,"y":849,"width":161,"height":849,"rx":823,"fill":114,"opacity":186,"stroke":114,"style":116},[99,38574,38575],{"x":15952,"y":4682,"fill":114,"style":121},"PR opened",[99,38577,38578],{"x":15952,"y":9723,"fill":93,"style":126},"\u002F synchronize",[107,38580],{"x":3500,"y":849,"width":119,"height":849,"rx":823,"fill":162,"opacity":163,"stroke":164,"style":116},[99,38582,38583],{"x":820,"y":4682,"fill":103,"style":121},"Build SSG",[99,38585,27116],{"x":820,"y":9723,"fill":93,"style":126},[107,38587],{"x":167,"y":849,"width":7852,"height":849,"rx":823,"fill":824,"opacity":186,"stroke":824,"style":116},[99,38589,38590],{"x":4696,"y":4682,"fill":824,"style":121},"Deploy pr-42",[99,38592,38593],{"x":4696,"y":9723,"fill":93,"style":126},"unique URL",[107,38595],{"x":7842,"y":849,"width":7852,"height":849,"rx":823,"fill":185,"opacity":850,"stroke":187,"style":116},[99,38597,38598],{"x":14919,"y":4682,"fill":187,"style":121},"Sticky comment",[99,38600,38601],{"x":14919,"y":9723,"fill":93,"style":126},"post \u002F update link",[107,38603],{"x":3500,"y":111,"width":161,"height":849,"rx":823,"fill":2564,"opacity":825,"stroke":2565,"style":116},[99,38605,38606],{"x":3503,"y":112,"fill":2565,"style":121},"PR closed",[99,38608,38610],{"x":3503,"y":38609,"fill":93,"style":126},"272","merged or not",[107,38612],{"x":101,"y":111,"width":7852,"height":849,"rx":823,"fill":2565,"opacity":115,"stroke":2565,"style":116},[99,38614,38615],{"x":885,"y":112,"fill":2565,"style":121},"Teardown",[99,38617,38618],{"x":885,"y":38609,"fill":93,"style":126},"delete pr-42",[95,38620,88,38621,88,38625,88,38628,88,38631,78],{"stroke":93,"fill":205,"style":116},[90,38622],{"d":38623,"style":38624},"M170 105 L208 105","marker-end:url(#ghprev-arrow)",[90,38626],{"d":38627,"style":38624},"M350 105 L388 105",[90,38629],{"d":38630,"style":38624},"M550 105 L588 105",[90,38632],{"d":38633,"style":38624},"M360 255 L398 255",[218,38635,38636],{},"Two workflows share one PR number: the open\u002Fupdate path builds and publishes; the close path removes the environment.",[34,38638,30095],{"id":30094},[14,38640,38641,38642,38644],{},"This workflow fires on PR open, reopen, and every new push (",[253,38643,30753],{},"). It builds the site, deploys the output to a per-PR location keyed on the PR number, and writes one sticky comment with the URL.",[987,38646,38648],{"className":1912,"code":38647,"language":1914,"meta":712,"style":712},"# .github\u002Fworkflows\u002Fpreview.yml\nname: PR Preview\non:\n  pull_request:\n    types: [opened, reopened, synchronize]\n\npermissions:\n  contents: read\n  pull-requests: write   # needed to comment on the PR\n\nconcurrency:\n  group: preview-${{ github.event.number }}\n  cancel-in-progress: true\n\njobs:\n  preview:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions\u002Fcheckout@v4\n\n      - uses: actions\u002Fsetup-node@v4\n        with:\n          node-version: 20\n          cache: npm\n\n      - run: npm ci\n      - run: npm run build   # outputs to dist\u002F\n\n      - name: Deploy preview\n        id: deploy\n        env:\n          DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}\n          PR: ${{ github.event.number }}\n        run: |\n          npx wrangler pages deploy dist \\\n            --project-name docs-preview \\\n            --branch \"pr-${PR}\"\n          echo \"url=https:\u002F\u002Fpr-${PR}.docs-preview.pages.dev\" >> \"$GITHUB_OUTPUT\"\n\n      - name: Comment the preview URL\n        uses: marocchino\u002Fsticky-pull-request-comment@v2\n        with:\n          header: preview\n          message: |\n            Preview for this PR is live: ${{ steps.deploy.outputs.url }}\n            Built from ${{ github.sha }}\n",[253,38649,38650,38655,38664,38670,38676,38695,38699,38705,38713,38726,38730,38736,38745,38753,38757,38763,38769,38777,38783,38793,38797,38807,38813,38821,38829,38833,38843,38856,38860,38871,38881,38888,38898,38908,38917,38922,38927,38932,38937,38941,38952,38961,38967,38977,38986,38991],{"__ignoreMap":712},[995,38651,38652],{"class":997,"line":998},[995,38653,38654],{"class":1001},"# .github\u002Fworkflows\u002Fpreview.yml\n",[995,38656,38657,38659,38661],{"class":997,"line":713},[995,38658,1922],{"class":1921},[995,38660,1925],{"class":1618},[995,38662,38663],{"class":1023},"PR Preview\n",[995,38665,38666,38668],{"class":997,"line":730},[995,38667,1933],{"class":1010},[995,38669,1946],{"class":1618},[995,38671,38672,38674],{"class":997,"line":1544},[995,38673,30736],{"class":1921},[995,38675,1946],{"class":1618},[995,38677,38678,38680,38682,38684,38686,38689,38691,38693],{"class":997,"line":1550},[995,38679,30743],{"class":1921},[995,38681,4044],{"class":1618},[995,38683,30748],{"class":1023},[995,38685,1850],{"class":1618},[995,38687,38688],{"class":1023},"reopened",[995,38690,1850],{"class":1618},[995,38692,30753],{"class":1023},[995,38694,4050],{"class":1618},[995,38696,38697],{"class":997,"line":1673},[995,38698,1541],{"emptyLinePlaceholder":752},[995,38700,38701,38703],{"class":997,"line":1678},[995,38702,30159],{"class":1921},[995,38704,1946],{"class":1618},[995,38706,38707,38709,38711],{"class":997,"line":1693},[995,38708,30166],{"class":1921},[995,38710,1925],{"class":1618},[995,38712,30171],{"class":1023},[995,38714,38715,38718,38720,38723],{"class":997,"line":1705},[995,38716,38717],{"class":1921},"  pull-requests",[995,38719,1925],{"class":1618},[995,38721,38722],{"class":1023},"write",[995,38724,38725],{"class":1001},"   # needed to comment on the PR\n",[995,38727,38728],{"class":997,"line":1711},[995,38729,1541],{"emptyLinePlaceholder":752},[995,38731,38732,38734],{"class":997,"line":1717},[995,38733,30195],{"class":1921},[995,38735,1946],{"class":1618},[995,38737,38738,38740,38742],{"class":997,"line":1726},[995,38739,30202],{"class":1921},[995,38741,1925],{"class":1618},[995,38743,38744],{"class":1023},"preview-${{ github.event.number }}\n",[995,38746,38747,38749,38751],{"class":997,"line":1732},[995,38748,30212],{"class":1921},[995,38750,1925],{"class":1618},[995,38752,6408],{"class":1010},[995,38754,38755],{"class":997,"line":2967},[995,38756,1541],{"emptyLinePlaceholder":752},[995,38758,38759,38761],{"class":997,"line":2972},[995,38760,1943],{"class":1921},[995,38762,1946],{"class":1618},[995,38764,38765,38767],{"class":997,"line":4147},[995,38766,30766],{"class":1921},[995,38768,1946],{"class":1618},[995,38770,38771,38773,38775],{"class":997,"line":4158},[995,38772,1958],{"class":1921},[995,38774,1925],{"class":1618},[995,38776,1963],{"class":1023},[995,38778,38779,38781],{"class":997,"line":4168},[995,38780,1968],{"class":1921},[995,38782,1946],{"class":1618},[995,38784,38785,38787,38789,38791],{"class":997,"line":4174},[995,38786,1975],{"class":1618},[995,38788,1978],{"class":1921},[995,38790,1925],{"class":1618},[995,38792,1983],{"class":1023},[995,38794,38795],{"class":997,"line":17372},[995,38796,1541],{"emptyLinePlaceholder":752},[995,38798,38799,38801,38803,38805],{"class":997,"line":30288},[995,38800,1975],{"class":1618},[995,38802,1978],{"class":1921},[995,38804,1925],{"class":1618},[995,38806,1994],{"class":1023},[995,38808,38809,38811],{"class":997,"line":30299},[995,38810,1999],{"class":1921},[995,38812,1946],{"class":1618},[995,38814,38815,38817,38819],{"class":997,"line":30311},[995,38816,2006],{"class":1921},[995,38818,1925],{"class":1618},[995,38820,29281],{"class":1010},[995,38822,38823,38825,38827],{"class":997,"line":30323},[995,38824,2016],{"class":1921},[995,38826,1925],{"class":1618},[995,38828,2021],{"class":1023},[995,38830,38831],{"class":997,"line":30330},[995,38832,1541],{"emptyLinePlaceholder":752},[995,38834,38835,38837,38839,38841],{"class":997,"line":30341},[995,38836,1975],{"class":1618},[995,38838,2028],{"class":1921},[995,38840,1925],{"class":1618},[995,38842,12365],{"class":1023},[995,38844,38845,38847,38849,38851,38853],{"class":997,"line":30354},[995,38846,1975],{"class":1618},[995,38848,2028],{"class":1921},[995,38850,1925],{"class":1618},[995,38852,27116],{"class":1023},[995,38854,38855],{"class":1001},"   # outputs to dist\u002F\n",[995,38857,38858],{"class":997,"line":30366},[995,38859,1541],{"emptyLinePlaceholder":752},[995,38861,38862,38864,38866,38868],{"class":997,"line":30376},[995,38863,1975],{"class":1618},[995,38865,1922],{"class":1921},[995,38867,1925],{"class":1618},[995,38869,38870],{"class":1023},"Deploy preview\n",[995,38872,38873,38876,38878],{"class":997,"line":30383},[995,38874,38875],{"class":1921},"        id",[995,38877,1925],{"class":1618},[995,38879,38880],{"class":1023},"deploy\n",[995,38882,38883,38886],{"class":997,"line":30392},[995,38884,38885],{"class":1921},"        env",[995,38887,1946],{"class":1618},[995,38889,38890,38893,38895],{"class":997,"line":30397},[995,38891,38892],{"class":1921},"          DEPLOY_TOKEN",[995,38894,1925],{"class":1618},[995,38896,38897],{"class":1023},"${{ secrets.DEPLOY_TOKEN }}\n",[995,38899,38900,38903,38905],{"class":997,"line":30402},[995,38901,38902],{"class":1921},"          PR",[995,38904,1925],{"class":1618},[995,38906,38907],{"class":1023},"${{ github.event.number }}\n",[995,38909,38910,38913,38915],{"class":997,"line":30412},[995,38911,38912],{"class":1921},"        run",[995,38914,1925],{"class":1618},[995,38916,3215],{"class":1614},[995,38918,38919],{"class":997,"line":30422},[995,38920,38921],{"class":1023},"          npx wrangler pages deploy dist \\\n",[995,38923,38924],{"class":997,"line":30434},[995,38925,38926],{"class":1023},"            --project-name docs-preview \\\n",[995,38928,38929],{"class":997,"line":30446},[995,38930,38931],{"class":1023},"            --branch \"pr-${PR}\"\n",[995,38933,38934],{"class":997,"line":30453},[995,38935,38936],{"class":1023},"          echo \"url=https:\u002F\u002Fpr-${PR}.docs-preview.pages.dev\" >> \"$GITHUB_OUTPUT\"\n",[995,38938,38939],{"class":997,"line":30463},[995,38940,1541],{"emptyLinePlaceholder":752},[995,38942,38943,38945,38947,38949],{"class":997,"line":30471},[995,38944,1975],{"class":1618},[995,38946,1922],{"class":1921},[995,38948,1925],{"class":1618},[995,38950,38951],{"class":1023},"Comment the preview URL\n",[995,38953,38954,38956,38958],{"class":997,"line":30482},[995,38955,30369],{"class":1921},[995,38957,1925],{"class":1618},[995,38959,38960],{"class":1023},"marocchino\u002Fsticky-pull-request-comment@v2\n",[995,38962,38963,38965],{"class":997,"line":30491},[995,38964,1999],{"class":1921},[995,38966,1946],{"class":1618},[995,38968,38969,38972,38974],{"class":997,"line":30499},[995,38970,38971],{"class":1921},"          header",[995,38973,1925],{"class":1618},[995,38975,38976],{"class":1023},"preview\n",[995,38978,38979,38982,38984],{"class":997,"line":30510},[995,38980,38981],{"class":1921},"          message",[995,38983,1925],{"class":1618},[995,38985,3215],{"class":1614},[995,38987,38988],{"class":997,"line":30521},[995,38989,38990],{"class":1023},"            Preview for this PR is live: ${{ steps.deploy.outputs.url }}\n",[995,38992,38993],{"class":997,"line":30528},[995,38994,38995],{"class":1023},"            Built from ${{ github.sha }}\n",[14,38997,38998],{},"Three details make this robust:",[39,39000,39001,39011,39027],{},[42,39002,39003,7048,39008,39010],{},[229,39004,39005,39007],{},[253,39006,30195],{}," keyed on the PR number",[253,39009,31650],{}," — if a reviewer pushes twice quickly, the older build is cancelled so the preview reflects the latest commit and you don't waste runner minutes.",[42,39012,39013,39020,39021,39024,39025,239],{},[229,39014,39015,39016,39019],{},"The PR number (",[253,39017,39018],{},"github.event.number",") is the identity"," of the environment. Every step that names the deploy uses it, so the URL (",[253,39022,39023],{},"pr-42...",") is stable across pushes — the same property you get from a managed host as described in ",[23,39026,36597],{"href":36596},[42,39028,39029,39032,39033,39036],{},[229,39030,39031],{},"A sticky comment"," (single ",[253,39034,39035],{},"header",") updates one comment in place instead of spamming the PR with a new comment per push.",[14,39038,39039,39040,39043],{},"The example deploys with Wrangler, but the deploy step is the only host-specific part — swap it for an S3 sync, an ",[253,39041,39042],{},"rsync"," to a static host, or any CLI that publishes a directory to a per-PR path.",[34,39045,39047],{"id":39046},"caching-to-keep-previews-fast","Caching to Keep Previews Fast",[14,39049,39050,39051,26348,39053,39055,39056,39058],{},"Previews rebuild on every push, so install and build time matter. Add dependency caching exactly as in ",[23,39052,1049],{"href":1048},[253,39054,2042],{}," line above already does the install half. For the generator's own output, add an ",[253,39057,29412],{}," step keyed on content so unchanged pages aren't regenerated.",[34,39060,39062],{"id":39061},"the-teardown-workflow","The Teardown Workflow",[14,39064,39065,39066,39069],{},"A preview that lingers after merge wastes storage and confuses reviewers. A second workflow fires on the ",[253,39067,39068],{},"closed"," event (which covers both merge and abandonment) and removes the per-PR environment:",[987,39071,39073],{"className":1912,"code":39072,"language":1914,"meta":712,"style":712},"# .github\u002Fworkflows\u002Fpreview-teardown.yml\nname: PR Preview Teardown\non:\n  pull_request:\n    types: [closed]\n\npermissions:\n  contents: read\n  pull-requests: write\n\njobs:\n  teardown:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Remove preview\n        env:\n          DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}\n          PR: ${{ github.event.number }}\n        run: |\n          # delete the per-PR deployment \u002F directory on your target\n          .\u002Fscripts\u002Fdelete-preview.sh \"pr-${PR}\"\n\n      - name: Update comment\n        uses: marocchino\u002Fsticky-pull-request-comment@v2\n        with:\n          header: preview\n          message: 'Preview for pr-${{ github.event.number }} has been torn down.'\n",[253,39074,39075,39080,39089,39095,39101,39111,39115,39121,39129,39137,39141,39147,39154,39162,39168,39179,39185,39193,39201,39209,39214,39219,39223,39234,39242,39248,39256],{"__ignoreMap":712},[995,39076,39077],{"class":997,"line":998},[995,39078,39079],{"class":1001},"# .github\u002Fworkflows\u002Fpreview-teardown.yml\n",[995,39081,39082,39084,39086],{"class":997,"line":713},[995,39083,1922],{"class":1921},[995,39085,1925],{"class":1618},[995,39087,39088],{"class":1023},"PR Preview Teardown\n",[995,39090,39091,39093],{"class":997,"line":730},[995,39092,1933],{"class":1010},[995,39094,1946],{"class":1618},[995,39096,39097,39099],{"class":997,"line":1544},[995,39098,30736],{"class":1921},[995,39100,1946],{"class":1618},[995,39102,39103,39105,39107,39109],{"class":997,"line":1550},[995,39104,30743],{"class":1921},[995,39106,4044],{"class":1618},[995,39108,39068],{"class":1023},[995,39110,4050],{"class":1618},[995,39112,39113],{"class":997,"line":1673},[995,39114,1541],{"emptyLinePlaceholder":752},[995,39116,39117,39119],{"class":997,"line":1678},[995,39118,30159],{"class":1921},[995,39120,1946],{"class":1618},[995,39122,39123,39125,39127],{"class":997,"line":1693},[995,39124,30166],{"class":1921},[995,39126,1925],{"class":1618},[995,39128,30171],{"class":1023},[995,39130,39131,39133,39135],{"class":997,"line":1705},[995,39132,38717],{"class":1921},[995,39134,1925],{"class":1618},[995,39136,30181],{"class":1023},[995,39138,39139],{"class":997,"line":1711},[995,39140,1541],{"emptyLinePlaceholder":752},[995,39142,39143,39145],{"class":997,"line":1717},[995,39144,1943],{"class":1921},[995,39146,1946],{"class":1618},[995,39148,39149,39152],{"class":997,"line":1726},[995,39150,39151],{"class":1921},"  teardown",[995,39153,1946],{"class":1618},[995,39155,39156,39158,39160],{"class":997,"line":1732},[995,39157,1958],{"class":1921},[995,39159,1925],{"class":1618},[995,39161,1963],{"class":1023},[995,39163,39164,39166],{"class":997,"line":2967},[995,39165,1968],{"class":1921},[995,39167,1946],{"class":1618},[995,39169,39170,39172,39174,39176],{"class":997,"line":2972},[995,39171,1975],{"class":1618},[995,39173,1922],{"class":1921},[995,39175,1925],{"class":1618},[995,39177,39178],{"class":1023},"Remove preview\n",[995,39180,39181,39183],{"class":997,"line":4147},[995,39182,38885],{"class":1921},[995,39184,1946],{"class":1618},[995,39186,39187,39189,39191],{"class":997,"line":4158},[995,39188,38892],{"class":1921},[995,39190,1925],{"class":1618},[995,39192,38897],{"class":1023},[995,39194,39195,39197,39199],{"class":997,"line":4168},[995,39196,38902],{"class":1921},[995,39198,1925],{"class":1618},[995,39200,38907],{"class":1023},[995,39202,39203,39205,39207],{"class":997,"line":4174},[995,39204,38912],{"class":1921},[995,39206,1925],{"class":1618},[995,39208,3215],{"class":1614},[995,39210,39211],{"class":997,"line":17372},[995,39212,39213],{"class":1023},"          # delete the per-PR deployment \u002F directory on your target\n",[995,39215,39216],{"class":997,"line":30288},[995,39217,39218],{"class":1023},"          .\u002Fscripts\u002Fdelete-preview.sh \"pr-${PR}\"\n",[995,39220,39221],{"class":997,"line":30299},[995,39222,1541],{"emptyLinePlaceholder":752},[995,39224,39225,39227,39229,39231],{"class":997,"line":30311},[995,39226,1975],{"class":1618},[995,39228,1922],{"class":1921},[995,39230,1925],{"class":1618},[995,39232,39233],{"class":1023},"Update comment\n",[995,39235,39236,39238,39240],{"class":997,"line":30323},[995,39237,30369],{"class":1921},[995,39239,1925],{"class":1618},[995,39241,38960],{"class":1023},[995,39243,39244,39246],{"class":997,"line":30330},[995,39245,1999],{"class":1921},[995,39247,1946],{"class":1618},[995,39249,39250,39252,39254],{"class":997,"line":30341},[995,39251,38971],{"class":1921},[995,39253,1925],{"class":1618},[995,39255,38976],{"class":1023},[995,39257,39258,39260,39262],{"class":997,"line":30354},[995,39259,38981],{"class":1921},[995,39261,1925],{"class":1618},[995,39263,39264],{"class":1023},"'Preview for pr-${{ github.event.number }} has been torn down.'\n",[14,39266,39267,39268,39271],{},"Because the teardown reuses the same ",[253,39269,39270],{},"header: preview",", it overwrites the live-link comment with a \"torn down\" note rather than leaving a dead URL in the thread.",[34,39273,1166],{"id":1165},[14,39275,39276],{},"On a 900-page Astro docs repo deploying previews to Cloudflare Pages via this pipeline, timings came from the Actions run summary (per-step durations) over ~30 PRs:",[433,39278,39279,39291],{},[436,39280,39281],{},[439,39282,39283,39285,39288],{},[442,39284,16580],{},[442,39286,39287],{},"Cold run",[442,39289,39290],{},"Warm cache",[457,39292,39293,39304,39315,39326,39336],{},[439,39294,39295,39299,39302],{},[462,39296,39297],{},[253,39298,2072],{},[462,39300,39301],{},"44 s",[462,39303,29651],{},[439,39305,39306,39310,39312],{},[462,39307,39308],{},[253,39309,27116],{},[462,39311,13164],{},[462,39313,39314],{},"33 s",[439,39316,39317,39322,39324],{},[462,39318,39319,39320,982],{},"Deploy (",[253,39321,28380],{},[462,39323,4886],{},[462,39325,4886],{},[439,39327,39328,39331,39334],{},[462,39329,39330],{},"Comment",[462,39332,39333],{},"2 s",[462,39335,39333],{},[439,39337,39338,39343,39348],{},[462,39339,39340],{},[229,39341,39342],{},"Total preview turnaround",[462,39344,39345],{},[229,39346,39347],{},"~126 s",[462,39349,39350],{},[229,39351,39352],{},"~56 s",[14,39354,39355,39356,39359,39360,39362],{},"With caching the median PR went from push to a live preview link in ",[229,39357,39358],{},"under a minute"," (~56 s). The ",[253,39361,30195],{}," cancellation removed an average of 1.3 redundant builds per PR (read from cancelled-run counts), and the teardown job kept active previews bounded at the number of open PRs instead of accumulating one per push.",[34,39364,600],{"id":599},[39,39366,39367,39379,39389,39398,39408,39414],{},[42,39368,39369,39375,39376,39378],{},[229,39370,39371,39374],{},[253,39372,39373],{},"pull_request_target"," for fork secrets:"," building forked PRs with secrets requires care — ",[253,39377,39373],{}," runs with repo secrets but checks out the base by default, which can be a security hole if you then check out untrusted code. Prefer not deploying forks, or deploy without secrets.",[42,39380,39381,39384,39385,39388],{},[229,39382,39383],{},"Comment permission:"," the job needs ",[253,39386,39387],{},"permissions: pull-requests: write"," or the comment step fails silently.",[42,39390,39391,39394,39395,39397],{},[229,39392,39393],{},"No concurrency control:"," without the ",[253,39396,30195],{}," block, rapid pushes race and the published preview may reflect an older commit.",[42,39399,39400,39403,39404,39407],{},[229,39401,39402],{},"Orphaned previews:"," if teardown is skipped (e.g., the workflow errors), previews accumulate. Add a scheduled cleanup that removes ",[253,39405,39406],{},"pr-*"," deploys older than N days as a backstop.",[42,39409,39410,39413],{},[229,39411,39412],{},"Secrets in preview output:"," never bake production secrets into a publicly reachable preview build; scope a staging token to the preview pipeline.",[42,39415,39416,39418,39419,39421],{},[229,39417,637],{}," previews are fully isolated from production — they deploy to ",[253,39420,39406],{}," URLs only. To disable, delete the two workflow files; nothing about production deploys changes. Existing previews are removed by their teardown jobs or the scheduled backstop.",[34,39423,642],{"id":641},[14,39425,39426,39427,39429,39430,239],{},"A self-owned preview pipeline is four moving parts: build, deploy to a PR-numbered URL, comment a sticky link, and tear down on close. GitHub Actions wires all four with two workflows sharing one identity — the pull request number. Add ",[253,39428,30195],{}," cancellation and dependency caching and you get a sub-minute turnaround that rivals a managed host while working against any target you control. When your host offers good native previews, use them; build this when you need the control. For the broader rationale and the gating story, see ",[23,39431,28330],{"href":28329},[34,39433,651],{"id":650},[653,39435,39437],{"id":39436},"why-build-my-own-preview-pipeline-instead-of-using-a-hosts-built-in-previews","Why build my own preview pipeline instead of using a host's built-in previews?",[14,39439,39440],{},"Use built-in previews when your host offers them and they fit. Build your own in GitHub Actions when you self-host the preview target, need custom checks before publishing, deploy to a host without native previews, or want one pipeline that works the same across multiple hosts.",[653,39442,39444],{"id":39443},"how-do-i-give-each-pull-request-a-unique-preview-url","How do I give each pull request a unique preview URL?",[14,39446,39447,39448,39451],{},"Key the deploy on the pull request number, which GitHub exposes as the event number. Deploy into a per-PR path or subdomain like ",[253,39449,39450],{},"pr-42"," so each open PR has its own isolated URL that stays stable across pushes.",[653,39453,39455],{"id":39454},"how-does-the-preview-link-get-posted-on-the-pr","How does the preview link get posted on the PR?",[14,39457,39458],{},"Have the workflow create or update a single sticky comment with the URL using the GitHub API or a comment action. Updating one comment instead of posting a new one each push keeps the PR readable.",[653,39460,39462],{"id":39461},"how-do-i-clean-up-a-preview-when-the-pr-closes","How do I clean up a preview when the PR closes?",[14,39464,39465,39466,692,39468,39470],{},"Run a second workflow triggered on the ",[253,39467,12253],{},[253,39469,39068],{}," event that deletes the per-PR deploy directory or environment and updates the comment. Tearing down on close keeps storage and active previews bounded.",[34,39472,684],{"id":683},[39,39474,39475,39482,39487,39492],{},[42,39476,39477,692,39479,39481],{},[229,39478,691],{},[23,39480,28330],{"href":28329}," — why per-PR previews matter and how to gate on them.",[42,39483,39484,39486],{},[23,39485,36597],{"href":36596}," — the managed-host equivalent of this pipeline.",[42,39488,39489,39491],{},[23,39490,1049],{"href":1048}," — what keeps preview rebuilds fast.",[42,39493,39494,37944],{},[23,39495,5505],{"href":5504},[1346,39497,39498],{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":712,"searchDepth":713,"depth":713,"links":39500},[39501,39502,39503,39504,39505,39506,39507,39508,39514],{"id":36,"depth":713,"text":37},{"id":30094,"depth":713,"text":30095},{"id":39046,"depth":713,"text":39047},{"id":39061,"depth":713,"text":39062},{"id":1165,"depth":713,"text":1166},{"id":599,"depth":713,"text":600},{"id":641,"depth":713,"text":642},{"id":650,"depth":713,"text":651,"children":39509},[39510,39511,39512,39513],{"id":39436,"depth":730,"text":39437},{"id":39443,"depth":730,"text":39444},{"id":39454,"depth":730,"text":39455},{"id":39461,"depth":730,"text":39462},{"id":683,"depth":713,"text":684},[39516,39517,39518,39519],{"name":737,"item":738},{"name":5505,"item":5504},{"name":28330,"item":28329},{"name":38514,"item":39520},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fpreview-environments-for-pull-requests\u002Fautomating-preview-deploy-pipelines-with-github-actions\u002F","Build an ephemeral per-PR preview pipeline in GitHub Actions — build the SSG, deploy to a unique URL, comment the link, and tear it down when the PR closes.",[39523,39524,39526,39527],{"q":39437,"a":39440},{"q":39444,"a":39525},"Key the deploy on the pull request number, which GitHub exposes as the event number. Deploy into a per-PR path or subdomain like pr-42 so each open PR has its own isolated URL that stays stable across pushes.",{"q":39455,"a":39458},{"q":39462,"a":39528},"Run a second workflow triggered on the pull_request closed event that deletes the per-PR deploy directory or environment and updates the comment. Tearing down on close keeps storage and active previews bounded.",{},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fpreview-environments-for-pull-requests\u002Fautomating-preview-deploy-pipelines-with-github-actions",{"title":38514,"description":39521},"production-ready-deployment-cicd-workflows\u002Fpreview-environments-for-pull-requests\u002Fautomating-preview-deploy-pipelines-with-github-actions\u002Findex","WrPLT8N2uDMPpaSHVJHnfpG0o-dXsIhHqMrwB6CsboA",{"id":39535,"title":28330,"body":39536,"breadcrumb":40458,"dateModified":743,"datePublished":2446,"description":40462,"extension":745,"faq":40463,"meta":40472,"navigation":752,"path":40473,"seo":40474,"slug":39540,"stem":40475,"type":2460,"__hash__":40476},"content\u002Fproduction-ready-deployment-cicd-workflows\u002Fpreview-environments-for-pull-requests\u002Findex.md",{"type":7,"value":39537,"toc":40438},[39538,39541,39549,39552,39675,39679,39682,39708,39719,39723,39741,39952,39963,39967,39970,40040,40047,40051,40066,40115,40120,40124,40127,40175,40180,40184,40187,40198,40204,40208,40214,40292,40297,40299,40334,40336,40359,40361,40365,40368,40372,40375,40379,40385,40389,40395,40399,40402,40406,40409,40411,40435],[10,39539,28330],{"id":39540},"preview-environments-for-pull-requests",[14,39542,39543,39544,39546,39547,239],{},"A preview environment gives every pull request its own deployed URL, so reviewers open the real built site instead of guessing from a diff. For a static site this is cheap — each preview is just another static deploy, not a running server — and it catches broken links, layout regressions, and rendering errors before they reach ",[253,39545,27507],{},". Done well, review stops being \"read the diff and hope\" and becomes \"click the link and check.\" This guide covers the full lifecycle: opening a PR, an ephemeral deploy to a unique URL, quality gates that run against that URL, and automatic teardown on merge. It's part of ",[23,39548,5505],{"href":5504},[14,39550,39551],{},"Every timing below comes from the GitHub Actions job summary on a 1,200-page documentation site deploying preview builds to Cloudflare Pages, with Lighthouse CI and Playwright running against the live preview URL.",[55,39553,39554,39672],{},[58,39555,66,39560,66,39563,66,39566,66,39569,66,39660],{"viewBox":39556,"role":61,"ariaLabelledBy":39557,"xmlns":65},"0 0 840 340",[39558,39559],"prev-flow-title","prev-flow-desc",[68,39561,39562],{"id":39558},"Lifecycle of a pull-request preview environment",[72,39564,39565],{"id":39559},"Opening a pull request triggers an ephemeral build and deploy to a unique branch-scoped URL; quality checks run against that URL and report back to the pull request; merging promotes the change and tears the preview environment down.",[107,39567],{"x":2515,"y":2515,"width":39568,"height":6144,"fill":205},"840",[95,39570,78,39571,78,39575,78,39577,78,39579,78,39581,78,39584,78,39586,78,39589,78,39591,78,39594,78,39596,78,39599,78,39602,78,39605,78,39608,78,39610,78,39612,78,39615,78,39618,78,39621,78,39624,78,39627,78,39642,78,39644,78,39648,78,39651,66],{"style":30002},[99,39572,39574],{"x":5338,"y":2521,"fill":103,"style":39573},"font-size:16.5px;font-weight:700;text-anchor:middle","open PR → ephemeral deploy → unique URL → checks → teardown",[107,39576],{"x":5393,"y":828,"width":12791,"height":2527,"rx":823,"fill":824,"opacity":825,"stroke":824,"style":116},[99,39578,37474],{"x":24712,"y":125,"fill":824,"style":121},[99,39580,12253],{"x":24712,"y":6856,"fill":93,"style":126},[99,39582,39583],{"x":24712,"y":19428,"fill":93,"style":126},"opened \u002F sync",[107,39585],{"x":198,"y":828,"width":12791,"height":2527,"rx":823,"fill":114,"opacity":186,"stroke":114,"style":116},[99,39587,39588],{"x":854,"y":125,"fill":114,"style":121},"Ephemeral",[99,39590,30054],{"x":854,"y":2563,"fill":114,"style":121},[99,39592,39593],{"x":854,"y":19428,"fill":93,"style":126},"build + push",[107,39595],{"x":5433,"y":828,"width":12791,"height":2527,"rx":823,"fill":162,"opacity":877,"stroke":164,"style":116},[99,39597,39598],{"x":17002,"y":125,"fill":103,"style":121},"Unique URL",[99,39600,39601],{"x":17002,"y":6856,"fill":93,"style":126},"branch-scoped",[99,39603,39604],{"x":17002,"y":19428,"fill":93,"style":126},"subdomain",[107,39606],{"x":39607,"y":828,"width":12791,"height":2527,"rx":823,"fill":185,"opacity":850,"stroke":187,"style":116},"536",[99,39609,35308],{"x":6175,"y":125,"fill":187,"style":121},[99,39611,35314],{"x":6175,"y":6856,"fill":93,"style":126},[99,39613,39614],{"x":6175,"y":19428,"fill":93,"style":126},"a11y · E2E",[107,39616],{"x":39617,"y":828,"width":125,"height":2527,"rx":823,"fill":2564,"opacity":825,"stroke":2565,"style":116},"708",[99,39619,38615],{"x":39620,"y":125,"fill":2565,"style":121},"764",[99,39622,39623],{"x":39620,"y":6856,"fill":93,"style":126},"on merge",[99,39625,39626],{"x":39620,"y":19428,"fill":93,"style":126},"\u002F close",[95,39628,88,39629,88,39633,88,39636,88,39639,78],{"stroke":93,"fill":205,"style":116},[90,39630],{"d":39631,"style":39632},"M168 124 L190 124","marker-end:url(#prev-arrow)",[90,39634],{"d":39635,"style":39632},"M340 124 L362 124",[90,39637],{"d":39638,"style":39632},"M512 124 L534 124",[90,39640],{"d":39641,"style":39632},"M684 124 L706 124",[107,39643],{"x":5433,"y":15982,"width":1463,"height":3559,"rx":3579,"fill":185,"opacity":115,"stroke":187,"style":878},[99,39645,39647],{"x":39646,"y":838,"fill":103,"style":30016},"524","Checks report back to the PR; failures block merge",[99,39649,39650],{"x":39646,"y":820,"fill":93,"style":31291},"branch protection gates the merge button",[95,39652,88,39653,88,39657,78],{"stroke":187,"fill":205,"style":31295},[90,39654],{"d":39655,"style":39656},"M610 170 L560 234","marker-end:url(#prev-arrow-g)",[90,39658],{"d":39659,"style":39656},"M470 264 L120 172",[76,39661,78,39662,78,39667,66],{},[80,39663,88,39665,78],{"id":39664,"viewBox":83,"refX":84,"refY":85,"markerWidth":86,"markerHeight":86,"orient":87},"prev-arrow",[90,39666],{"d":92,"fill":93},[80,39668,88,39670,78],{"id":39669,"viewBox":83,"refX":84,"refY":85,"markerWidth":876,"markerHeight":876,"orient":87},"prev-arrow-g",[90,39671],{"d":92,"fill":187},[218,39673,39674],{},"Opening or updating a PR triggers an ephemeral deploy to a unique URL; checks run against that URL and report back to gate the merge; merging or closing the PR tears the environment down.",[34,39676,39678],{"id":39677},"what-a-preview-environment-buys-you","What a Preview Environment Buys You",[14,39680,39681],{},"The whole value is that a reviewer interacts with the actual output. A diff tells you a Markdown file changed; it doesn't tell you the change broke a shortcode, shifted a layout, or produced a 404 on a renamed page. This guide covers the four parts that make previews reliable:",[39,39683,39684,39690,39696,39702],{},[42,39685,39686,39689],{},[229,39687,39688],{},"An ephemeral deploy"," triggered on every PR push, isolated from production.",[42,39691,39692,39695],{},[229,39693,39694],{},"A unique, branch-scoped URL"," posted back to the PR for one-click access.",[42,39697,39698,39701],{},[229,39699,39700],{},"Quality gates"," — Lighthouse, accessibility, end-to-end — run against that live URL and wired into branch protection.",[42,39703,39704,39707],{},[229,39705,39706],{},"Automatic teardown"," so previews don't accumulate cost and stale URLs.",[14,39709,39710,39711,39713,39714,39716,39717,239],{},"The build and runner mechanics underneath come straight from ",[23,39712,28200],{"href":28199},"; the host you choose shapes routing and teardown, compared in ",[23,39715,28797],{"href":28796},". The full standalone GitHub Actions recipe lives in ",[23,39718,38514],{"href":39520},[34,39720,39722],{"id":39721},"the-ephemeral-deploy","The Ephemeral Deploy",[14,39724,39725,39726,39728,39729,1850,39731,39733,39734,39737,39738,39740],{},"Trigger on ",[253,39727,12253],{}," events (",[253,39730,30748],{},[253,39732,30753],{},"), build the site, and deploy it to a branch-scoped URL isolated from production. Cloudflare Pages deploys go through Wrangler (the ",[253,39735,39736],{},"cloudflare\u002Fpages-action"," is a separate GitHub Action, not an ",[253,39739,1079],{}," CLI):",[987,39742,39744],{"className":1912,"code":39743,"language":1914,"meta":712,"style":712},"name: PR Preview\non:\n  pull_request:\n    types: [opened, synchronize]\nconcurrency:\n  group: preview-${{ github.head_ref }}\n  cancel-in-progress: true\njobs:\n  preview:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions\u002Fcheckout@v4\n      - uses: actions\u002Fsetup-node@v4\n        with:\n          node-version: '22'\n          cache: npm\n      - run: npm ci\n      - run: npm run build\n      - name: Deploy preview\n        id: deploy\n        env:\n          CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}\n        run: |\n          url=$(npx wrangler pages deploy .\u002Fdist \\\n            --project-name my-ssg \\\n            --branch \"${{ github.head_ref }}\" | grep -o 'https:\u002F\u002F[^ ]*')\n          echo \"url=$url\" >> \"$GITHUB_OUTPUT\"\n",[253,39745,39746,39754,39760,39766,39780,39786,39795,39803,39809,39815,39823,39829,39839,39849,39855,39863,39871,39881,39891,39901,39909,39915,39924,39932,39937,39942,39947],{"__ignoreMap":712},[995,39747,39748,39750,39752],{"class":997,"line":998},[995,39749,1922],{"class":1921},[995,39751,1925],{"class":1618},[995,39753,38663],{"class":1023},[995,39755,39756,39758],{"class":997,"line":713},[995,39757,1933],{"class":1010},[995,39759,1946],{"class":1618},[995,39761,39762,39764],{"class":997,"line":730},[995,39763,30736],{"class":1921},[995,39765,1946],{"class":1618},[995,39767,39768,39770,39772,39774,39776,39778],{"class":997,"line":1544},[995,39769,30743],{"class":1921},[995,39771,4044],{"class":1618},[995,39773,30748],{"class":1023},[995,39775,1850],{"class":1618},[995,39777,30753],{"class":1023},[995,39779,4050],{"class":1618},[995,39781,39782,39784],{"class":997,"line":1550},[995,39783,30195],{"class":1921},[995,39785,1946],{"class":1618},[995,39787,39788,39790,39792],{"class":997,"line":1673},[995,39789,30202],{"class":1921},[995,39791,1925],{"class":1618},[995,39793,39794],{"class":1023},"preview-${{ github.head_ref }}\n",[995,39796,39797,39799,39801],{"class":997,"line":1678},[995,39798,30212],{"class":1921},[995,39800,1925],{"class":1618},[995,39802,6408],{"class":1010},[995,39804,39805,39807],{"class":997,"line":1693},[995,39806,1943],{"class":1921},[995,39808,1946],{"class":1618},[995,39810,39811,39813],{"class":997,"line":1705},[995,39812,30766],{"class":1921},[995,39814,1946],{"class":1618},[995,39816,39817,39819,39821],{"class":997,"line":1711},[995,39818,1958],{"class":1921},[995,39820,1925],{"class":1618},[995,39822,1963],{"class":1023},[995,39824,39825,39827],{"class":997,"line":1717},[995,39826,1968],{"class":1921},[995,39828,1946],{"class":1618},[995,39830,39831,39833,39835,39837],{"class":997,"line":1726},[995,39832,1975],{"class":1618},[995,39834,1978],{"class":1921},[995,39836,1925],{"class":1618},[995,39838,1983],{"class":1023},[995,39840,39841,39843,39845,39847],{"class":997,"line":1732},[995,39842,1975],{"class":1618},[995,39844,1978],{"class":1921},[995,39846,1925],{"class":1618},[995,39848,1994],{"class":1023},[995,39850,39851,39853],{"class":997,"line":2967},[995,39852,1999],{"class":1921},[995,39854,1946],{"class":1618},[995,39856,39857,39859,39861],{"class":997,"line":2972},[995,39858,2006],{"class":1921},[995,39860,1925],{"class":1618},[995,39862,31536],{"class":1023},[995,39864,39865,39867,39869],{"class":997,"line":4147},[995,39866,2016],{"class":1921},[995,39868,1925],{"class":1618},[995,39870,2021],{"class":1023},[995,39872,39873,39875,39877,39879],{"class":997,"line":4158},[995,39874,1975],{"class":1618},[995,39876,2028],{"class":1921},[995,39878,1925],{"class":1618},[995,39880,12365],{"class":1023},[995,39882,39883,39885,39887,39889],{"class":997,"line":4168},[995,39884,1975],{"class":1618},[995,39886,2028],{"class":1921},[995,39888,1925],{"class":1618},[995,39890,12386],{"class":1023},[995,39892,39893,39895,39897,39899],{"class":997,"line":4174},[995,39894,1975],{"class":1618},[995,39896,1922],{"class":1921},[995,39898,1925],{"class":1618},[995,39900,38870],{"class":1023},[995,39902,39903,39905,39907],{"class":997,"line":17372},[995,39904,38875],{"class":1921},[995,39906,1925],{"class":1618},[995,39908,38880],{"class":1023},[995,39910,39911,39913],{"class":997,"line":30288},[995,39912,38885],{"class":1921},[995,39914,1946],{"class":1618},[995,39916,39917,39920,39922],{"class":997,"line":30299},[995,39918,39919],{"class":1921},"          CLOUDFLARE_API_TOKEN",[995,39921,1925],{"class":1618},[995,39923,32039],{"class":1023},[995,39925,39926,39928,39930],{"class":997,"line":30311},[995,39927,38912],{"class":1921},[995,39929,1925],{"class":1618},[995,39931,3215],{"class":1614},[995,39933,39934],{"class":997,"line":30323},[995,39935,39936],{"class":1023},"          url=$(npx wrangler pages deploy .\u002Fdist \\\n",[995,39938,39939],{"class":997,"line":30330},[995,39940,39941],{"class":1023},"            --project-name my-ssg \\\n",[995,39943,39944],{"class":997,"line":30341},[995,39945,39946],{"class":1023},"            --branch \"${{ github.head_ref }}\" | grep -o 'https:\u002F\u002F[^ ]*')\n",[995,39948,39949],{"class":997,"line":30354},[995,39950,39951],{"class":1023},"          echo \"url=$url\" >> \"$GITHUB_OUTPUT\"\n",[14,39953,8896,39954,39956,39957,39960,39961,239],{},[253,39955,30195],{}," group keyed on ",[253,39958,39959],{},"github.head_ref"," cancels an in-flight preview build when a reviewer pushes a fix, so a fast-iterating PR never queues redundant deploys. Each branch gets its own deployment; production is untouched because the deploy step never runs on a push to ",[253,39962,27507],{},[34,39964,39966],{"id":39965},"posting-the-url-back-to-the-pr","Posting the URL Back to the PR",[14,39968,39969],{},"A preview no one can find is useless. Post the URL back as a PR comment so reviewers have one click:",[987,39971,39973],{"className":1912,"code":39972,"language":1914,"meta":712,"style":712},"      - name: Comment preview URL\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,\n              repo: context.repo.repo,\n              body: `Preview ready → ${{ steps.deploy.outputs.url }}`\n            })\n",[253,39974,39975,39986,39995,40001,40010,40015,40020,40025,40030,40035],{"__ignoreMap":712},[995,39976,39977,39979,39981,39983],{"class":997,"line":998},[995,39978,1975],{"class":1618},[995,39980,1922],{"class":1921},[995,39982,1925],{"class":1618},[995,39984,39985],{"class":1023},"Comment preview URL\n",[995,39987,39988,39990,39992],{"class":997,"line":713},[995,39989,30369],{"class":1921},[995,39991,1925],{"class":1618},[995,39993,39994],{"class":1023},"actions\u002Fgithub-script@v7\n",[995,39996,39997,39999],{"class":997,"line":730},[995,39998,1999],{"class":1921},[995,40000,1946],{"class":1618},[995,40002,40003,40006,40008],{"class":997,"line":1544},[995,40004,40005],{"class":1921},"          script",[995,40007,1925],{"class":1618},[995,40009,3215],{"class":1614},[995,40011,40012],{"class":997,"line":1550},[995,40013,40014],{"class":1023},"            github.rest.issues.createComment({\n",[995,40016,40017],{"class":997,"line":1673},[995,40018,40019],{"class":1023},"              issue_number: context.issue.number,\n",[995,40021,40022],{"class":997,"line":1678},[995,40023,40024],{"class":1023},"              owner: context.repo.owner,\n",[995,40026,40027],{"class":997,"line":1693},[995,40028,40029],{"class":1023},"              repo: context.repo.repo,\n",[995,40031,40032],{"class":997,"line":1705},[995,40033,40034],{"class":1023},"              body: `Preview ready → ${{ steps.deploy.outputs.url }}`\n",[995,40036,40037],{"class":997,"line":1711},[995,40038,40039],{"class":1023},"            })\n",[14,40041,40042,40043,40046],{},"Host-managed previews on Netlify and Vercel post this comment automatically; with a self-managed Wrangler deploy you do it yourself via ",[253,40044,40045],{},"actions\u002Fgithub-script",", reading the URL captured from the deploy step.",[34,40048,40050],{"id":40049},"keeping-previews-cheap","Keeping Previews Cheap",[14,40052,40053,40054,40056,40057,40059,40060,40062,40063,40065],{},"Previews rebuild on every push to a PR, so speed is a cost decision, not a nicety. Use the same two caches as the production pipeline — ",[253,40055,2046],{}," plus a framework build cache — and scope the cache key per branch so one PR can't serve another's content. Where the generator supports it, lean on incremental local builds: Eleventy and Jekyll both have ",[253,40058,981],{},", and Hugo warm-starts from a persisted ",[253,40061,3253],{}," cache (",[253,40064,5730],{}," to clean stale resources).",[433,40067,40068,40080],{},[436,40069,40070],{},[439,40071,40072,40075,40078],{},[442,40073,40074],{},"Build scenario",[442,40076,40077],{},"Preview build time",[442,40079,9113],{},[457,40081,40082,40093,40104],{},[439,40083,40084,40087,40090],{},[462,40085,40086],{},"Cold, no cache",[462,40088,40089],{},"4m05s",[462,40091,40092],{},"every dependency and image rebuilt",[439,40094,40095,40098,40101],{},[462,40096,40097],{},"Warm, shared cache key",[462,40099,40100],{},"1m40s",[462,40102,40103],{},"but risks serving another branch's artifacts",[439,40105,40106,40109,40112],{},[462,40107,40108],{},"Warm, per-branch cache key",[462,40110,40111],{},"70s",[462,40113,40114],{},"correct isolation, full speedup",[14,40116,40117,40118,239],{},"The per-branch key is both faster and safer: a shared key occasionally restores a sibling branch's processed content, producing a preview that doesn't match the PR. Choose the host's routing and isolation model via ",[23,40119,28797],{"href":28796},[34,40121,40123],{"id":40122},"quality-gates-on-the-preview","Quality Gates on the Preview",[14,40125,40126],{},"The point of a real URL is that you can test against it. Run Lighthouse, accessibility, and end-to-end checks on the deployed preview and fail the PR if they regress:",[987,40128,40130],{"className":989,"code":40129,"language":991,"meta":712,"style":712},"lhci autorun --collect.url=\"$PREVIEW_URL\"\npa11y-ci --json --threshold 0\nnpx playwright test --reporter=line\n",[253,40131,40132,40148,40162],{"__ignoreMap":712},[995,40133,40134,40136,40138,40141,40143,40146],{"class":997,"line":998},[995,40135,16647],{"class":1007},[995,40137,16650],{"class":1023},[995,40139,40140],{"class":1010}," --collect.url=",[995,40142,18873],{"class":1023},[995,40144,40145],{"class":1618},"$PREVIEW_URL",[995,40147,34967],{"class":1023},[995,40149,40150,40153,40156,40159],{"class":997,"line":713},[995,40151,40152],{"class":1007},"pa11y-ci",[995,40154,40155],{"class":1010}," --json",[995,40157,40158],{"class":1010}," --threshold",[995,40160,40161],{"class":1010}," 0\n",[995,40163,40164,40166,40169,40172],{"class":997,"line":730},[995,40165,1079],{"class":1007},[995,40167,40168],{"class":1023}," playwright",[995,40170,40171],{"class":1023}," test",[995,40173,40174],{"class":1010}," --reporter=line\n",[14,40176,40177,40178,239],{},"Wire these into branch protection so the merge button stays disabled when performance or accessibility drops below baseline. Running Lighthouse against the live preview (not a local server) is what makes the number trustworthy: it exercises the real edge, the real cache headers, and the real assets. This is the deployment-side companion to the lab-vs-field measurement in ",[23,40179,5501],{"href":5500},[34,40181,40183],{"id":40182},"isolation-and-indexing","Isolation and Indexing",[14,40185,40186],{},"Two leaks are easy to ship by accident. The first is secrets: many platforms inherit production environment variables into preview deploys by default, so a contributor's PR — or a fork — could read or spend against production credentials. Scope variables to the preview context and point integrations at staging or dummy keys.",[14,40188,40189,40190,40193,40194,40197],{},"The second is search indexing: a unique preview URL is still a public URL, and crawlers will find it if it leaks. Add a ",[253,40191,40192],{},"noindex"," header or a ",[253,40195,40196],{},"robots.txt"," rule on the preview context, and gate sensitive previews behind platform access controls or a token.",[987,40199,40202],{"className":40200,"code":40201,"language":99,"meta":712},[11603],"# _headers on the preview context only\n\u002F*\n  X-Robots-Tag: noindex\n",[253,40203,40201],{"__ignoreMap":712},[34,40205,40207],{"id":40206},"teardown-on-merge","Teardown on Merge",[14,40209,40210,40211,40213],{},"An ephemeral environment that never dies isn't ephemeral. Trigger cleanup on the ",[253,40212,39068],{}," event so the deployment and its URL are removed whether the PR was merged or abandoned:",[987,40215,40217],{"className":1912,"code":40216,"language":1914,"meta":712,"style":712},"on:\n  pull_request:\n    types: [closed]\njobs:\n  teardown:\n    runs-on: ubuntu-latest\n    steps:\n      - run: npx wrangler pages deployment delete --branch \"${{ github.head_ref }}\"\n        env:\n          CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}\n",[253,40218,40219,40225,40231,40241,40247,40253,40261,40267,40278,40284],{"__ignoreMap":712},[995,40220,40221,40223],{"class":997,"line":998},[995,40222,1933],{"class":1010},[995,40224,1946],{"class":1618},[995,40226,40227,40229],{"class":997,"line":713},[995,40228,30736],{"class":1921},[995,40230,1946],{"class":1618},[995,40232,40233,40235,40237,40239],{"class":997,"line":730},[995,40234,30743],{"class":1921},[995,40236,4044],{"class":1618},[995,40238,39068],{"class":1023},[995,40240,4050],{"class":1618},[995,40242,40243,40245],{"class":997,"line":1544},[995,40244,1943],{"class":1921},[995,40246,1946],{"class":1618},[995,40248,40249,40251],{"class":997,"line":1550},[995,40250,39151],{"class":1921},[995,40252,1946],{"class":1618},[995,40254,40255,40257,40259],{"class":997,"line":1673},[995,40256,1958],{"class":1921},[995,40258,1925],{"class":1618},[995,40260,1963],{"class":1023},[995,40262,40263,40265],{"class":997,"line":1678},[995,40264,1968],{"class":1921},[995,40266,1946],{"class":1618},[995,40268,40269,40271,40273,40275],{"class":997,"line":1693},[995,40270,1975],{"class":1618},[995,40272,2028],{"class":1921},[995,40274,1925],{"class":1618},[995,40276,40277],{"class":1023},"npx wrangler pages deployment delete --branch \"${{ github.head_ref }}\"\n",[995,40279,40280,40282],{"class":997,"line":1705},[995,40281,38885],{"class":1921},[995,40283,1946],{"class":1618},[995,40285,40286,40288,40290],{"class":997,"line":1711},[995,40287,39919],{"class":1921},[995,40289,1925],{"class":1618},[995,40291,32039],{"class":1023},[14,40293,40294,40295,239],{},"Host-managed previews on Netlify and Vercel expire their own deploys on merge, so this explicit job is mainly for self-managed Wrangler pipelines. Either way, the rule is the same: every preview has a defined end. The complete open-to-teardown pipeline is in ",[23,40296,38514],{"href":39520},[34,40298,2266],{"id":2265},[39,40300,40301,40307,40313,40319,40328],{},[42,40302,40303,40306],{},[229,40304,40305],{},"No teardown:"," previews accumulate, raising cost and leaving stale URLs around. Auto-delete on PR close or merge and expire idle ones.",[42,40308,40309,40312],{},[229,40310,40311],{},"Production secrets in previews:"," many platforms inherit them by default. Scope variables to the preview context and use dummy keys for staging integrations.",[42,40314,40315,40318],{},[229,40316,40317],{},"Shared cache across PRs:"," an unscoped build cache serves another branch's content. Use branch-prefixed cache keys or platform-native per-branch isolation.",[42,40320,40321,40324,40325,40327],{},[229,40322,40323],{},"Indexable previews:"," a leaked preview URL gets crawled. Add ",[253,40326,40192],{}," and access controls on the preview context.",[42,40329,40330,40333],{},[229,40331,40332],{},"Testing a local build instead of the preview:"," run Lighthouse and E2E against the deployed URL so checks reflect the real edge and headers.",[34,40335,2321],{"id":2320},[39,40337,40338,40344,40347,40350,40356],{},[42,40339,40340,40341,40343],{},"Build on ",[253,40342,12253],{},", deploy to a branch-scoped URL, and post that URL back to the PR for one-click review.",[42,40345,40346],{},"Keep previews cheap with per-branch cache keys and incremental builds — our warm preview build ran in 70s.",[42,40348,40349],{},"Run Lighthouse, accessibility, and E2E checks against the live preview and gate the merge on them via branch protection.",[42,40351,40352,40353,40355],{},"Scope secrets and add ",[253,40354,40192],{}," so a preview can't read production credentials or get crawled.",[42,40357,40358],{},"Tear every environment down on PR close or merge — an ephemeral deploy must have a defined end.",[34,40360,651],{"id":650},[653,40362,40364],{"id":40363},"how-do-i-keep-pr-previews-from-slowing-down-ci","How do I keep PR previews from slowing down CI?",[14,40366,40367],{},"Cache dependencies and the framework build cache, scope the cache key per branch so one PR cannot serve another's content, and only rebuild when relevant paths change. Use incremental local builds where the generator supports them. On our site these took the warm preview build to about 70 seconds.",[653,40369,40371],{"id":40370},"can-preview-environments-run-automated-tests-before-merge","Can preview environments run automated tests before merge?",[14,40373,40374],{},"Yes, and that is the main reason to have a real URL. Run Lighthouse CI, accessibility checks, and Playwright end-to-end tests against the live preview URL, then wire those checks into branch protection so a regression blocks the merge instead of shipping.",[653,40376,40378],{"id":40377},"how-are-preview-urls-kept-out-of-search-results","How are preview URLs kept out of search results?",[14,40380,40381,40382,40384],{},"Previews get a unique per-branch subdomain that you should not link publicly. Add a ",[253,40383,40192],{}," header or robots rule on the preview context, and restrict access with platform access controls or a token if the content is sensitive, so crawlers never index a throwaway environment.",[653,40386,40388],{"id":40387},"what-happens-to-a-preview-when-the-pr-is-merged-or-closed","What happens to a preview when the PR is merged or closed?",[14,40390,40391,40392,40394],{},"It should be torn down automatically. Trigger teardown on the ",[253,40393,12253],{}," closed event so the ephemeral deployment and its URL are removed. Host-managed previews on Netlify and Vercel expire their own deploys, but self-managed ones need an explicit cleanup job.",[653,40396,40398],{"id":40397},"why-is-my-preview-reading-production-secrets","Why is my preview reading production secrets?",[14,40400,40401],{},"Many platforms inherit production environment variables into preview deploys by default. Scope variables to the preview context and point integrations at staging or dummy keys, so a pull request from a fork or a contributor can never read or spend against production credentials.",[653,40403,40405],{"id":40404},"do-preview-environments-cost-much-to-run","Do preview environments cost much to run?",[14,40407,40408],{},"For a static site they are cheap — each preview is a static deploy, not a running server. The cost is build minutes and storage for old deploys, which is why teardown on merge and per-branch cache scoping matter more than raw compute.",[34,40410,684],{"id":683},[39,40412,40413,40420,40425,40430],{},[42,40414,40415,692,40417,40419],{},[229,40416,691],{},[23,40418,5505],{"href":5504}," — where preview deploys fit reproducible, atomic releases.",[42,40421,40422,40424],{},[23,40423,38514],{"href":39520}," — the full open-to-teardown recipe.",[42,40426,40427,40429],{},[23,40428,28797],{"href":28796}," — host-managed preview routing and isolation.",[42,40431,40432,40434],{},[23,40433,28200],{"href":28199}," — the build and runner mechanics underneath.",[1346,40436,40437],{},"html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}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 .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}",{"title":712,"searchDepth":713,"depth":713,"links":40439},[40440,40441,40442,40443,40444,40445,40446,40447,40448,40449,40457],{"id":39677,"depth":713,"text":39678},{"id":39721,"depth":713,"text":39722},{"id":39965,"depth":713,"text":39966},{"id":40049,"depth":713,"text":40050},{"id":40122,"depth":713,"text":40123},{"id":40182,"depth":713,"text":40183},{"id":40206,"depth":713,"text":40207},{"id":2265,"depth":713,"text":2266},{"id":2320,"depth":713,"text":2321},{"id":650,"depth":713,"text":651,"children":40450},[40451,40452,40453,40454,40455,40456],{"id":40363,"depth":730,"text":40364},{"id":40370,"depth":730,"text":40371},{"id":40377,"depth":730,"text":40378},{"id":40387,"depth":730,"text":40388},{"id":40397,"depth":730,"text":40398},{"id":40404,"depth":730,"text":40405},{"id":683,"depth":713,"text":684},[40459,40460,40461],{"name":737,"item":738},{"name":5505,"item":5504},{"name":28330,"item":28329},"Give every pull request its own deployed URL so reviewers test the real built site — ephemeral deploys, quality gates against the preview, and teardown on merge.",[40464,40465,40466,40468,40470,40471],{"q":40364,"a":40367},{"q":40371,"a":40374},{"q":40378,"a":40467},"Previews get a unique per-branch subdomain that you should not link publicly. Add a noindex header or robots rule on the preview context, and restrict access with platform access controls or a token if the content is sensitive, so crawlers never index a throwaway environment.",{"q":40388,"a":40469},"It should be torn down automatically. Trigger teardown on the pull_request closed event so the ephemeral deployment and its URL are removed. Host-managed previews on Netlify and Vercel expire their own deploys, but self-managed ones need an explicit cleanup job.",{"q":40398,"a":40401},{"q":40405,"a":40408},{},"\u002Fproduction-ready-deployment-cicd-workflows\u002Fpreview-environments-for-pull-requests",{"title":28330,"description":40462},"production-ready-deployment-cicd-workflows\u002Fpreview-environments-for-pull-requests\u002Findex","z7XQxP2WNA-pf3bq5-rav9CrY4okmb1nbIzCF6g0Hsw",1781789805477]