[{"data":1,"prerenderedAt":2155},["ShallowReactive",2],{"navigation":3,"\u002Fdeployment\u002Fcloudflare":413,"\u002Fdeployment\u002Fcloudflare-surround":2152},[4,14,36,69,144,384],{"title":5,"path":6,"stem":7,"children":8},"Introduction","\u002Fgetting-started","1.getting-started\u002F1.index",[9,10],{"title":5,"path":6,"stem":7},{"title":11,"path":12,"stem":13},"Installation","\u002Fgetting-started\u002Finstallation","1.getting-started\u002F2.installation",{"title":15,"path":16,"stem":17,"children":18,"page":35},"Guides","\u002Fguides","2.guides",[19,23,27,31],{"title":20,"path":21,"stem":22},"Your First Layout","\u002Fguides\u002Fyour-first-layout","2.guides\u002F1.your-first-layout",{"title":24,"path":25,"stem":26},"Your First Page Template","\u002Fguides\u002Fyour-first-page-template","2.guides\u002F2.your-first-page-template",{"title":28,"path":29,"stem":30},"Your First Component","\u002Fguides\u002Fyour-first-component","2.guides\u002F3.your-first-component",{"title":32,"path":33,"stem":34},"Alternative UI Variants","\u002Fguides\u002Falternative-ui-variants","2.guides\u002F4.alternative-ui-variants",false,{"title":37,"path":38,"stem":39,"children":40,"page":35},"Core Concepts","\u002Fcore-concepts","3.core-concepts",[41,45,49,53,57,61,65],{"title":42,"path":43,"stem":44},"How It All Works","\u002Fcore-concepts\u002Farchitecture","3.core-concepts\u002F1.architecture",{"title":46,"path":47,"stem":48},"The Data Model","\u002Fcore-concepts\u002Fthe-data-model","3.core-concepts\u002F2.the-data-model",{"title":50,"path":51,"stem":52},"Layouts & Pages","\u002Fcore-concepts\u002Flayouts-and-pages","3.core-concepts\u002F3.layouts-and-pages",{"title":54,"path":55,"stem":56},"Dynamic Pages","\u002Fcore-concepts\u002Fdynamic-pages","3.core-concepts\u002F4.dynamic-pages",{"title":58,"path":59,"stem":60},"Components","\u002Fcore-concepts\u002Fcomponents","3.core-concepts\u002F5.components",{"title":62,"path":63,"stem":64},"Draft & Publish Workflow","\u002Fcore-concepts\u002Fdraft-and-publish","3.core-concepts\u002F6.draft-and-publish",{"title":66,"path":67,"stem":68},"The Admin Panel","\u002Fcore-concepts\u002Fadmin-panel","3.core-concepts\u002F7.admin-panel",{"title":70,"path":71,"stem":72,"children":73,"page":35},"Api","\u002Fapi","4.api",[74,78,120,124,128,132,136,140],{"title":75,"path":76,"stem":77},"Bundle Setup","\u002Fapi\u002Fbundle-setup","4.api\u002F1.bundle-setup",{"title":58,"path":79,"stem":80,"children":81,"page":35},"\u002Fapi\u002Fcomponents","4.api\u002F2.components",[82,86,107],{"title":83,"path":84,"stem":85},"Creating Components","\u002Fapi\u002Fcomponents\u002Fcreating-components","4.api\u002F2.components\u002F1.creating-components",{"title":87,"path":88,"stem":89,"children":90,"page":35},"Annotations","\u002Fapi\u002Fcomponents\u002Fannotations","4.api\u002F2.components\u002F2.annotations",[91,95,99,103],{"title":92,"path":93,"stem":94},"Publishable","\u002Fapi\u002Fcomponents\u002Fannotations\u002Fpublishable","4.api\u002F2.components\u002F2.annotations\u002F1.publishable",{"title":96,"path":97,"stem":98},"Uploadable","\u002Fapi\u002Fcomponents\u002Fannotations\u002Fuploadable","4.api\u002F2.components\u002F2.annotations\u002F2.uploadable",{"title":100,"path":101,"stem":102},"Timestamped","\u002Fapi\u002Fcomponents\u002Fannotations\u002Ftimestamped","4.api\u002F2.components\u002F2.annotations\u002F3.timestamped",{"title":104,"path":105,"stem":106},"Explicit Allow Only","\u002Fapi\u002Fcomponents\u002Fannotations\u002Fexplicit-allow-only","4.api\u002F2.components\u002F2.annotations\u002F4.explicit-allow-only",{"title":108,"path":109,"stem":110,"children":111,"page":35},"Built Ins","\u002Fapi\u002Fcomponents\u002Fbuilt-ins","4.api\u002F2.components\u002F3.built-ins",[112,116],{"title":113,"path":114,"stem":115},"Collection Component","\u002Fapi\u002Fcomponents\u002Fbuilt-ins\u002Fcollection-component","4.api\u002F2.components\u002F3.built-ins\u002F1.collection-component",{"title":117,"path":118,"stem":119},"Form Component","\u002Fapi\u002Fcomponents\u002Fbuilt-ins\u002Fform-component","4.api\u002F2.components\u002F3.built-ins\u002F2.form-component",{"title":121,"path":122,"stem":123},"Dynamic & Nested Pages","\u002Fapi\u002Fdynamic-pages","4.api\u002F3.dynamic-pages",{"title":125,"path":126,"stem":127},"Users & Security","\u002Fapi\u002Fusers-and-security","4.api\u002F4.users-and-security",{"title":129,"path":130,"stem":131},"Data Fixtures","\u002Fapi\u002Fdata-fixtures","4.api\u002F5.data-fixtures",{"title":133,"path":134,"stem":135},"Configuration Reference","\u002Fapi\u002Fconfiguration","4.api\u002F6.configuration",{"title":137,"path":138,"stem":139},"Console Commands","\u002Fapi\u002Fconsole-commands","4.api\u002F7.console-commands",{"title":141,"path":142,"stem":143},"Debugging & Profiler","\u002Fapi\u002Fdebugging","4.api\u002F8.debugging",{"title":145,"path":146,"stem":147,"children":148,"page":35},"Nuxt Module","\u002Fnuxt-module","5.nuxt-module",[149,153,157,161,174,194,215,248,332,356,360],{"title":150,"path":151,"stem":152},"Module Setup","\u002Fnuxt-module\u002Fmodule-setup","5.nuxt-module\u002F1.module-setup",{"title":154,"path":155,"stem":156},"PWA & Offline","\u002Fnuxt-module\u002Fpwa","5.nuxt-module\u002F10.pwa",{"title":158,"path":159,"stem":160},"Page Caching","\u002Fnuxt-module\u002Fpage-caching","5.nuxt-module\u002F11.page-caching",{"title":162,"path":163,"stem":164,"children":165,"page":35},"Configuration","\u002Fnuxt-module\u002Fconfiguration","5.nuxt-module\u002F2.configuration",[166,170],{"title":167,"path":168,"stem":169},"Nuxt Config","\u002Fnuxt-module\u002Fconfiguration\u002Fnuxt-config","5.nuxt-module\u002F2.configuration\u002F1.nuxt-config",{"title":171,"path":172,"stem":173},"Site Config & SEO","\u002Fnuxt-module\u002Fconfiguration\u002Fsite-config-and-seo","5.nuxt-module\u002F2.configuration\u002F2.site-config-and-seo",{"title":175,"path":176,"stem":177,"children":178,"page":35},"Building Your Ui","\u002Fnuxt-module\u002Fbuilding-your-ui","5.nuxt-module\u002F3.building-your-ui",[179,183,187,190],{"title":180,"path":181,"stem":182},"Layouts","\u002Fnuxt-module\u002Fbuilding-your-ui\u002Fcreating-layouts","5.nuxt-module\u002F3.building-your-ui\u002F1.creating-layouts",{"title":184,"path":185,"stem":186},"Page Templates","\u002Fnuxt-module\u002Fbuilding-your-ui\u002Fcreating-page-templates","5.nuxt-module\u002F3.building-your-ui\u002F2.creating-page-templates",{"title":83,"path":188,"stem":189},"\u002Fnuxt-module\u002Fbuilding-your-ui\u002Fcreating-components","5.nuxt-module\u002F3.building-your-ui\u002F3.creating-components",{"title":191,"path":192,"stem":193},"CLI Generator","\u002Fnuxt-module\u002Fbuilding-your-ui\u002Fcwa-cli","5.nuxt-module\u002F3.building-your-ui\u002F4.cwa-cli",{"title":195,"path":196,"stem":197,"children":198,"page":35},"Cwa Components","\u002Fnuxt-module\u002Fcwa-components","5.nuxt-module\u002F4.cwa-components",[199,203,207,211],{"title":200,"path":201,"stem":202},"\u003CCwaComponentGroup \u002F>","\u002Fnuxt-module\u002Fcwa-components\u002Fcwa-component-group","5.nuxt-module\u002F4.cwa-components\u002F1.cwa-component-group",{"title":204,"path":205,"stem":206},"\u003CCwaPage \u002F>","\u002Fnuxt-module\u002Fcwa-components\u002Fcwa-page","5.nuxt-module\u002F4.cwa-components\u002F2.cwa-page",{"title":208,"path":209,"stem":210},"\u003CCwaLink \u002F>","\u002Fnuxt-module\u002Fcwa-components\u002Fcwa-link","5.nuxt-module\u002F4.cwa-components\u002F3.cwa-link",{"title":212,"path":213,"stem":214},"\u003CCwaDefaultLayout \u002F>","\u002Fnuxt-module\u002Fcwa-components\u002Fcwa-default-layout","5.nuxt-module\u002F4.cwa-components\u002F5.cwa-default-layout",{"title":216,"path":217,"stem":218,"children":219,"page":35},"Cwa Api","\u002Fnuxt-module\u002Fcwa-api","5.nuxt-module\u002F5.cwa-api",[220,224,228,232,236,240,244],{"title":221,"path":222,"stem":223},"Overview","\u002Fnuxt-module\u002Fcwa-api\u002Foverview","5.nuxt-module\u002F5.cwa-api\u002F1.overview",{"title":225,"path":226,"stem":227},"Resources","\u002Fnuxt-module\u002Fcwa-api\u002Fresources","5.nuxt-module\u002F5.cwa-api\u002F2.resources",{"title":229,"path":230,"stem":231},"Resources Manager","\u002Fnuxt-module\u002Fcwa-api\u002Fresources-manager","5.nuxt-module\u002F5.cwa-api\u002F3.resources-manager",{"title":233,"path":234,"stem":235},"Auth","\u002Fnuxt-module\u002Fcwa-api\u002Fauth","5.nuxt-module\u002F5.cwa-api\u002F4.auth",{"title":237,"path":238,"stem":239},"Forms","\u002Fnuxt-module\u002Fcwa-api\u002Fforms","5.nuxt-module\u002F5.cwa-api\u002F5.forms",{"title":241,"path":242,"stem":243},"Admin","\u002Fnuxt-module\u002Fcwa-api\u002Fadmin","5.nuxt-module\u002F5.cwa-api\u002F6.admin",{"title":245,"path":246,"stem":247},"Site Config","\u002Fnuxt-module\u002Fcwa-api\u002Fsite-config","5.nuxt-module\u002F5.cwa-api\u002F7.site-config",{"title":249,"path":250,"stem":251,"children":252,"page":35},"Composables","\u002Fnuxt-module\u002Fcomposables","5.nuxt-module\u002F6.composables",[253,261,298,315],{"title":254,"path":255,"stem":256,"children":257,"page":35},"Layout","\u002Fnuxt-module\u002Fcomposables\u002Flayout","5.nuxt-module\u002F6.composables\u002F0.layout",[258],{"title":254,"path":259,"stem":260},"\u002Fnuxt-module\u002Fcomposables\u002Flayout\u002Fuse-cwa-layout","5.nuxt-module\u002F6.composables\u002F0.layout\u002F1.use-cwa-layout",{"title":262,"path":263,"stem":264,"children":265,"page":35},"Component","\u002Fnuxt-module\u002Fcomposables\u002Fcomponent","5.nuxt-module\u002F6.composables\u002F1.component",[266,270,274,278,282,286,290,294],{"title":267,"path":268,"stem":269},"Component (recommended)","\u002Fnuxt-module\u002Fcomposables\u002Fcomponent\u002Fuse-cwa-component","5.nuxt-module\u002F6.composables\u002F1.component\u002F0.use-cwa-component",{"title":271,"path":272,"stem":273},"Resource","\u002Fnuxt-module\u002Fcomposables\u002Fcomponent\u002Fuse-cwa-resource","5.nuxt-module\u002F6.composables\u002F1.component\u002F1.use-cwa-resource",{"title":275,"path":276,"stem":277},"Collection Resource","\u002Fnuxt-module\u002Fcomposables\u002Fcomponent\u002Fuse-cwa-collection-resource","5.nuxt-module\u002F6.composables\u002F1.component\u002F2.use-cwa-collection-resource",{"title":279,"path":280,"stem":281},"File Field","\u002Fnuxt-module\u002Fcomposables\u002Fcomponent\u002Fuse-cwa-file-field","5.nuxt-module\u002F6.composables\u002F1.component\u002F3.use-cwa-file-field",{"title":283,"path":284,"stem":285},"Form","\u002Fnuxt-module\u002Fcomposables\u002Fcomponent\u002Fuse-cwa-form","5.nuxt-module\u002F6.composables\u002F1.component\u002F4.use-cwa-form",{"title":287,"path":288,"stem":289},"Form Input","\u002Fnuxt-module\u002Fcomposables\u002Fcomponent\u002Fuse-cwa-form-input","5.nuxt-module\u002F6.composables\u002F1.component\u002F5.use-cwa-form-input",{"title":291,"path":292,"stem":293},"Form Repeated","\u002Fnuxt-module\u002Fcomposables\u002Fcomponent\u002Fuse-cwa-form-repeated","5.nuxt-module\u002F6.composables\u002F1.component\u002F6.use-cwa-form-repeated",{"title":295,"path":296,"stem":297},"Form Collection","\u002Fnuxt-module\u002Fcomposables\u002Fcomponent\u002Fuse-cwa-form-collection","5.nuxt-module\u002F6.composables\u002F1.component\u002F7.use-cwa-form-collection",{"title":299,"path":300,"stem":301,"children":302,"page":35},"Admin Manager","\u002Fnuxt-module\u002Fcomposables\u002Fadmin-manager","5.nuxt-module\u002F6.composables\u002F2.admin-manager",[303,307,311],{"title":304,"path":305,"stem":306},"Manager Tab","\u002Fnuxt-module\u002Fcomposables\u002Fadmin-manager\u002Fuse-cwa-resource-manager-tab","5.nuxt-module\u002F6.composables\u002F2.admin-manager\u002F1.use-cwa-resource-manager-tab",{"title":308,"path":309,"stem":310},"Resource Model","\u002Fnuxt-module\u002Fcomposables\u002Fadmin-manager\u002Fuse-cwa-resource-model","5.nuxt-module\u002F6.composables\u002F2.admin-manager\u002F2.use-cwa-resource-model",{"title":312,"path":313,"stem":314},"Resource Upload","\u002Fnuxt-module\u002Fcomposables\u002Fadmin-manager\u002Fuse-cwa-resource-upload","5.nuxt-module\u002F6.composables\u002F2.admin-manager\u002F3.use-cwa-resource-upload",{"title":316,"path":317,"stem":318,"children":319,"page":35},"Utilities","\u002Fnuxt-module\u002Fcomposables\u002Futilities","5.nuxt-module\u002F6.composables\u002F3.utilities",[320,324,328],{"title":321,"path":322,"stem":323},"Resource Endpoint","\u002Fnuxt-module\u002Fcomposables\u002Futilities\u002Fuse-cwa-resource-endpoint","5.nuxt-module\u002F6.composables\u002F3.utilities\u002F1.use-cwa-resource-endpoint",{"title":325,"path":326,"stem":327},"Query Model","\u002Fnuxt-module\u002Fcomposables\u002Futilities\u002Fuse-query-bound-model","5.nuxt-module\u002F6.composables\u002F3.utilities\u002F2.use-query-bound-model",{"title":329,"path":330,"stem":331},"Resource Route","\u002Fnuxt-module\u002Fcomposables\u002Futilities\u002Fuse-cwa-resource-route","5.nuxt-module\u002F6.composables\u002F3.utilities\u002F3.use-cwa-resource-route",{"title":333,"path":334,"stem":335,"children":336,"page":35},"Component Helpers","\u002Fnuxt-module\u002Fcomponent-helpers","5.nuxt-module\u002F7.component-helpers",[337,341,345,349,353],{"title":338,"path":339,"stem":340},"Images & Media","\u002Fnuxt-module\u002Fcomponent-helpers\u002Fimages-and-uploads","5.nuxt-module\u002F7.component-helpers\u002F1.images-and-uploads",{"title":342,"path":343,"stem":344},"Collections & Pagination","\u002Fnuxt-module\u002Fcomponent-helpers\u002Fcollections-and-pagination","5.nuxt-module\u002F7.component-helpers\u002F2.collections-and-pagination",{"title":346,"path":347,"stem":348},"HTML Content","\u002Fnuxt-module\u002Fcomponent-helpers\u002Fhtml-content","5.nuxt-module\u002F7.component-helpers\u002F3.html-content",{"title":350,"path":351,"stem":352},"Real-Time Updates","\u002Fnuxt-module\u002Fcomponent-helpers\u002Freal-time-updates","5.nuxt-module\u002F7.component-helpers\u002F4.real-time-updates",{"title":237,"path":354,"stem":355},"\u002Fnuxt-module\u002Fcomponent-helpers\u002Fforms","5.nuxt-module\u002F7.component-helpers\u002F5.forms",{"title":357,"path":358,"stem":359},"Authentication","\u002Fnuxt-module\u002Fauthentication","5.nuxt-module\u002F8.authentication",{"title":361,"path":362,"stem":363,"children":364,"page":35},"Cwa Layer","\u002Fnuxt-module\u002Fcwa-layer","5.nuxt-module\u002F9.cwa-layer",[365,368,372,376,380],{"title":221,"path":366,"stem":367},"\u002Fnuxt-module\u002Fcwa-layer\u002Foverview","5.nuxt-module\u002F9.cwa-layer\u002F1.overview",{"title":369,"path":370,"stem":371},"Auth Pages","\u002Fnuxt-module\u002Fcwa-layer\u002Fauth-pages","5.nuxt-module\u002F9.cwa-layer\u002F2.auth-pages",{"title":373,"path":374,"stem":375},"Admin Panel","\u002Fnuxt-module\u002Fcwa-layer\u002Fadmin-panel","5.nuxt-module\u002F9.cwa-layer\u002F3.admin-panel",{"title":377,"path":378,"stem":379},"Styling","\u002Fnuxt-module\u002Fcwa-layer\u002Fstyling","5.nuxt-module\u002F9.cwa-layer\u002F4.styling",{"title":381,"path":382,"stem":383},"Date Pickers","\u002Fnuxt-module\u002Fcwa-layer\u002Fdate-pickers","5.nuxt-module\u002F9.cwa-layer\u002F5.date-pickers",{"title":385,"path":386,"stem":387,"children":388,"page":35},"Deployment","\u002Fdeployment","6.deployment",[389,393,397,401,405,409],{"title":390,"path":391,"stem":392},"Docker","\u002Fdeployment\u002Fdocker","6.deployment\u002F1.docker",{"title":394,"path":395,"stem":396},"Kubernetes & Helm","\u002Fdeployment\u002Fkubernetes","6.deployment\u002F2.kubernetes",{"title":398,"path":399,"stem":400},"CI\u002FCD","\u002Fdeployment\u002Fci-cd","6.deployment\u002F3.ci-cd",{"title":402,"path":403,"stem":404},"Load Testing","\u002Fdeployment\u002Fload-testing","6.deployment\u002F4.load-testing",{"title":406,"path":407,"stem":408},"Faster Functional Tests","\u002Fdeployment\u002Ffaster-functional-tests","6.deployment\u002F5.faster-functional-tests",{"title":410,"path":411,"stem":412},"Cloudflare","\u002Fdeployment\u002Fcloudflare","6.deployment\u002F6.cloudflare",{"id":414,"title":410,"badge":415,"body":416,"description":2146,"extension":2147,"links":415,"meta":2148,"navigation":2149,"path":411,"seo":2150,"stem":412,"__hash__":2151},"docs\u002F6.deployment\u002F6.cloudflare.md",null,{"type":417,"value":418,"toc":2125},"minimark",[419,423,453,461,466,471,500,520,531,535,552,555,604,615,625,629,636,668,707,762,769,773,835,844,862,867,877,881,893,910,1074,1078,1101,1104,1136,1151,1159,1206,1209,1286,1300,1330,1339,1358,1372,1375,1395,1399,1405,1435,1456,1462,1494,1498,1509,1515,1518,1560,1591,1622,1625,1670,1692,1695,1704,1716,1723,1727,1730,1736,1779,1798,1813,1820,1837,1841,1847,1867,1873,1876,1879,1927,1930,1934,1939,1980,1985,2121],[420,421,422],"p",{},"Cloudflare can sit in front of a CWA site as a proxy: it hides the origin, ends TLS close to the visitor, and blocks some abusive traffic before it reaches your cluster. There are two ways to set it up:",[424,425,426,443],"ul",{},[427,428,429,433,434,438,439,442],"li",{},[430,431,432],"strong",{},"Option A, proxy only."," Cloudflare passes requests through and caches no pages. Souin, in the php container, still does all the ",[435,436,437],"a",{"href":159},"page caching",". ",[430,440,441],{},"This is the recommended default:"," it has the fewest moving parts.",[427,444,445,448,449,452],{},[430,446,447],{},"Option B, proxy with edge caching."," Cloudflare also caches page HTML, and Souin purges it there. It has been ",[430,450,451],{},"tested end to end on the Free plan"," and is a supported choice when you want pages served from Cloudflare's edge.",[420,454,455,456,460],{},"Both end with a ",[435,457,459],{"href":458},"#per-site-checklist","per-site checklist",".",[462,463,465],"h2",{"id":464},"option-a-proxy-only","Option A: proxy only",[467,468,470],"h3",{"id":469},"dns-and-tls","DNS and TLS",[472,473,474,485],"ol",{},[427,475,476,477,480,481,484],{},"In ",[430,478,479],{},"DNS",", set the site's records to ",[430,482,483],{},"Proxied"," (the orange cloud).",[427,486,476,487,490,491,494,495,499],{},[430,488,489],{},"SSL\u002FTLS → Overview",", set the encryption mode to ",[430,492,493],{},"Full (strict)",". Cloudflare then connects to your origin over HTTPS and checks its certificate. The template's ingress already serves a Let's Encrypt certificate (see ",[435,496,498],{"href":497},"\u002Fdeployment\u002Fkubernetes#ingress-with-tls","Ingress with TLS","), so strict mode works as soon as that certificate is issued.",[501,502,505],"callout",{"icon":503,"color":504},"i-heroicons-exclamation-triangle","warning",[420,506,507,510,511,513,514,517,518,460],{},[430,508,509],{},"Get the origin certificate before you turn the proxy on."," With ",[430,512,493],{},", Cloudflare refuses an origin whose certificate isn't valid, and visitors get a Cloudflare error page. For a new site or a new hostname, the simplest order is: leave the record ",[430,515,516],{},"DNS only"," (grey cloud), deploy, check that the site serves a valid certificate, then switch the record to ",[430,519,483],{},[501,521,523],{"icon":522},"i-heroicons-photo",[420,524,525,528,529,460],{},[430,526,527],{},"Image placeholder:"," Cloudflare's SSL\u002FTLS overview with the encryption mode set to ",[430,530,493],{},[467,532,534],{"id":533},"leave-cloudflares-cache-alone","Leave Cloudflare's cache alone",[420,536,537,538,542,543,546,547,551],{},"Don't add Cache Rules in Option A. By default Cloudflare doesn't cache HTML or JSON, so pages and ",[539,540,541],"code",{},"\u002F_api"," responses pass straight through to Souin. Their ",[539,544,545],{},"s-maxage"," is meant for Souin, which is purged on every write. Without the setup in ",[435,548,550],{"href":549},"#option-b-proxy-with-edge-caching","Option B",", purges don't reach Cloudflare, so it must not keep them.",[420,553,554],{},"Check it with any page:",[556,557,562],"pre",{"className":558,"code":559,"language":560,"meta":561,"style":561},"language-sh shiki shiki-themes github-light github-dark material-theme-palenight","curl -sI https:\u002F\u002Fwww.example.com\u002F | grep -i -E 'cf-cache-status|cache-status'\n","sh","",[539,563,564],{"__ignoreMap":561},[565,566,569,573,577,581,585,588,591,594,598,601],"span",{"class":567,"line":568},"line",1,[565,570,572],{"class":571},"sRCss","curl",[565,574,576],{"class":575},"szhYu"," -sI",[565,578,580],{"class":579},"sLL54"," https:\u002F\u002Fwww.example.com\u002F",[565,582,584],{"class":583},"sVlFx"," |",[565,586,587],{"class":571}," grep",[565,589,590],{"class":575}," -i",[565,592,593],{"class":575}," -E",[565,595,597],{"class":596},"seSrl"," '",[565,599,600],{"class":579},"cf-cache-status|cache-status",[565,602,603],{"class":596},"'\n",[420,605,606,607,610,611,614],{},"You should see ",[539,608,609],{},"cf-cache-status: DYNAMIC"," (Cloudflare didn't cache it) next to Souin's own ",[539,612,613],{},"cache-status"," header.",[501,616,618],{"icon":617},"i-heroicons-information-circle",[420,619,620,621,624],{},"Cloudflare's defaults still cache static files by file extension: images, scripts, stylesheets, PDFs and so on, for as long as their headers allow. That's safe for ",[539,622,623],{},"\u002F_nuxt\u002F*",", whose names change with every build, and for uploads, whose stored names are unique. If you delete an upload and it must disappear at once, purge its URL in Cloudflare too.",[467,626,628],{"id":627},"real-client-ips","Real client IPs",[420,630,631,632,635],{},"Behind Cloudflare, every request reaches your cluster from a Cloudflare address, and Cloudflare sends the visitor's address in the ",[539,633,634],{},"CF-Connecting-IP"," header. The template's Caddy rate limit already handles this, so there's nothing to configure for Cloudflare:",[424,637,638,655,658],{},[427,639,640,641,643,644,650,651,654],{},"Caddy uses ",[539,642,634],{}," only when the request comes from one of ",[435,645,649],{"href":646,"rel":647},"https:\u002F\u002Fwww.cloudflare.com\u002Fips\u002F",[648],"nofollow","Cloudflare's published ranges",". The ranges are built into the Caddyfile, and ",[539,652,653],{},"CLOUDFLARE_IP_RANGES"," replaces them if Cloudflare adds new ones. From any other address the header is ignored, because anyone can send it.",[427,656,657],{},"Each visitor behind Cloudflare is counted separately, so one busy visitor can't get the whole Cloudflare edge throttled.",[427,659,660,663,664,667],{},[539,661,662],{},"CADDY_EDGE=true"," (the default in ",[539,665,666],{},"compose.prod.yaml",", where Caddy faces the internet itself) still recognises Cloudflare by its ranges.",[501,669,670],{"icon":503,"color":504},[420,671,672,675,676,679,680,683,684,687,688,691,692,695,696,703,704,706],{},[430,673,674],{},"Symfony doesn't see the visitor's address."," ",[539,677,678],{},"Request::getClientIp()"," trusts ",[539,681,682],{},"X-Forwarded-For"," only from ",[539,685,686],{},"TRUSTED_PROXIES"," (private ranges), so behind Cloudflare it returns a Cloudflare address. Don't rely on the client IP in your own code. Caddy's access log records the visitor as ",[539,689,690],{},"visitor_ip",", next to ",[539,693,694],{},"client_ip",", which is Cloudflare's (template ",[435,697,700],{"href":698,"rel":699},"https:\u002F\u002Fgithub.com\u002Fcomponents-web-app\u002Fcomponents-web-app\u002Fcommit\u002Fb3b8622d3fc1375da564668aab3dea9053e1807f",[648],[539,701,702],{},"b3b8622","). Don't add Cloudflare's ranges to ",[539,705,686],{}," either: that would let any client that connects through Cloudflare choose its own address.",[501,708,709],{"icon":617},[420,710,711,722,723,725,726,729,730,733,734,737,738,741,742,745,746,748,749,752,753,755,756,759,760,460],{},[430,712,713,714,721],{},"Projects created before template ",[435,715,718],{"href":716,"rel":717},"https:\u002F\u002Fgithub.com\u002Fcomponents-web-app\u002Fcomponents-web-app\u002Fcommit\u002Fb5aa360393fe77f0d2dbfd3ca33fa44bf7fe069b",[648],[539,719,720],{},"b5aa360",":"," the ",[539,724,686],{}," default in ",[539,727,728],{},"api\u002F.env"," listed ",[539,731,732],{},"172.0.0.0\u002F8",", which includes public addresses such as Cloudflare's ",[539,735,736],{},"172.64.0.0\u002F13",". It's now the private range ",[539,739,740],{},"172.16.0.0\u002F12",". The Helm chart and ",[539,743,744],{},"compose.yaml"," always set ",[539,747,740],{},", and a real environment variable wins over ",[539,750,751],{},".env",", so no template deployment used the old value. If you copied it into your project's ",[539,754,751],{},", ",[539,757,758],{},".env.local"," or anything else, change it to ",[539,761,740],{},[420,763,764,765,768],{},"The template's optional ingress-nginx limits (",[539,766,767],{},"INGRESS_RATE_LIMIT_RPS"," and the related variables) count by the address that reached nginx. Behind Cloudflare that is a Cloudflare server, shared by many visitors, so leave them unset.",[467,770,772],{"id":771},"free-protection-worth-turning-on","Free protection worth turning on",[774,775,776,792],"table",{},[777,778,779],"thead",{},[780,781,782,786,789],"tr",{},[783,784,785],"th",{},"Setting",[783,787,788],{},"Where",[783,790,791],{},"What it does",[793,794,795,809,822],"tbody",{},[780,796,797,803,806],{},[798,799,800],"td",{},[430,801,802],{},"Rate limiting rule",[798,804,805],{},"Security rules → Rate limiting rules",[798,807,808],{},"Blocks an IP that sends too many matching requests.",[780,810,811,816,819],{},[798,812,813],{},[430,814,815],{},"Bot Fight Mode",[798,817,818],{},"Security → Settings",[798,820,821],{},"Challenges traffic that Cloudflare identifies as automated.",[780,823,824,829,832],{},[798,825,826],{},[430,827,828],{},"Under Attack Mode",[798,830,831],{},"the zone's Overview → Quick Actions",[798,833,834],{},"Emergency switch: every visitor gets an interstitial check before the site loads.",[420,836,837,840,841,843],{},[430,838,839],{},"Rate limiting."," The Free plan allows one rule, counted per IP over 10 seconds, and its expression can match only on the path. Pro allows two. Matching on the query string (to catch cache-busting requests) needs Pro or above. A rule on paths starting with ",[539,842,541],{}," is a reasonable start. SSR reaches the API inside the cluster, not through Cloudflare, so this counts only browser requests. Set the threshold generously: editors in one office share an IP, and the admin makes many API calls while editing. The template's own Caddy rate limit still applies behind it.",[420,845,846,848,849,852,853,857,858,861],{},[430,847,815],{}," can't be skipped by any rule or limited to some paths. It may challenge non-browser clients, including your pipeline's cache warm and performance audit, an uptime monitor, or a ",[435,850,851],{"href":403},"load test",". After you turn it on, check that the next deploy's ",[435,854,856],{"href":855},"\u002Fdeployment\u002Fci-cd#warming-the-cache-after-a-deploy","cache warm"," still gets ",[539,859,860],{},"200","s.",[420,863,864,866],{},[430,865,828],{}," needs JavaScript to pass, so it challenges curl, your CI jobs and k6 while it's on. Use it only during an attack, and turn it off afterwards.",[501,868,869],{"icon":522},[420,870,871,873,874,876],{},[430,872,527],{}," the rate limiting rule editor with a path rule for ",[539,875,541],{},", counted per IP over 10 seconds.",[462,878,880],{"id":879},"option-b-proxy-with-edge-caching","Option B: proxy with edge caching",[420,882,883,884,887,888,460],{},"Edge caching needs a patched Souin whose Cloudflare purge works, and a ",[539,885,886],{},"Cache-Tag"," header on every tagged response. The template has both since ",[435,889,892],{"href":890,"rel":891},"https:\u002F\u002Fgithub.com\u002Fcomponents-web-app\u002Fcomponents-web-app\u002Fcommit\u002Fcd99b5541becc7e9b5539c29e2bb929769e97842",[648],"cd99b55",[420,894,895,896,899,900,903,904,909],{},"The setup on this page was tested end to end on ",[539,897,898],{},"preview.cwa.rocks",", on Cloudflare's ",[430,901,902],{},"Free"," plan, with a scoped token (",[435,905,908],{"href":906,"rel":907},"https:\u002F\u002Fgithub.com\u002Fcomponents-web-app\u002Fcomponents-web-app\u002Fissues\u002F108",[648],"components-web-app#108","):",[774,911,912,922],{},[777,913,914],{},[780,915,916,919],{},[783,917,918],{},"Check",[783,920,921],{},"Result",[793,923,924,939,949,963,974,988,999,1020,1035],{},[780,925,926,929],{},[798,927,928],{},"Pages are cached at the edge",[798,930,931,932,935,936,460],{},"Every sitemap page went ",[539,933,934],{},"MISS"," → ",[539,937,938],{},"HIT",[780,940,941,946],{},[798,942,943,945],{},[539,944,886],{}," reaches Cloudflare",[798,947,948],{},"Yes, and Cloudflare removes it before the visitor sees the response.",[780,950,951,954],{},[798,952,953],{},"An edit purges the affected page",[798,955,956,957,959,960,962],{},"That page went ",[539,958,934],{},", a sibling page stayed ",[539,961,938],{},", and no purge errors were logged.",[780,964,965,968],{},[798,966,967],{},"A deploy purges every page",[798,969,970,971,973],{},"Every page went ",[539,972,934],{}," within seconds.",[780,975,976,985],{},[798,977,978,981,982],{},[430,979,980],{},"Purge all cached data"," \u002F ",[539,983,984],{},"purge-http-cache",[798,986,987],{},"Doesn't reach Cloudflare, as expected. The edge kept serving its copy.",[780,989,990,993],{},[798,991,992],{},"Mercure through Cloudflare",[798,994,995,998],{},[539,996,997],{},"cf-cache-status: BYPASS",", and live updates arrive.",[780,1000,1001,1008],{},[798,1002,1003,1004,1007],{},"Requests with an ",[539,1005,1006],{},"api_component"," cookie",[798,1009,1010,1013,1014,1017,1018,460],{},[539,1011,1012],{},"DYNAMIC"," with ",[539,1015,1016],{},"no-store",". A request with an unrelated cookie still gets a ",[539,1019,938],{},[780,1021,1022,1031],{},[798,1023,1024,1026,1027],{},[539,1025,541],{}," without the ",[435,1028,1030],{"href":1029},"#api-responses-optional","API rule",[798,1032,1033],{},[539,1034,1012],{},[780,1036,1037,1042],{},[798,1038,1039,1041],{},[539,1040,541],{}," with the API rule",[798,1043,1044,1045,1048,1049,935,1051,1053,1054,1056,1057,1059,1060,1063,1064,755,1066,1069,1070,1073],{},"The module's ",[539,1046,1047],{},"Accept"," goes ",[539,1050,934],{},[539,1052,938],{},". Any other ",[539,1055,1047],{},", an ",[539,1058,1006],{}," cookie or an ",[539,1061,1062],{},"Authorization"," header is ",[539,1065,1012],{},[539,1067,1068],{},"\u002F_api\u002Fme"," is ",[539,1071,1072],{},"BYPASS",", and an edit's tag purge drops the API response at the edge.",[467,1075,1077],{"id":1076},"purges-must-reach-cloudflare","Purges must reach Cloudflare",[420,1079,1080,1081,1084,1085,1089,1090,1092,1093,1095,1096,1100],{},"In production, the API tells shared caches to keep responses for a year (",[539,1082,1083],{},"s-maxage=31557600","), and pages inherit it (see ",[435,1086,1088],{"href":1087},"\u002Fnuxt-module\u002Fpage-caching#how-long-a-page-is-kept","How long a page is kept","). Cloudflare honours ",[539,1091,545],{},". If purges don't reach Cloudflare, an edited page stays stale at the edge for up to a year. So does a page that points at the previous build's ",[539,1094,623],{}," files after a deploy, which then never becomes interactive (see ",[435,1097,1099],{"href":1098},"\u002Fdeployment\u002Fci-cd#why-every-deploy-clears-the-page-cache","Why every deploy clears the page cache",").",[420,1102,1103],{},"Cloudflare purges by tag, and that needs two things:",[424,1105,1106,1122],{},[427,1107,1108,1114,1115,1118,1119,1121],{},[430,1109,1110,1111,1113],{},"Responses must carry ",[539,1112,886],{}," when Cloudflare stores them."," Souin never sends that header, on a miss or a hit. Since template cd99b55, the Caddyfile copies ",[539,1116,1117],{},"Surrogate-Key"," into ",[539,1120,886],{}," on every response that has one. Cloudflare indexes it and strips it before the response reaches the visitor, so you can't see it in a browser.",[427,1123,1124,1127,1128,1131,1132,1135],{},[430,1125,1126],{},"Souin must send each tag purge to Cloudflare."," Unpatched Souin v1.7.9, the version the template builds, posts to ",[539,1129,1130],{},"\u002Fzones\u002F\u003Cid>\u002Fpurge"," instead of ",[539,1133,1134],{},"\u002Fpurge_cache",", so no purge ever reached Cloudflare, with any credential, and the failure was silent. Since cd99b55 the template patches Souin: it calls the right endpoint, sends 30 tags per request, gives each request a 30-second timeout and logs any failure.",[420,1137,1138,1139,1142,1143,1146,1147,1150],{},"Souin's ",[539,1140,1141],{},"cdn"," block comes from ",[539,1144,1145],{},"CADDY_CACHE_CDN_CONFIG",", which defaults to ",[539,1148,1149],{},"strategy hard"," (no CDN). For Cloudflare it reads:",[556,1152,1157],{"className":1153,"code":1155,"language":1156,"meta":561},[1154],"language-text","strategy hard\nprovider cloudflare\nzone_id \u003Cyour zone ID>\napi_key $CLOUDFLARE_API_TOKEN\n","text",[539,1158,1155],{"__ignoreMap":561},[424,1160,1161,1169,1195],{},[427,1162,1163,1168],{},[430,1164,1165,1166,460],{},"Repeat ",[539,1167,1149],{}," Setting the variable replaces the whole default.",[427,1170,1171,1177,1178,1180,1181,1184,1185,1188,1189,1191,1192,1194],{},[430,1172,1173,1174,460],{},"Use a scoped API token, and leave out ",[539,1175,1176],{},"email"," With no ",[539,1179,1176],{}," line, the patched Souin sends ",[539,1182,1183],{},"api_key"," as a bearer token, so a token with only the ",[430,1186,1187],{},"Zone → Cache Purge"," permission, limited to the site's zone, is enough. This works on the Free plan. With an ",[539,1190,1176],{}," line, Souin treats ",[539,1193,1183],{}," as a Global API Key instead, and a scoped token doesn't work that way. Don't use the Global API Key: it carries every permission your Cloudflare user has.",[427,1196,1197,1198,1201,1202,460],{},"How ",[539,1199,1200],{},"$CLOUDFLARE_API_TOKEN"," gets its value is in ",[435,1203,1205],{"href":1204},"#secrets","Secrets",[420,1207,1208],{},"Which purges reach Cloudflare:",[774,1210,1211,1221],{},[777,1212,1213],{},[780,1214,1215,1218],{},[783,1216,1217],{},"Purge",[783,1219,1220],{},"Reaches Cloudflare?",[793,1222,1223,1231,1246,1261],{},[780,1224,1225,1228],{},[798,1226,1227],{},"Saving content in the admin (tag purge of the changed IRIs)",[798,1229,1230],{},"Yes",[780,1232,1233,1239],{},[798,1234,1235,1236],{},"Site settings → purge page cache, and ",[539,1237,1238],{},"purge-rendered-html",[798,1240,1241,1242,1245],{},"Yes, the ",[539,1243,1244],{},"cwa-html"," tag",[780,1247,1248,1258],{},[798,1249,1250,755,1252,1254,1255,1257],{},[430,1251,980],{},[539,1253,984],{},", and every fixture load (it ends with ",[539,1256,984],{},")",[798,1259,1260],{},"Yes, everything in the zone",[780,1262,1263,1273],{},[798,1264,1265,1266,1269,1270],{},"The deploy purge, ",[539,1267,1268],{},"purge_rendered_html"," in ",[539,1271,1272],{},"k8s.sh",[798,1274,1275,1276,1269,1279,1281,1282,1131,1284],{},"Yes, everything in the zone. With ",[539,1277,1278],{},"provider cloudflare",[539,1280,1145],{},", it runs ",[539,1283,984],{},[539,1285,1238],{},[420,1287,1288,1289,1291,1292,1295,1296,1299],{},"Every row needs the patched Souin. The two tag rows also need the ",[539,1290,886],{}," rule; a full flush doesn't, because Souin sends it to Cloudflare as a ",[430,1293,1294],{},"purge everything",". A tag purge removes exactly the pages carrying that tag, and IRI tags, which contain ",[539,1297,1298],{},"\u002F",", match.",[420,1301,1302,1303,1308,1309,1311,1312,1315,1316,1318,1319,1322,1323,1325,1326,1329],{},"The full-flush and deploy rows are new in template ",[435,1304,1307],{"href":1305,"rel":1306},"https:\u002F\u002Fgithub.com\u002Fcomponents-web-app\u002Fcomponents-web-app\u002Fcommit\u002F6a8c8984b1408d48fced26655ad32b83afcbaeac",[648],"6a8c898",". Before it, a full flush cleared only Souin's own store, and the deploy purged only the ",[539,1310,1244],{}," tag, so you had to use ",[430,1313,1314],{},"Purge Everything"," in Cloudflare (Caching → Configuration) after each full flush, and after a release that changed API output if you use the ",[435,1317,1030],{"href":1029},". To get the new behaviour on an older project, take both the regenerated ",[539,1320,1321],{},"api\u002Ffrankenphp\u002Fsouin\u002Fv1.7.9-cloudflare-purge.patch"," and the ",[539,1324,1268],{}," change in ",[539,1327,1328],{},"bin\u002Fdevops\u002Fk8s.sh",", then rebuild the API image.",[420,1331,1332,1335,1336,1338],{},[430,1333,1334],{},"The deploy empties the edge for every hostname in the zone."," A purge everything can't be limited to one hostname, and staging deploys share ",[539,1337,1145],{}," with production. So a staging deploy also empties production's edge cache, as staging's tag purges already reach production's pages. The edge refills from the origin: the warm job requests every page, and normal traffic does the rest.",[420,1340,1341,1344,1345,1348,1349,1352,1353,460],{},[430,1342,1343],{},"Rate limits."," Cloudflare's purge-by-tag limit is ",[430,1346,1347],{},"5 requests per minute on Free"," (bursts up to 25), 5 per second on Pro and 10 per second on Business. Each save in the admin is at least one request. An editor saving more than about five times a minute on a Free zone gets ",[539,1350,1351],{},"429","s, and those pages stay stale at the edge. Souin doesn't retry. Check the current figures in ",[435,1354,1357],{"href":1355,"rel":1356},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Fcache\u002Fhow-to\u002Fpurge-cache\u002F",[648],"Cloudflare's purge limits",[420,1359,1360,1363,1364,1367,1368,1371],{},[430,1361,1362],{},"Purge failures are logged."," The patched Souin logs a refused purge (for example ",[539,1365,1366],{},"Cloudflare purge of tags [...] failed with status 429: ..."," or ",[539,1369,1370],{},"Cloudflare purge everything failed with status ...",") or a connection error in the php container's log. Nothing else reports it: the admin save still succeeds.",[420,1373,1374],{},"There's also a narrow race: the Cloudflare purge starts before Souin deletes its own copy, so a request in that moment can refill the edge from the stale entry.",[420,1376,1377,1380,1381,1384,1385,1367,1387,1390,1391,1394],{},[430,1378,1379],{},"Checking that purges arrive."," Request a page twice and see ",[539,1382,1383],{},"cf-cache-status: HIT",". Edit something on that page in the admin, then request it again. You should get ",[539,1386,934],{},[539,1388,1389],{},"EXPIRED",", with the new content. If not, look for ",[539,1392,1393],{},"Cloudflare purge"," in the php container's log.",[467,1396,1398],{"id":1397},"turning-edge-caching-on","Turning edge caching on",[420,1400,1401,1402,1404],{},"Cloudflare can only purge a page by tag if the response carried ",[539,1403,886],{}," when Cloudflare stored it. A page cached before that can't be purged by tag at all, and is kept for up to a year. So the order matters:",[472,1406,1407,1417,1429,1432],{},[427,1408,1409,1410,1412,1413,1416],{},"Update to a template that includes cd99b55 (the patched Souin and the ",[539,1411,886],{}," rule), or better ",[435,1414,1307],{"href":1305,"rel":1415},[648],", where full flushes and deploys also purge everything at the edge.",[427,1418,1419,1420,1422,1423,1426,1427,1100],{},"Create the API token, and set ",[539,1421,1145],{}," and ",[539,1424,1425],{},"CLOUDFLARE_API_TOKEN"," (see ",[435,1428,1205],{"href":1204},[427,1430,1431],{},"Deploy.",[427,1433,1434],{},"Only then add the Cache Rule below.",[501,1436,1437],{"icon":503,"color":504},[420,1438,1439,1448,1449,1451,1452,1455],{},[430,1440,1441,1442,1445,1446,460],{},"On a template before ",[435,1443,1307],{"href":1305,"rel":1444},[648],", if the Cache Rule went in first, do one Purge Everything after the first deploy that sends ",[539,1447,886],{}," Until then Cloudflare stores untagged pages, and after the next front-end deploy they point at ",[539,1450,623],{}," files that no longer exist. A Purge Everything before that deploy doesn't help: the old image refills the edge with untagged pages straight away. From ",[435,1453,1307],{"href":1305,"rel":1454},[648],", that deploy purges everything itself, after the new image is rolled out, so the edge refills with tagged pages and nothing more is needed.",[467,1457,1459,1461],{"id":1458},"_api-needs-its-own-rule",[539,1460,541],{}," needs its own rule",[420,1463,1464,1465,1468,1469,1472,1473,1475,1476,1478,1479,1484,1485,1487,1488,1490,1491,1493],{},"Cloudflare ignores ",[539,1466,1467],{},"Vary",", apart from ",[539,1470,1471],{},"Accept-Encoding",". The API negotiates on the ",[539,1474,1047],{}," header (JSON-LD, JSON, or the HTML docs), and custom cache keys on headers are an Enterprise feature. So a rule that caches ",[539,1477,541],{}," by path alone could serve a response in the wrong format, which is the problem Souin hit in ",[435,1480,1483],{"href":1481,"rel":1482},"https:\u002F\u002Fgithub.com\u002Fcomponents-web-app\u002Fcomponents-web-app\u002Fissues\u002F79",[648],"components-web-app#79",". The pages rule below leaves ",[539,1486,541],{}," out. The optional ",[435,1489,1030],{"href":1029}," caches it only for the exact ",[539,1492,1047],{}," the module sends.",[467,1495,1497],{"id":1496},"the-cache-rules-must-match-the-templates-exclusions","The cache rules must match the template's exclusions",[420,1499,1500,1501,1504,1505,1508],{},"Cloudflare doesn't cache HTML unless a Cache Rule (on the ",[430,1502,1503],{},"Cache Rules"," page) marks it ",[430,1506,1507],{},"Eligible for cache",". This is the pages rule that was tested. Replace the host with your own:",[556,1510,1513],{"className":1511,"code":1512,"language":1156,"meta":561},[1154],"(http.host eq \"preview.cwa.rocks\" and not starts_with(http.request.uri.path, \"\u002F_api\") and not http.cookie contains \"api_component=\")\n",[539,1514,1512],{"__ignoreMap":561},[420,1516,1517],{},"Set it up like this:",[774,1519,1520,1529],{},[777,1521,1522],{},[780,1523,1524,1526],{},[783,1525,785],{},[783,1527,1528],{},"Value",[793,1530,1531,1540,1550],{},[780,1532,1533,1536],{},[798,1534,1535],{},"Cache eligibility",[798,1537,1538],{},[430,1539,1507],{},[780,1541,1542,1545],{},[798,1543,1544],{},"Edge TTL",[798,1546,1547],{},[430,1548,1549],{},"Use cache-control header if present, bypass cache if not",[780,1551,1552,1555],{},[798,1553,1554],{},"Browser TTL",[798,1556,1557],{},[430,1558,1559],{},"Respect origin",[420,1561,1562,1565,1566,1569,1570,1573,1574,1576,1577,1580,1581,1367,1584,1586,1587,1590],{},[430,1563,1564],{},"The Edge TTL setting does most of the work."," The template's ",[539,1567,1568],{},"@use_cache"," matcher in ",[539,1571,1572],{},"api\u002Ffrankenphp\u002FCaddyfile"," decides what Souin may cache, and the origin's responses already say what may be shared: a cacheable page carries ",[539,1575,545],{},", and ",[539,1578,1579],{},"\u002Flogin",", error pages and signed-in renders carry ",[539,1582,1583],{},"private",[539,1585,1016],{},", or no ",[539,1588,1589],{},"Cache-Control"," at all. With this setting Cloudflare caches only what the origin allows and bypasses everything else, so you don't have to copy every path from the matcher into the rule. The rule itself only has to leave out what the origin's headers can't protect:",[424,1592,1593,1604],{},[427,1594,1595,1599,1600,1603],{},[430,1596,1597],{},[539,1598,541],{},", because Cloudflare ignores ",[539,1601,1602],{},"Vary: Accept"," (see above). The optional API rule handles it separately.",[427,1605,1606,1610,1611,1613,1614,1616,1617,1422,1619,460],{},[430,1607,1003,1608,1007],{},[539,1609,1006],{},", so editors get their own render. The condition matches any ",[539,1612,1006],{}," cookie, including the empty one a signed-out browser can send, so it may bypass a request it didn't need to. In the test, requests with the cookie were ",[539,1615,1012],{},". Signed-in renders are never stored either way, because they carry ",[539,1618,1016],{},[539,1620,1621],{},"Set-Cookie",[420,1623,1624],{},"Other exclusions in the template's matcher, and how they're covered at the edge:",[424,1626,1627,1636,1659],{},[427,1628,1629,1632,1633,1635],{},[539,1630,1631],{},"\u002F.well-known\u002Fmercure"," is a stream (SSE). It sends no cacheable headers, and the test showed ",[539,1634,997],{}," with live updates arriving.",[427,1637,1638,755,1640,755,1643,755,1646,755,1649,755,1652,1422,1655,1658],{},[539,1639,1579],{},[539,1641,1642],{},"\u002Fforgot-password",[539,1644,1645],{},"\u002Freset-password\u002F*",[539,1647,1648],{},"\u002Fverify-email",[539,1650,1651],{},"\u002Fconfirm-new-email",[539,1653,1654],{},"\u002Fuser-area",[539,1656,1657],{},"\u002F_cwa\u002F*"," are kept out by their own response headers, through the Edge TTL setting.",[427,1660,1661,755,1663,1422,1666,1669],{},[539,1662,623],{},[539,1664,1665],{},"\u002Fuploads\u002F*",[539,1667,1668],{},"\u002Fbundles\u002F*"," are static files. Cloudflare may cache them as it does in Option A, which is safe for the same reasons.",[420,1671,1672,1673,1676,1677,1680,1681,1684,1685,1422,1688,1691],{},"On the Free plan, Cache Rules can't use ",[539,1674,1675],{},"matches"," (regex needs Business) or ",[539,1678,1679],{},"http.request.cookies"," (needs Pro), which is why the cookie condition is a plain ",[539,1682,1683],{},"contains",". Request-header fields do work on Free: the API rule's ",[539,1686,1687],{},"http.request.headers[\"accept\"]",[539,1689,1690],{},"[\"authorization\"]"," conditions were accepted and behave as expected.",[420,1693,1694],{},"When the template's matcher changes, check the rule against it again.",[420,1696,1697,1698,1700,1701,1703],{},"Cloudflare also doesn't cache any response that carries ",[539,1699,1621],{},". If pages stay ",[539,1702,1012],{}," under the rule, check for that first.",[420,1705,1706,1709,1710,1712,1713,1715],{},[430,1707,1708],{},"Tag size."," Cloudflare reads at most 16 KB of ",[539,1711,886],{}," per response. A page's tags are its ",[539,1714,1244],{}," tag plus every resource IRI it rendered from. The largest page in the test sent 1,628 bytes, 26 tags. A page with many more components has a longer header, and what Cloudflare does past the limit hasn't been checked.",[501,1717,1718],{"icon":522},[420,1719,1720,1722],{},[430,1721,527],{}," the Cache Rules page with the eligible rule for page HTML, showing the expression, the Edge TTL set to \"Use cache-control header if present, bypass cache if not\" and the Browser TTL set to \"Respect origin\".",[467,1724,1726],{"id":1725},"api-responses-optional","API responses (optional)",[420,1728,1729],{},"A second rule lets Cloudflare cache API responses too. It speeds up the API calls the browser makes during client-side navigation. Server-side rendering calls the API inside the cluster, so it isn't affected. Use the same settings as the pages rule. The two rules can't overlap:",[556,1731,1734],{"className":1732,"code":1733,"language":1156,"meta":561},[1154],"(http.host eq \"preview.cwa.rocks\"\n and starts_with(http.request.uri.path, \"\u002F_api\u002F\")\n and not starts_with(http.request.uri.path, \"\u002F_api\u002F_\u002Fcomponent_positions\u002F\")\n and any(http.request.headers[\"accept\"][*] in {\"application\u002Fld+json,application\u002Fjson\" \"application\u002Fld+json\"})\n and not http.cookie contains \"api_component=\"\n and not len(http.request.headers[\"authorization\"]) gt 0)\n",[539,1735,1733],{"__ignoreMap":561},[420,1737,1738,1739,1741,1742,1745,1746,1749,1750,1752,1753,1755,1756,1059,1758,1760,1761,755,1764,1767,1768,1770,1771,755,1773,1775,1776,1778],{},"Only the exact ",[539,1740,1047],{}," the module sends (",[539,1743,1744],{},"application\u002Fld+json,application\u002Fjson",") and plain ",[539,1747,1748],{},"application\u002Fld+json"," are eligible. That covers ",[539,1751,1602],{},", which Cloudflare ignores: every other ",[539,1754,1047],{},", such as a browser asking for the HTML docs, goes to Souin. Requests with an ",[539,1757,1006],{},[539,1759,1062],{}," header are left out too. In the test, ",[539,1762,1763],{},"text\u002Fhtml",[539,1765,1766],{},"application\u002Fjson"," and no ",[539,1769,1047],{}," were all ",[539,1772,1012],{},[539,1774,1068],{}," was ",[539,1777,1072],{},", and an edit's tag purge dropped the API response at the edge.",[420,1780,1781,1787,1788,1790,1791,1794,1795,1797],{},[430,1782,1783,1784,460],{},"Component positions are left out because their responses also vary by ",[539,1785,1786],{},"path"," On a page built from page data, a position bound to a page-data property returns a different component for each page: the API picks the page data from the ",[539,1789,1786],{}," request header the module sends, and marks the response ",[539,1792,1793],{},"Vary: path",". Souin honours that, but Cloudflare ignores ",[539,1796,1467],{}," and the Free plan can't add a request header to the cache key. Without the exclusion, the first page-data page whose position Cloudflare stored is served for all of them. Every blog article then shows one article's body after client-side navigation, while titles and server renders look right, and it stays that way for up to a year. Souin still caches positions, per path.",[501,1799,1800],{"icon":503,"color":504},[420,1801,1802,1809,1810,1812],{},[430,1803,1804,1805,1808],{},"If you added the API rule before it excluded ",[539,1806,1807],{},"\u002F_api\u002F_\u002Fcomponent_positions\u002F",", update it, then do one Purge Everything"," (or ",[430,1811,980],{}," in site settings). The wrong copies stay at the edge until they're purged.",[420,1814,1815,1816,1819],{},"A deploy purges everything in the zone (see ",[435,1817,1077],{"href":1818},"#purges-must-reach-cloudflare","), so a release that changes API output, such as a bundle update or a serializer change, doesn't leave stale API responses at the edge.",[501,1821,1822],{"icon":503,"color":504},[420,1823,1824,1830,1831,1833,1834,1836],{},[430,1825,1441,1826,1829],{},[435,1827,1307],{"href":1305,"rel":1828},[648],", a deploy doesn't purge edge-cached API responses."," Its deploy purge removes only the ",[539,1832,1244],{}," tag, which covers pages. After a release that changes API output, use ",[430,1835,1314],{}," in Cloudflare, or leave the API rule off.",[467,1838,1840],{"id":1839},"edge-lifetime","Edge lifetime",[420,1842,1843,1844,1846],{},"Keep the Edge TTL on ",[430,1845,1549],{},". Don't choose an override. Two reasons:",[424,1848,1849,1861],{},[427,1850,1851,1856,1857,1860],{},[430,1852,1853,1854,460],{},"\"Ignore cache-control header and use this TTL\" ignores the origin's ",[539,1855,1589],{}," The override replaces the origin's directives, so ",[539,1858,1859],{},"private, no-store"," on a personalised response or an error page would no longer stop Cloudflare caching it.",[427,1862,1863,1866],{},[430,1864,1865],{},"The shortest Edge TTL depends on the plan:"," 2 hours on Free, 1 hour on Pro, 1 second on Business and Enterprise. On Free and Pro, a shorter TTL can't fix a missed purge quickly anyway.",[420,1868,1869,1870,1872],{},"With purges reaching Cloudflare, the origin's year-long ",[539,1871,545],{}," is what you want.",[467,1874,1205],{"id":1875},"secrets",[420,1877,1878],{},"The API token must not appear in pipeline logs.",[424,1880,1881,1906],{},[427,1882,1883,675,1886,1888,1889,1891,1892,1269,1895,1897,1898,1901,1902,1905],{},[430,1884,1885],{},"GitLab.",[539,1887,1145],{}," has several lines, and GitLab can't mask a multi-line variable. Put the token in its own masked variable, ",[539,1890,1425],{},". Then write ",[539,1893,1894],{},"api_key $CLOUDFLARE_API_TOKEN",[539,1896,1145],{}," and turn on ",[430,1899,1900],{},"Expand variable reference"," for that variable. GitLab substitutes the token before the deploy runs. Caddy doesn't expand ",[539,1903,1904],{},"$"," variables itself.",[427,1907,1908,1911,1912,1914,1915,1919,1920,1923,1924,460],{},[430,1909,1910],{},"GitHub."," GitHub doesn't expand one variable inside another, so put the token directly in ",[539,1913,1145],{}," and store the whole value as a secret. The workflows export repository ",[1916,1917,1918],"em",{},"variables"," to the deploy scripts automatically, but not secrets, so the deploy jobs' ",[539,1921,1922],{},"env:"," must also map it: ",[539,1925,1926],{},"CADDY_CACHE_CDN_CONFIG: ${{ secrets.CADDY_CACHE_CDN_CONFIG }}",[420,1928,1929],{},"In the cluster, the chart stores the value in the release's Kubernetes Secret, not its ConfigMap.",[462,1931,1933],{"id":1932},"per-site-checklist","Per-site checklist",[420,1935,1936],{},[430,1937,1938],{},"Option A (every site):",[472,1940,1941,1944,1949,1955,1963,1966,1971,1977],{},[427,1942,1943],{},"The origin serves a valid certificate on every hostname before its record is proxied.",[427,1945,1946,1947,460],{},"DNS records are ",[430,1948,483],{},[427,1950,1951,1952,1954],{},"SSL\u002FTLS mode is ",[430,1953,493],{}," (SSL\u002FTLS → Overview).",[427,1956,1957,1958,1960,1961,460],{},"There are no Cache Rules, and a page and an ",[539,1959,541],{}," response both show ",[539,1962,609],{},[427,1964,1965],{},"A rate limiting rule is in place, with a threshold editors won't hit.",[427,1967,1968,1969,861],{},"Bot Fight Mode is on, and the next deploy's cache warm still gets ",[539,1970,860],{},[427,1972,1973,1974,1976],{},"Someone on the team knows where ",[430,1975,828],{}," is (Overview → Quick Actions) and when to use it.",[427,1978,1979],{},"Nothing in the site's own code relies on the client IP, and the ingress-nginx rate limit variables are unset.",[420,1981,1982],{},[430,1983,1984],{},"Option B (if you turn on edge caching):",[472,1986,1987,2014,2022,2049,2052,2069,2087,2097,2100,2107,2118],{},[427,1988,1989,1990,1992,1993,1995,1996,1998,1999,2001,2002,2004,2005,2007,2008,2011,2012,460],{},"Everything in Option A, except the \"no Cache Rules\" check: a page shows ",[539,1991,938],{}," on its second request. Without the API rule, an ",[539,1994,541],{}," response still shows ",[539,1997,1012],{},". With it, an ",[539,2000,541],{}," request with the module's ",[539,2003,1047],{}," shows ",[539,2006,938],{}," on its second request, and with ",[539,2009,2010],{},"Accept: text\u002Fhtml"," it shows ",[539,2013,1012],{},[427,2015,2016,2017,2019,2020,460],{},"The site runs a template that includes cd99b55: a page response from the origin carries ",[539,2018,886],{}," matching its ",[539,2021,1117],{},[427,2023,2024,2026,2027,755,2029,755,2031,1422,2034,2036,2037,2039,2040,2042,2043,2045,2046,2048],{},[539,2025,1145],{}," has ",[539,2028,1149],{},[539,2030,1278],{},[539,2032,2033],{},"zone_id",[539,2035,1183],{},", and no ",[539,2038,1176],{}," line. The token has only ",[430,2041,1187],{}," for this zone, and is masked (GitLab, with ",[430,2044,1900],{}," on for ",[539,2047,1145],{},") or a secret (GitHub).",[427,2050,2051],{},"That was deployed before the Cache Rule went in, or the site runs a template from 6a8c898, or one Purge Everything was done after that deploy.",[427,2053,2054,2055,2057,2058,2060,2061,2063,2064,2066,2067,460],{},"There's a pages Cache Rule, and optionally the ",[435,2056,1030],{"href":1029},", with the tested expressions for your host (the API rule excludes ",[539,2059,1807],{},"), each with ",[430,2062,1507],{},", Edge TTL ",[430,2065,1549],{},", Browser TTL ",[430,2068,1559],{},[427,2070,2071,2072,2075,2076,2078,2079,1367,2081,2083,2084,2086],{},"An edit in the admin turns that page's ",[539,2073,2074],{},"cf-cache-status"," from ",[539,2077,938],{}," to ",[539,2080,934],{},[539,2082,1389],{},", and the php log has no ",[539,2085,1393],{}," errors.",[427,2088,2089,2090,2093,2094,2096],{},"After a deploy, the deploy log says ",[539,2091,2092],{},"Cloudflare is configured: flushing the HTTP cache, which also purges everything at Cloudflare...",", and pages (and, with the API rule, API responses) are ",[539,2095,934],{}," at the edge and become interactive.",[427,2098,2099],{},"With the API rule, client-side navigation between pages built from the same page-data template shows each page's own content, not one page's content on all of them.",[427,2101,2102,2103,2004,2105,460],{},"Live updates still arrive, and ",[539,2104,1631],{},[539,2106,997],{},[427,2108,2109,2111,2112,2114,2115,2117],{},[430,2110,980],{}," in site settings turns a ",[539,2113,938],{}," page into ",[539,2116,934],{}," at the edge.",[427,2119,2120],{},"The plan's purge rate limit suits how often editors save: 5 purge requests a minute on Free.",[2122,2123,2124],"style",{},"html pre.shiki code .sRCss, html code.shiki .sRCss{--shiki-light:#6F42C1;--shiki-default:#B392F0;--shiki-dark:#FFCB6B}html pre.shiki code .szhYu, html code.shiki .szhYu{--shiki-light:#005CC5;--shiki-default:#79B8FF;--shiki-dark:#C3E88D}html pre.shiki code .sLL54, html code.shiki .sLL54{--shiki-light:#032F62;--shiki-default:#9ECBFF;--shiki-dark:#C3E88D}html pre.shiki code .sVlFx, html code.shiki .sVlFx{--shiki-light:#D73A49;--shiki-default:#F97583;--shiki-dark:#89DDFF}html pre.shiki code .seSrl, html code.shiki .seSrl{--shiki-light:#032F62;--shiki-default:#9ECBFF;--shiki-dark:#89DDFF}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":561,"searchDepth":2126,"depth":2126,"links":2127},2,[2128,2135,2145],{"id":464,"depth":2126,"text":465,"children":2129},[2130,2132,2133,2134],{"id":469,"depth":2131,"text":470},3,{"id":533,"depth":2131,"text":534},{"id":627,"depth":2131,"text":628},{"id":771,"depth":2131,"text":772},{"id":879,"depth":2126,"text":880,"children":2136},[2137,2138,2139,2141,2142,2143,2144],{"id":1076,"depth":2131,"text":1077},{"id":1397,"depth":2131,"text":1398},{"id":1458,"depth":2131,"text":2140},"\u002F_api needs its own rule",{"id":1496,"depth":2131,"text":1497},{"id":1725,"depth":2131,"text":1726},{"id":1839,"depth":2131,"text":1840},{"id":1875,"depth":2131,"text":1205},{"id":1932,"depth":2126,"text":1933},"Putting a CWA site behind Cloudflare. Proxy only is the recommended default. Edge caching has been tested end to end on the Free plan and is a supported choice, with an optional second rule for API responses.","md",{},true,{"title":410,"description":2146},"2I5fYifYv5GQizv7V2xEPl4gbg4gx0vAR_sbMoDSzEc",[2153,415],{"title":406,"path":407,"stem":408,"description":2154,"children":-1},"When a project's functional test suite slows CI down — measure where the time goes, reset the database without rebuilding the schema, and shard the job by test.",1791209736531]