[{"data":1,"prerenderedAt":3662},["ShallowReactive",2],{"navigation":3,"\u002Fdeployment\u002Fcloudflare":604,"\u002Fdeployment\u002Fcloudflare-surround":3657},[4,24,67,93,236,533],{"title":5,"path":6,"stem":7,"children":8,"collapsed":23},"Getting Started","\u002Fgetting-started","1.getting-started\u002F1.index",[9,11,15,19],{"title":10,"path":6,"stem":7},"Introduction",{"title":12,"path":13,"stem":14},"Installation","\u002Fgetting-started\u002Finstallation","1.getting-started\u002F2.installation",{"title":16,"path":17,"stem":18},"Key Ideas","\u002Fgetting-started\u002Fkey-ideas","1.getting-started\u002F3.key-ideas",{"title":20,"path":21,"stem":22},"Releases","\u002Fgetting-started\u002Freleases","1.getting-started\u002F4.releases",true,{"title":25,"path":26,"stem":27,"children":28,"collapsed":23},"Core Concepts","\u002Fcore-concepts","2.core-concepts\u002F0.index",[29,31,35,39,43,47,51,55,59,63],{"title":30,"path":26,"stem":27},"Overview",{"title":32,"path":33,"stem":34},"How It All Works","\u002Fcore-concepts\u002Farchitecture","2.core-concepts\u002F1.architecture",{"title":36,"path":37,"stem":38},"The Data Model","\u002Fcore-concepts\u002Fthe-data-model","2.core-concepts\u002F2.the-data-model",{"title":40,"path":41,"stem":42},"Layouts & Pages","\u002Fcore-concepts\u002Flayouts-and-pages","2.core-concepts\u002F3.layouts-and-pages",{"title":44,"path":45,"stem":46},"Dynamic Pages","\u002Fcore-concepts\u002Fdynamic-pages","2.core-concepts\u002F4.dynamic-pages",{"title":48,"path":49,"stem":50},"Components","\u002Fcore-concepts\u002Fcomponents","2.core-concepts\u002F5.components",{"title":52,"path":53,"stem":54},"Draft & Publish Workflow","\u002Fcore-concepts\u002Fdraft-and-publish","2.core-concepts\u002F6.draft-and-publish",{"title":56,"path":57,"stem":58},"The Admin Panel","\u002Fcore-concepts\u002Fadmin-panel","2.core-concepts\u002F7.admin-panel",{"title":60,"path":61,"stem":62},"Caching","\u002Fcore-concepts\u002Fcaching","2.core-concepts\u002F8.caching",{"title":64,"path":65,"stem":66},"Glossary","\u002Fcore-concepts\u002Fglossary","2.core-concepts\u002F9.glossary",{"title":68,"path":69,"stem":70,"children":71,"collapsed":23},"Guides","\u002Fguides","3.guides\u002F0.index",[72,73,77,81,85,89],{"title":30,"path":69,"stem":70},{"title":74,"path":75,"stem":76},"Your First Layout","\u002Fguides\u002Fyour-first-layout","3.guides\u002F1.your-first-layout",{"title":78,"path":79,"stem":80},"Your First Page Template","\u002Fguides\u002Fyour-first-page-template","3.guides\u002F2.your-first-page-template",{"title":82,"path":83,"stem":84},"Your First Component","\u002Fguides\u002Fyour-first-component","3.guides\u002F3.your-first-component",{"title":86,"path":87,"stem":88},"Alternative UI Variants","\u002Fguides\u002Falternative-ui-variants","3.guides\u002F4.alternative-ui-variants",{"title":90,"path":91,"stem":92},"Your First Dynamic Page","\u002Fguides\u002Fyour-first-dynamic-page","3.guides\u002F5.your-first-dynamic-page",{"title":94,"path":95,"stem":96,"children":97,"collapsed":23},"API Bundle (Back-End)","\u002Fapi","4.api\u002F0.index",[98,99,103,107,129,175,179,183,212,216,220,224,228,232],{"title":30,"path":95,"stem":96},{"title":100,"path":101,"stem":102},"Bundle Setup","\u002Fapi\u002Fbundle-setup","4.api\u002F02.bundle-setup",{"title":104,"path":105,"stem":106},"Configuration Reference","\u002Fapi\u002Fconfiguration","4.api\u002F03.configuration",{"title":108,"path":109,"stem":110,"children":111,"page":128},"Users","\u002Fapi\u002Fusers","4.api\u002F04.users",[112,116,120,124],{"title":113,"path":114,"stem":115},"Authentication","\u002Fapi\u002Fusers\u002Fauthentication","4.api\u002F04.users\u002F1.authentication",{"title":117,"path":118,"stem":119},"User Emails","\u002Fapi\u002Fusers\u002Fuser-emails","4.api\u002F04.users\u002F2.user-emails",{"title":121,"path":122,"stem":123},"Access Control","\u002Fapi\u002Fusers\u002Faccess-control","4.api\u002F04.users\u002F3.access-control",{"title":125,"path":126,"stem":127},"Sites with Non-Admin Users","\u002Fapi\u002Fusers\u002Fnon-admin-users","4.api\u002F04.users\u002F4.non-admin-users",false,{"title":48,"path":130,"stem":131,"children":132,"page":128},"\u002Fapi\u002Fcomponents","4.api\u002F05.components",[133,137,158,171],{"title":134,"path":135,"stem":136},"Creating Components","\u002Fapi\u002Fcomponents\u002Fcreating-components","4.api\u002F05.components\u002F1.creating-components",{"title":138,"path":139,"stem":140,"children":141,"page":128},"Annotations","\u002Fapi\u002Fcomponents\u002Fannotations","4.api\u002F05.components\u002F2.annotations",[142,146,150,154],{"title":143,"path":144,"stem":145},"Publishable","\u002Fapi\u002Fcomponents\u002Fannotations\u002Fpublishable","4.api\u002F05.components\u002F2.annotations\u002F1.publishable",{"title":147,"path":148,"stem":149},"Uploadable","\u002Fapi\u002Fcomponents\u002Fannotations\u002Fuploadable","4.api\u002F05.components\u002F2.annotations\u002F2.uploadable",{"title":151,"path":152,"stem":153},"Timestamped","\u002Fapi\u002Fcomponents\u002Fannotations\u002Ftimestamped","4.api\u002F05.components\u002F2.annotations\u002F3.timestamped",{"title":155,"path":156,"stem":157},"Explicit Allow Only","\u002Fapi\u002Fcomponents\u002Fannotations\u002Fexplicit-allow-only","4.api\u002F05.components\u002F2.annotations\u002F4.explicit-allow-only",{"title":159,"path":160,"stem":161,"children":162,"page":128},"Built Ins","\u002Fapi\u002Fcomponents\u002Fbuilt-ins","4.api\u002F05.components\u002F3.built-ins",[163,167],{"title":164,"path":165,"stem":166},"Collection Component","\u002Fapi\u002Fcomponents\u002Fbuilt-ins\u002Fcollection-component","4.api\u002F05.components\u002F3.built-ins\u002F1.collection-component",{"title":168,"path":169,"stem":170},"Form Component","\u002Fapi\u002Fcomponents\u002Fbuilt-ins\u002Fform-component","4.api\u002F05.components\u002F3.built-ins\u002F2.form-component",{"title":172,"path":173,"stem":174},"Groups & Positions","\u002Fapi\u002Fcomponents\u002Fcomponent-groups","4.api\u002F05.components\u002F4.component-groups",{"title":176,"path":177,"stem":178},"Page Data & Nested Pages","\u002Fapi\u002Fdynamic-pages","4.api\u002F06.dynamic-pages",{"title":180,"path":181,"stem":182},"Routes","\u002Fapi\u002Froutes","4.api\u002F07.routes",{"title":184,"path":185,"stem":186,"children":187,"page":128},"Data Fixtures","\u002Fapi\u002Fdata-fixtures","4.api\u002F08.data-fixtures",[188,192,196,200,204,208],{"title":189,"path":190,"stem":191},"Getting Started with Fixtures","\u002Fapi\u002Fdata-fixtures\u002Fgetting-started","4.api\u002F08.data-fixtures\u002F1.getting-started",{"title":193,"path":194,"stem":195},"Writing a Scaffold","\u002Fapi\u002Fdata-fixtures\u002Fwriting-a-scaffold","4.api\u002F08.data-fixtures\u002F2.writing-a-scaffold",{"title":197,"path":198,"stem":199},"Builder Reference","\u002Fapi\u002Fdata-fixtures\u002Fbuilder-reference","4.api\u002F08.data-fixtures\u002F3.builder-reference",{"title":201,"path":202,"stem":203},"Seeding Content","\u002Fapi\u002Fdata-fixtures\u002Fseeding-content","4.api\u002F08.data-fixtures\u002F4.seeding-content",{"title":205,"path":206,"stem":207},"Appending & Multiple Scaffolds","\u002Fapi\u002Fdata-fixtures\u002Fappending","4.api\u002F08.data-fixtures\u002F5.appending",{"title":209,"path":210,"stem":211},"Generating Fixtures","\u002Fapi\u002Fdata-fixtures\u002Fgenerating-fixtures","4.api\u002F08.data-fixtures\u002F6.generating-fixtures",{"title":213,"path":214,"stem":215},"Real-Time (Mercure)","\u002Fapi\u002Freal-time","4.api\u002F09.real-time",{"title":217,"path":218,"stem":219},"Versions & Conflicts","\u002Fapi\u002Fversions-and-conflicts","4.api\u002F09.versions-and-conflicts",{"title":221,"path":222,"stem":223},"HTTP Caching","\u002Fapi\u002Fhttp-caching","4.api\u002F10.http-caching",{"title":225,"path":226,"stem":227},"Orphaned Data","\u002Fapi\u002Forphaned-data","4.api\u002F11.orphaned-data",{"title":229,"path":230,"stem":231},"Console Commands","\u002Fapi\u002Fconsole-commands","4.api\u002F12.console-commands",{"title":233,"path":234,"stem":235},"Debugging & Errors","\u002Fapi\u002Fdebugging","4.api\u002F13.debugging",{"title":237,"path":238,"stem":239,"children":240,"collapsed":23},"Nuxt Module (Front-End)","\u002Fnuxt-module","5.nuxt-module\u002F0.index",[241,242,246,259,279,308,329,460,494,497,525,529],{"title":30,"path":238,"stem":239},{"title":243,"path":244,"stem":245},"Module Setup","\u002Fnuxt-module\u002Fmodule-setup","5.nuxt-module\u002F01.module-setup",{"title":247,"path":248,"stem":249,"children":250,"page":128},"Configuration","\u002Fnuxt-module\u002Fconfiguration","5.nuxt-module\u002F02.configuration",[251,255],{"title":252,"path":253,"stem":254},"Nuxt Config","\u002Fnuxt-module\u002Fconfiguration\u002Fnuxt-config","5.nuxt-module\u002F02.configuration\u002F1.nuxt-config",{"title":256,"path":257,"stem":258},"Site Config & SEO","\u002Fnuxt-module\u002Fconfiguration\u002Fsite-config-and-seo","5.nuxt-module\u002F02.configuration\u002F2.site-config-and-seo",{"title":260,"path":261,"stem":262,"children":263,"page":128},"Building Your Ui","\u002Fnuxt-module\u002Fbuilding-your-ui","5.nuxt-module\u002F03.building-your-ui",[264,268,272,275],{"title":265,"path":266,"stem":267},"Layouts","\u002Fnuxt-module\u002Fbuilding-your-ui\u002Fcreating-layouts","5.nuxt-module\u002F03.building-your-ui\u002F1.creating-layouts",{"title":269,"path":270,"stem":271},"Page Templates","\u002Fnuxt-module\u002Fbuilding-your-ui\u002Fcreating-page-templates","5.nuxt-module\u002F03.building-your-ui\u002F2.creating-page-templates",{"title":48,"path":273,"stem":274},"\u002Fnuxt-module\u002Fbuilding-your-ui\u002Fcreating-components","5.nuxt-module\u002F03.building-your-ui\u002F3.creating-components",{"title":276,"path":277,"stem":278},"CLI Generator","\u002Fnuxt-module\u002Fbuilding-your-ui\u002Fcwa-cli","5.nuxt-module\u002F03.building-your-ui\u002F4.cwa-cli",{"title":280,"path":281,"stem":282,"children":283,"page":128},"Component Helpers","\u002Fnuxt-module\u002Fcomponent-helpers","5.nuxt-module\u002F04.component-helpers",[284,288,292,296,300,304],{"title":285,"path":286,"stem":287},"Images & Media","\u002Fnuxt-module\u002Fcomponent-helpers\u002Fimages-and-uploads","5.nuxt-module\u002F04.component-helpers\u002F1.images-and-uploads",{"title":289,"path":290,"stem":291},"Collections & Pagination","\u002Fnuxt-module\u002Fcomponent-helpers\u002Fcollections-and-pagination","5.nuxt-module\u002F04.component-helpers\u002F2.collections-and-pagination",{"title":293,"path":294,"stem":295},"HTML Content","\u002Fnuxt-module\u002Fcomponent-helpers\u002Fhtml-content","5.nuxt-module\u002F04.component-helpers\u002F3.html-content",{"title":297,"path":298,"stem":299},"Rich-Text Editor","\u002Fnuxt-module\u002Fcomponent-helpers\u002Frich-text-editor","5.nuxt-module\u002F04.component-helpers\u002F4.rich-text-editor",{"title":301,"path":302,"stem":303},"Real-Time Updates","\u002Fnuxt-module\u002Fcomponent-helpers\u002Freal-time-updates","5.nuxt-module\u002F04.component-helpers\u002F5.real-time-updates",{"title":305,"path":306,"stem":307},"Forms","\u002Fnuxt-module\u002Fcomponent-helpers\u002Fforms","5.nuxt-module\u002F04.component-helpers\u002F6.forms",{"title":309,"path":310,"stem":311,"children":312,"page":128},"Cwa Components","\u002Fnuxt-module\u002Fcwa-components","5.nuxt-module\u002F05.cwa-components",[313,317,321,325],{"title":314,"path":315,"stem":316},"\u003CCwaComponentGroup \u002F>","\u002Fnuxt-module\u002Fcwa-components\u002Fcwa-component-group","5.nuxt-module\u002F05.cwa-components\u002F1.cwa-component-group",{"title":318,"path":319,"stem":320},"\u003CCwaPage \u002F>","\u002Fnuxt-module\u002Fcwa-components\u002Fcwa-page","5.nuxt-module\u002F05.cwa-components\u002F2.cwa-page",{"title":322,"path":323,"stem":324},"\u003CCwaLink \u002F>","\u002Fnuxt-module\u002Fcwa-components\u002Fcwa-link","5.nuxt-module\u002F05.cwa-components\u002F3.cwa-link",{"title":326,"path":327,"stem":328},"\u003CCwaDefaultLayout \u002F>","\u002Fnuxt-module\u002Fcwa-components\u002Fcwa-default-layout","5.nuxt-module\u002F05.cwa-components\u002F5.cwa-default-layout",{"title":330,"path":331,"stem":332,"children":333,"page":128},"Composables","\u002Fnuxt-module\u002Fcomposables","5.nuxt-module\u002F06.composables",[334,337,345,370,390,415,435],{"title":30,"path":335,"stem":336},"\u002Fnuxt-module\u002Fcomposables\u002Foverview","5.nuxt-module\u002F06.composables\u002F0.overview",{"title":338,"path":339,"stem":340,"children":341,"page":128},"Layout","\u002Fnuxt-module\u002Fcomposables\u002Flayout","5.nuxt-module\u002F06.composables\u002F1.layout",[342],{"title":338,"path":343,"stem":344},"\u002Fnuxt-module\u002Fcomposables\u002Flayout\u002Fuse-cwa-layout","5.nuxt-module\u002F06.composables\u002F1.layout\u002F1.use-cwa-layout",{"title":346,"path":347,"stem":348,"children":349,"page":128},"Component","\u002Fnuxt-module\u002Fcomposables\u002Fcomponent","5.nuxt-module\u002F06.composables\u002F2.component",[350,354,358,362,366],{"title":351,"path":352,"stem":353},"Component (recommended)","\u002Fnuxt-module\u002Fcomposables\u002Fcomponent\u002Fuse-cwa-component","5.nuxt-module\u002F06.composables\u002F2.component\u002F1.use-cwa-component",{"title":355,"path":356,"stem":357},"Resource","\u002Fnuxt-module\u002Fcomposables\u002Fcomponent\u002Fuse-cwa-resource","5.nuxt-module\u002F06.composables\u002F2.component\u002F2.use-cwa-resource",{"title":359,"path":360,"stem":361},"File Field","\u002Fnuxt-module\u002Fcomposables\u002Fcomponent\u002Fuse-cwa-file-field","5.nuxt-module\u002F06.composables\u002F2.component\u002F3.use-cwa-file-field",{"title":363,"path":364,"stem":365},"Collection Pagination","\u002Fnuxt-module\u002Fcomposables\u002Fcomponent\u002Fuse-cwa-collection-pagination","5.nuxt-module\u002F06.composables\u002F2.component\u002F4.use-cwa-collection-pagination",{"title":367,"path":368,"stem":369},"Collection Resource (deprecated)","\u002Fnuxt-module\u002Fcomposables\u002Fcomponent\u002Fuse-cwa-collection-resource","5.nuxt-module\u002F06.composables\u002F2.component\u002F9.use-cwa-collection-resource",{"title":371,"path":372,"stem":373,"children":374,"page":128},"Form","\u002Fnuxt-module\u002Fcomposables\u002Fform","5.nuxt-module\u002F06.composables\u002F3.form",[375,378,382,386],{"title":371,"path":376,"stem":377},"\u002Fnuxt-module\u002Fcomposables\u002Fform\u002Fuse-cwa-form","5.nuxt-module\u002F06.composables\u002F3.form\u002F1.use-cwa-form",{"title":379,"path":380,"stem":381},"Form Input","\u002Fnuxt-module\u002Fcomposables\u002Fform\u002Fuse-cwa-form-input","5.nuxt-module\u002F06.composables\u002F3.form\u002F2.use-cwa-form-input",{"title":383,"path":384,"stem":385},"Form Repeated","\u002Fnuxt-module\u002Fcomposables\u002Fform\u002Fuse-cwa-form-repeated","5.nuxt-module\u002F06.composables\u002F3.form\u002F3.use-cwa-form-repeated",{"title":387,"path":388,"stem":389},"Form Collection","\u002Fnuxt-module\u002Fcomposables\u002Fform\u002Fuse-cwa-form-collection","5.nuxt-module\u002F06.composables\u002F3.form\u002F4.use-cwa-form-collection",{"title":391,"path":392,"stem":393,"children":394,"page":128},"Admin Manager","\u002Fnuxt-module\u002Fcomposables\u002Fadmin-manager","5.nuxt-module\u002F06.composables\u002F4.admin-manager",[395,399,403,407,411],{"title":396,"path":397,"stem":398},"Manager Tab","\u002Fnuxt-module\u002Fcomposables\u002Fadmin-manager\u002Fuse-cwa-resource-manager-tab","5.nuxt-module\u002F06.composables\u002F4.admin-manager\u002F1.use-cwa-resource-manager-tab",{"title":400,"path":401,"stem":402},"Resource Model","\u002Fnuxt-module\u002Fcomposables\u002Fadmin-manager\u002Fuse-cwa-resource-model","5.nuxt-module\u002F06.composables\u002F4.admin-manager\u002F2.use-cwa-resource-model",{"title":404,"path":405,"stem":406},"Resource Upload","\u002Fnuxt-module\u002Fcomposables\u002Fadmin-manager\u002Fuse-cwa-resource-upload","5.nuxt-module\u002F06.composables\u002F4.admin-manager\u002F3.use-cwa-resource-upload",{"title":408,"path":409,"stem":410},"Remote Changes","\u002Fnuxt-module\u002Fcomposables\u002Fadmin-manager\u002Fon-remote-change","5.nuxt-module\u002F06.composables\u002F4.admin-manager\u002F4.on-remote-change",{"title":412,"path":413,"stem":414},"Real-Time Events","\u002Fnuxt-module\u002Fcomposables\u002Fadmin-manager\u002Fuse-cwa-realtime","5.nuxt-module\u002F06.composables\u002F4.admin-manager\u002F5.use-cwa-realtime",{"title":416,"path":417,"stem":418,"children":419,"page":128},"Utilities","\u002Fnuxt-module\u002Fcomposables\u002Futilities","5.nuxt-module\u002F06.composables\u002F5.utilities",[420,424,428,432],{"title":421,"path":422,"stem":423},"Resource Endpoint","\u002Fnuxt-module\u002Fcomposables\u002Futilities\u002Fuse-cwa-resource-endpoint","5.nuxt-module\u002F06.composables\u002F5.utilities\u002F1.use-cwa-resource-endpoint",{"title":425,"path":426,"stem":427},"Query Model","\u002Fnuxt-module\u002Fcomposables\u002Futilities\u002Fuse-query-bound-model","5.nuxt-module\u002F06.composables\u002F5.utilities\u002F2.use-query-bound-model",{"title":429,"path":430,"stem":431},"Resource Route","\u002Fnuxt-module\u002Fcomposables\u002Futilities\u002Fuse-cwa-resource-route","5.nuxt-module\u002F06.composables\u002F5.utilities\u002F3.use-cwa-resource-route",{"title":293,"path":433,"stem":434},"\u002Fnuxt-module\u002Fcomposables\u002Futilities\u002Fuse-html-content","5.nuxt-module\u002F06.composables\u002F5.utilities\u002F4.use-html-content",{"title":436,"path":437,"stem":438,"children":439,"page":128},"Auth","\u002Fnuxt-module\u002Fcomposables\u002Fauth","5.nuxt-module\u002F06.composables\u002F6.auth",[440,444,448,452,456],{"title":441,"path":442,"stem":443},"Login","\u002Fnuxt-module\u002Fcomposables\u002Fauth\u002Fuse-login","5.nuxt-module\u002F06.composables\u002F6.auth\u002F1.use-login",{"title":445,"path":446,"stem":447},"Forgot Password","\u002Fnuxt-module\u002Fcomposables\u002Fauth\u002Fuse-forgot-password","5.nuxt-module\u002F06.composables\u002F6.auth\u002F2.use-forgot-password",{"title":449,"path":450,"stem":451},"Reset Password","\u002Fnuxt-module\u002Fcomposables\u002Fauth\u002Fuse-reset-password","5.nuxt-module\u002F06.composables\u002F6.auth\u002F3.use-reset-password",{"title":453,"path":454,"stem":455},"Verify Email","\u002Fnuxt-module\u002Fcomposables\u002Fauth\u002Fuse-verify-email","5.nuxt-module\u002F06.composables\u002F6.auth\u002F4.use-verify-email",{"title":457,"path":458,"stem":459},"Resend Verify Email","\u002Fnuxt-module\u002Fcomposables\u002Fauth\u002Fuse-resend-verify-email","5.nuxt-module\u002F06.composables\u002F6.auth\u002F5.use-resend-verify-email",{"title":461,"path":462,"stem":463,"children":464,"page":128},"Cwa Api","\u002Fnuxt-module\u002Fcwa-api","5.nuxt-module\u002F07.cwa-api",[465,468,472,476,479,482,486,490],{"title":30,"path":466,"stem":467},"\u002Fnuxt-module\u002Fcwa-api\u002Foverview","5.nuxt-module\u002F07.cwa-api\u002F1.overview",{"title":469,"path":470,"stem":471},"Resources","\u002Fnuxt-module\u002Fcwa-api\u002Fresources","5.nuxt-module\u002F07.cwa-api\u002F2.resources",{"title":473,"path":474,"stem":475},"Resources Manager","\u002Fnuxt-module\u002Fcwa-api\u002Fresources-manager","5.nuxt-module\u002F07.cwa-api\u002F3.resources-manager",{"title":436,"path":477,"stem":478},"\u002Fnuxt-module\u002Fcwa-api\u002Fauth","5.nuxt-module\u002F07.cwa-api\u002F4.auth",{"title":305,"path":480,"stem":481},"\u002Fnuxt-module\u002Fcwa-api\u002Fforms","5.nuxt-module\u002F07.cwa-api\u002F5.forms",{"title":483,"path":484,"stem":485},"Admin","\u002Fnuxt-module\u002Fcwa-api\u002Fadmin","5.nuxt-module\u002F07.cwa-api\u002F6.admin",{"title":487,"path":488,"stem":489},"Site Config","\u002Fnuxt-module\u002Fcwa-api\u002Fsite-config","5.nuxt-module\u002F07.cwa-api\u002F7.site-config",{"title":491,"path":492,"stem":493},"Orphaned Resources","\u002Fnuxt-module\u002Fcwa-api\u002Forphaned-resources","5.nuxt-module\u002F07.cwa-api\u002F8.orphaned-resources",{"title":113,"path":495,"stem":496},"\u002Fnuxt-module\u002Fauthentication","5.nuxt-module\u002F08.authentication",{"title":498,"path":499,"stem":500,"children":501,"page":128},"Cwa Layer","\u002Fnuxt-module\u002Fcwa-layer","5.nuxt-module\u002F9.cwa-layer",[502,505,509,513,517,521],{"title":30,"path":503,"stem":504},"\u002Fnuxt-module\u002Fcwa-layer\u002Foverview","5.nuxt-module\u002F9.cwa-layer\u002F1.overview",{"title":506,"path":507,"stem":508},"Auth Pages","\u002Fnuxt-module\u002Fcwa-layer\u002Fauth-pages","5.nuxt-module\u002F9.cwa-layer\u002F2.auth-pages",{"title":510,"path":511,"stem":512},"Admin Panel","\u002Fnuxt-module\u002Fcwa-layer\u002Fadmin-panel","5.nuxt-module\u002F9.cwa-layer\u002F3.admin-panel",{"title":514,"path":515,"stem":516},"Styling","\u002Fnuxt-module\u002Fcwa-layer\u002Fstyling","5.nuxt-module\u002F9.cwa-layer\u002F4.styling",{"title":518,"path":519,"stem":520},"Date Pickers","\u002Fnuxt-module\u002Fcwa-layer\u002Fdate-pickers","5.nuxt-module\u002F9.cwa-layer\u002F5.date-pickers",{"title":522,"path":523,"stem":524},"Maintenance","\u002Fnuxt-module\u002Fcwa-layer\u002Fmaintenance","5.nuxt-module\u002F9.cwa-layer\u002F6.maintenance",{"title":526,"path":527,"stem":528},"PWA & Offline","\u002Fnuxt-module\u002Fpwa","5.nuxt-module\u002F91.pwa",{"title":530,"path":531,"stem":532},"Page Caching","\u002Fnuxt-module\u002Fpage-caching","5.nuxt-module\u002F92.page-caching",{"title":534,"path":535,"stem":536,"children":537,"collapsed":23},"Deployment & Operations","\u002Fdeployment","6.deployment\u002F0.index",[538,540,544,548,552,556,560,564,568,572,576,580,584,588,592,596,600],{"title":539,"path":535,"stem":536},"Deploying a CWA Site",{"title":541,"path":542,"stem":543},"Docker","\u002Fdeployment\u002Fdocker","6.deployment\u002F1.docker",{"title":545,"path":546,"stem":547},"Deploy to One Server","\u002Fdeployment\u002Fone-server","6.deployment\u002F1.one-server",{"title":549,"path":550,"stem":551},"Kubernetes & Helm","\u002Fdeployment\u002Fkubernetes","6.deployment\u002F2.kubernetes",{"title":553,"path":554,"stem":555},"Moving From One Server to Kubernetes","\u002Fdeployment\u002Fmoving-to-kubernetes","6.deployment\u002F2.moving-to-kubernetes",{"title":557,"path":558,"stem":559},"Scaled Mode","\u002Fdeployment\u002Fscaled-mode","6.deployment\u002F2.scaled-mode",{"title":561,"path":562,"stem":563},"CI\u002FCD","\u002Fdeployment\u002Fci-cd","6.deployment\u002F3.ci-cd",{"title":565,"path":566,"stem":567},"Migrations and Backups","\u002Fdeployment\u002Fmigrations-and-backups","6.deployment\u002F3.migrations-and-backups",{"title":569,"path":570,"stem":571},"CI\u002FCD Variables","\u002Fdeployment\u002Fvariables","6.deployment\u002F4.variables",{"title":573,"path":574,"stem":575},"Cloudflare","\u002Fdeployment\u002Fcloudflare","6.deployment\u002F5.cloudflare",{"title":577,"path":578,"stem":579},"Origin Protection","\u002Fdeployment\u002Forigin-protection","6.deployment\u002F5.origin-protection",{"title":581,"path":582,"stem":583},"Load Testing","\u002Fdeployment\u002Fload-testing","6.deployment\u002F6.load-testing",{"title":585,"path":586,"stem":587},"Performance Audit","\u002Fdeployment\u002Fperformance-audit","6.deployment\u002F6.performance-audit",{"title":589,"path":590,"stem":591},"Reading Lighthouse Scores","\u002Fdeployment\u002Freading-lighthouse-scores","6.deployment\u002F6.reading-lighthouse-scores",{"title":593,"path":594,"stem":595},"Browser Tests","\u002Fdeployment\u002Fbrowser-tests","6.deployment\u002F7.browser-tests",{"title":597,"path":598,"stem":599},"Faster Functional Tests","\u002Fdeployment\u002Ffaster-functional-tests","6.deployment\u002F7.faster-functional-tests",{"title":601,"path":602,"stem":603},"Troubleshooting","\u002Fdeployment\u002Ftroubleshooting","6.deployment\u002F7.troubleshooting",{"id":605,"title":573,"badge":606,"body":607,"description":3652,"extension":3653,"links":606,"meta":3654,"navigation":23,"path":574,"seo":3655,"stem":575,"__hash__":3656},"docs\u002F6.deployment\u002F5.cloudflare.md",null,{"type":608,"value":609,"toc":3627},"minimark",[610,614,649,662,667,672,701,721,732,736,748,812,830,844,850,874,880,891,894,906,912,916,923,962,981,984,1052,1055,1129,1178,1182,1189,1224,1262,1267,1277,1493,1517,1524,1528,1584,1601,1604,1635,1649,1666,1671,1681,1684,1688,1722,1729,1733,1739,1784,1793,1796,1810,1818,1860,1863,1933,1952,1983,1986,2001,2007,2010,2021,2027,2030,2072,2103,2138,2141,2188,2210,2213,2222,2235,2242,2248,2281,2284,2287,2293,2340,2359,2367,2371,2377,2397,2403,2406,2412,2480,2491,2494,2514,2517,2539,2542,2580,2639,2648,2754,2778,2792,2801,2804,2807,2867,2895,2903,2906,2909,2956,2959,2963,3006,3011,3050,3057,3063,3071,3074,3229,3232,3246,3249,3329,3350,3367,3377,3381,3386,3465,3470,3623],[611,612,613],"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:",[615,616,617,634],"ul",{},[618,619,620,624,625,629,630,633],"li",{},[621,622,623],"strong",{},"Option A, proxy only."," Cloudflare passes requests through and caches no pages. Souin, in the php container, still does all the ",[626,627,628],"a",{"href":531},"page caching",". ",[621,631,632],{},"This is the recommended default:"," it has the fewest moving parts.",[618,635,636,639,640,643,644,648],{},[621,637,638],{},"Option B, proxy with edge caching."," Cloudflare also caches page HTML, and Souin purges it there. It has been ",[621,641,642],{},"tested end to end on the Free plan"," and is a supported choice when you want pages served from Cloudflare's edge. Free allows only 5 purges a minute. The template queues and paces purges within that (see ",[626,645,647],{"href":646},"#queued-and-paced-purges","Queued and paced purges","), but Pro or above suits a site that editors update regularly.",[611,650,651,652,656,657,661],{},"Both end with a ",[626,653,655],{"href":654},"#per-site-checklist","per-site checklist",". To move a site to another Cloudflare account, see ",[626,658,660],{"href":659},"#moving-a-zone-to-another-cloudflare-account","Moving a zone to another Cloudflare account",".",[663,664,666],"h2",{"id":665},"option-a-proxy-only","Option A: proxy only",[668,669,671],"h3",{"id":670},"dns-and-tls","DNS and TLS",[673,674,675,686],"ol",{},[618,676,677,678,681,682,685],{},"In ",[621,679,680],{},"DNS",", set the site's records to ",[621,683,684],{},"Proxied"," (the orange cloud).",[618,687,677,688,691,692,695,696,700],{},[621,689,690],{},"SSL\u002FTLS → Overview",", set the encryption mode to ",[621,693,694],{},"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 ",[626,697,699],{"href":698},"\u002Fdeployment\u002Fkubernetes#ingress-with-tls","Ingress with TLS","), so strict mode works as soon as that certificate is issued.",[702,703,706],"callout",{"icon":704,"color":705},"i-heroicons-exclamation-triangle","warning",[611,707,708,711,712,714,715,718,719,661],{},[621,709,710],{},"Get the origin certificate before you turn the proxy on."," With ",[621,713,694],{},", 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 ",[621,716,717],{},"DNS only"," (grey cloud), deploy, check that the site serves a valid certificate, then switch the record to ",[621,720,684],{},[702,722,724],{"icon":723},"i-heroicons-photo",[611,725,726,729,730,661],{},[621,727,728],{},"Image placeholder:"," Cloudflare's SSL\u002FTLS overview with the encryption mode set to ",[621,731,694],{},[668,733,735],{"id":734},"each-deploy-track","Each deploy track",[611,737,738,739,743,744,747],{},"The template deploys each track to its own hostname: production to the site's domain, staging to ",[740,741,742],"code",{},"staging.\u003Cdomain>",", and review apps to ",[740,745,746],{},"\u003Cbranch>-review.\u003Cdomain>",". Set them up like this:",[749,750,751,767],"table",{},[752,753,754],"thead",{},[755,756,757,761,764],"tr",{},[758,759,760],"th",{},"Track",[758,762,763],{},"DNS record",[758,765,766],{},"SSL\u002FTLS mode",[768,769,770,783,795],"tbody",{},[755,771,772,776,781],{},[773,774,775],"td",{},"Production",[773,777,778,780],{},[621,779,684],{},", but DNS only until its first certificate is issued",[773,782,694],{},[755,784,785,788,793],{},[773,786,787],{},"Staging",[773,789,790,792],{},[621,791,684],{},", but DNS only until its first certificate is issued, and always DNS only two levels below the zone (see below)",[773,794,694],{},[755,796,797,800,809],{},[773,798,799],{},"Review apps",[773,801,802,804,805,808],{},[621,803,717],{},", on a wildcard record (",[740,806,807],{},"*.\u003Cdomain>",")",[773,810,811],{},"Not used",[611,813,814,817,818,821,822,825,826,829],{},[621,815,816],{},"Staging keeps its certificate between deploys."," Every production deploy runs ",[740,819,820],{},"delete staging",", which uninstalls the staging release, so the staging hostname is down until the next staging deploy, whatever the proxy setting. The certificate isn't lost with it. cert-manager creates staging's Certificate from the ingress and deletes it with the ingress, but it keeps the Secret that holds the certificate, unless its controller runs with ",[740,823,824],{},"--enable-certificate-owner-ref",", which is off by default. The Secret has the same name on every deploy (",[740,827,828],{},"\u003CCWA_CI_TLS_SECRET_NAME>-staging-api","), so the next staging deploy finds a valid certificate for the same hostname and issues nothing. That means:",[615,831,832,835,838],{},[618,833,834],{},"The ingress serves the real certificate from its first second, so Full (strict) never sees a temporary one.",[618,836,837],{},"No request reaches Let's Encrypt, so deploying staging and then production many times a day can't hit its limit of 5 identical certificates a week.",[618,839,840,843],{},[621,841,842],{},"The exception is a long gap."," Nothing renews the Secret while staging is uninstalled. If staging goes more than about 90 days without a deploy, the next deploy serves the expired certificate for the minute it takes to renew it, and Cloudflare returns a 526 for that minute. It fixes itself.",[611,845,846,847,849],{},"If your cluster's cert-manager does run with ",[740,848,824],{},", every staging deploy issues a new certificate and briefly serves a temporary one. Keep staging DNS only on such a cluster.",[702,851,852],{"icon":704,"color":705},[611,853,854,857,858,861,862,865,866,869,870,873],{},[621,855,856],{},"A proxied hostname must be at most one level below the zone."," On the Free plan, Cloudflare's Universal SSL certificate covers only the zone's apex and its first-level subdomains (",[740,859,860],{},"example.com"," and ",[740,863,864],{},"*.example.com","). A proxied hostname two levels down, such as ",[740,867,868],{},"staging.preview.cwa.rocks"," in the ",[740,871,872],{},"cwa.rocks"," zone, gets no valid certificate at Cloudflare's edge, so the browser's TLS handshake fails before the request reaches your origin. When the site's own domain is a subdomain, keep staging DNS only, or cover the deeper names with Advanced Certificate Manager (paid) or a custom certificate.",[611,875,876,879],{},[621,877,878],{},"Review apps stay DNS only."," Cloudflare lets every plan proxy a wildcard record, but proxying review apps gains nothing and costs a lot:",[615,881,882,885,888],{},[618,883,884],{},"Under a domain that is itself a subdomain, review hostnames are two levels down and get no edge certificate on Free (see the callout above).",[618,886,887],{},"Every review app is a new hostname. Its first certificate is issued while the ingress serves a temporary one, so Full (strict) returns a 526 for the first minute or so.",[618,889,890],{},"A proxied wildcard turns every unused name into a 526 instead of a DNS miss.",[611,892,893],{},"Review apps are development hostnames with no real traffic, and nothing else on this page applies to them.",[611,895,896,899,900,903,904,661],{},[621,897,898],{},"Leave Always Use HTTPS off"," (SSL\u002FTLS → Edge Certificates). ingress-nginx already redirects HTTP to HTTPS at your origin, except for ",[740,901,902],{},"\u002F.well-known\u002Facme-challenge\u002F",", where cert-manager answers Let's Encrypt's HTTP-01 challenge over plain HTTP. Always Use HTTPS redirects at Cloudflare instead, before the request reaches the origin, and it makes no exception for that path. Let's Encrypt follows the redirect to HTTPS, and Cloudflare then connects to your origin over HTTPS with Full (strict). While the origin's certificate is missing or expired, Cloudflare refuses it, so the challenge fails: a hostname proxied before its first certificate can't get one, and an expired certificate (such as staging's after a long gap) can never renew. If a zone needs an edge redirect, turn Always Use HTTPS off and use a redirect rule that leaves out requests whose path starts with ",[740,905,902],{},[611,907,908,911],{},[621,909,910],{},"Delete proxied records that no deploy serves."," A leftover record, such as one for a retired staging host or an old wildcard, still answers through Cloudflare, with an error or the ingress's temporary certificate.",[668,913,915],{"id":914},"leave-cloudflares-cache-alone","Leave Cloudflare's cache alone",[611,917,918,919,922],{},"Don't add Cache Rules in Option A. Cloudflare's default caching then does the right thing. It caches only files with static extensions (scripts, stylesheets, images, fonts, PDFs and so on), never HTML or JSON, and never a ",[740,920,921],{},"private"," response or one that sets a cookie:",[615,924,925,944],{},[618,926,927,934,935,938,939,943],{},[621,928,929,930,933],{},"Pages and ",[740,931,932],{},"\u002F_api"," responses pass straight through to Souin."," Their ",[740,936,937],{},"s-maxage"," is meant for Souin, which is purged on every write. Without the setup in ",[626,940,942],{"href":941},"#option-b-proxy-with-edge-caching","Option B: proxy with edge caching",", purges don't reach Cloudflare, so it must not keep them.",[618,945,946,949,950,953,954,957,958,961],{},[621,947,948],{},"Static files are served from the edge."," That covers ",[740,951,952],{},"\u002F_nuxt\u002F*",", whose names change with every build, the files in Nuxt's ",[740,955,956],{},"public\u002F"," folder, and media on a proxied ",[740,959,960],{},"cdn."," hostname, whose stored names are unique. A deleted upload stays at the edge until its lifetime runs out; purge its URL in Cloudflare if it must disappear at once.",[702,963,964],{"icon":704,"color":705},[611,965,966,969,970,973,974,980],{},[621,967,968],{},"Don't add a Cache Rule that bypasses the cache for everything",", as some account templates do. With it, every ",[740,971,972],{},"\u002F_nuxt"," request reaches php. Under a load test, that ran php out of memory (",[626,975,979],{"href":976,"rel":977},"https:\u002F\u002Fgithub.com\u002Fcomponents-web-app\u002Fcomponents-web-app\u002Fissues\u002F117",[978],"nofollow","components-web-app#117","). If a zone has one, delete it.",[611,982,983],{},"Two settings make this safe:",[615,985,986,1012],{},[618,987,988,991,992,995,996,999,1000,1003,1004,1008,1009,1011],{},[621,989,990],{},"The service worker must not be cached."," Cloudflare caches ",[740,993,994],{},".js"," by default, and ",[740,997,998],{},"\u002Fsw.js"," keeps its name across builds. The template sends it with ",[740,1001,1002],{},"Cache-Control: public, max-age=0, must-revalidate",", so Cloudflare passes every update check to the origin (see ",[626,1005,1007],{"href":1006},"\u002Fnuxt-module\u002Fpwa#serving-the-service-worker","PWA › Serving the service worker","). Without that header, add a Cache Rule that bypasses the cache for ",[740,1010,998],{}," only, or visitors get the update up to two hours late after a deploy.",[618,1013,1014,1017,1018,1021,1022,1024,1025,1028,1029,1032,1033,1035,1036,1039,1040,1042,1043,1045,1046,1051],{},[621,1015,1016],{},"Set Browser Cache TTL to Respect existing headers"," (Caching → Configuration). Any other value replaces a shorter ",[740,1019,1020],{},"Cache-Control"," from the origin on the files Cloudflare caches. A 4-hour setting turns ",[740,1023,998],{},"'s ",[740,1026,1027],{},"max-age=0"," into ",[740,1030,1031],{},"max-age=14400"," for browsers, and gives ",[740,1034,956],{}," files such as the logo and favicon 4 hours in browsers. With ",[621,1037,1038],{},"Respect existing headers",", the origin decides: ",[740,1041,972],{}," files keep their immutable year, ",[740,1044,998],{}," is always checked, and media keeps the bucket's header (",[626,1047,1050],{"href":1048,"rel":1049},"https:\u002F\u002Fgithub.com\u002Fcomponents-web-app\u002Fcomponents-web-app\u002Fissues\u002F120",[978],"components-web-app#120",").",[611,1053,1054],{},"Check it:",[1056,1057,1062],"pre",{"className":1058,"code":1059,"language":1060,"meta":1061,"style":1061},"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'\ncurl -sI https:\u002F\u002Fwww.example.com\u002Fsw.js | grep -i -E 'cf-cache-status|cache-control'\n","sh","",[740,1063,1064,1104],{"__ignoreMap":1061},[1065,1066,1069,1073,1077,1081,1085,1088,1091,1094,1098,1101],"span",{"class":1067,"line":1068},"line",1,[1065,1070,1072],{"class":1071},"sRCss","curl",[1065,1074,1076],{"class":1075},"szhYu"," -sI",[1065,1078,1080],{"class":1079},"sLL54"," https:\u002F\u002Fwww.example.com\u002F",[1065,1082,1084],{"class":1083},"sVlFx"," |",[1065,1086,1087],{"class":1071}," grep",[1065,1089,1090],{"class":1075}," -i",[1065,1092,1093],{"class":1075}," -E",[1065,1095,1097],{"class":1096},"seSrl"," '",[1065,1099,1100],{"class":1079},"cf-cache-status|cache-status",[1065,1102,1103],{"class":1096},"'\n",[1065,1105,1107,1109,1111,1114,1116,1118,1120,1122,1124,1127],{"class":1067,"line":1106},2,[1065,1108,1072],{"class":1071},[1065,1110,1076],{"class":1075},[1065,1112,1113],{"class":1079}," https:\u002F\u002Fwww.example.com\u002Fsw.js",[1065,1115,1084],{"class":1083},[1065,1117,1087],{"class":1071},[1065,1119,1090],{"class":1075},[1065,1121,1093],{"class":1075},[1065,1123,1097],{"class":1096},[1065,1125,1126],{"class":1079},"cf-cache-status|cache-control",[1065,1128,1103],{"class":1096},[615,1130,1131,1145,1160],{},[618,1132,1133,1134,1136,1137,1140,1141,1144],{},"A page and an ",[740,1135,932],{}," response show ",[740,1138,1139],{},"cf-cache-status: DYNAMIC"," (Cloudflare didn't cache it), next to Souin's own ",[740,1142,1143],{},"cache-status"," header.",[618,1146,1147,1148,1151,1152,1155,1156,1159],{},"A ",[740,1149,1150],{},"\u002F_nuxt\u002F*.js"," file shows ",[740,1153,1154],{},"MISS",", then ",[740,1157,1158],{},"HIT"," on the second request.",[618,1161,1162,1164,1165,1167,1168,1171,1172,1175,1176,661],{},[740,1163,998],{}," is never ",[740,1166,1158],{},", and its ",[740,1169,1170],{},"cache-control"," is the template's ",[740,1173,1174],{},"public, max-age=0, must-revalidate",", not ",[740,1177,1031],{},[668,1179,1181],{"id":1180},"real-client-ips","Real client IPs",[611,1183,1184,1185,1188],{},"Behind Cloudflare, every request reaches your cluster from a Cloudflare address, and Cloudflare sends the visitor's address in the ",[740,1186,1187],{},"CF-Connecting-IP"," header. The template's Caddy rate limit already handles this, so there's nothing to configure for Cloudflare:",[615,1190,1191,1211,1214],{},[618,1192,1193,1194,1196,1197,1202,1203,1206,1207,1210],{},"Caddy uses ",[740,1195,1187],{}," only when the request comes from one of ",[626,1198,1201],{"href":1199,"rel":1200},"https:\u002F\u002Fwww.cloudflare.com\u002Fips\u002F",[978],"Cloudflare's published ranges",". The ranges are built into the Caddyfile, and ",[740,1204,1205],{},"CLOUDFLARE_IP_RANGES"," (",[740,1208,1209],{},"CWA_API_CLOUDFLARE_IP_RANGES"," as a CI variable) replaces them if Cloudflare adds new ones. From any other address the header is ignored, because anyone can send it.",[618,1212,1213],{},"Each visitor behind Cloudflare is counted separately, so one busy visitor can't get the whole Cloudflare edge throttled.",[618,1215,1216,1219,1220,1223],{},[740,1217,1218],{},"CADDY_EDGE=true"," (the default in ",[740,1221,1222],{},"compose.prod.yaml",", where Caddy faces the internet itself) still recognises Cloudflare by its ranges.",[702,1225,1226],{"icon":704,"color":705},[611,1227,1228,1234,1235,1238,1239,1242,1243,1246,1247,1249,1250,1253,1254,1257,1258,1261],{},[621,1229,1230,1233],{},[740,1231,1232],{},"getClientIp()"," isn't the visitor."," ",[740,1236,1237],{},"Request::getClientIp()"," trusts ",[740,1240,1241],{},"X-Forwarded-For"," only from ",[740,1244,1245],{},"TRUSTED_PROXIES"," (private ranges), so behind Cloudflare it returns a Cloudflare address, shared by many visitors. Don't add Cloudflare's ranges to ",[740,1248,1245],{},": that would let any client that connects through Cloudflare choose its own address. Use ",[740,1251,1252],{},"App\\Http\\VisitorIp"," instead, as below. Caddy's access log records the visitor as ",[740,1255,1256],{},"visitor_ip",", next to ",[740,1259,1260],{},"client_ip",", which is Cloudflare's.",[1263,1264,1266],"h4",{"id":1265},"the-visitors-address-in-your-own-code","The visitor's address in your own code",[611,1268,1269,1270,1272,1273,1276],{},"Code that needs the visitor's address, such as a per-visitor rate limit on a form, injects the template's ",[740,1271,1252],{}," and calls ",[740,1274,1275],{},"of()",":",[1056,1278,1282],{"className":1279,"code":1280,"language":1281,"meta":1061,"style":1061},"language-php shiki shiki-themes github-light github-dark material-theme-palenight","use App\\Http\\VisitorIp;\nuse Symfony\\Component\\HttpFoundation\\Request;\n\nfinal class ContactFormLimiter\n{\n    public function __construct(private readonly VisitorIp $visitorIp)\n    {\n    }\n\n    public function key(Request $request): string\n    {\n        return $this->visitorIp->of($request) ?? 'unknown';\n    }\n}\n","php",[740,1283,1284,1310,1333,1339,1352,1358,1393,1399,1405,1410,1437,1442,1482,1487],{"__ignoreMap":1061},[1065,1285,1286,1290,1294,1298,1301,1303,1306],{"class":1067,"line":1068},[1065,1287,1289],{"class":1288},"stmX-","use",[1065,1291,1293],{"class":1292},"sc2zw"," App",[1065,1295,1297],{"class":1296},"sBtbT","\\",[1065,1299,1300],{"class":1292},"Http",[1065,1302,1297],{"class":1296},[1065,1304,1305],{"class":1292},"VisitorIp",[1065,1307,1309],{"class":1308},"sOvfz",";\n",[1065,1311,1312,1314,1317,1319,1321,1323,1326,1328,1331],{"class":1067,"line":1106},[1065,1313,1289],{"class":1288},[1065,1315,1316],{"class":1292}," Symfony",[1065,1318,1297],{"class":1296},[1065,1320,346],{"class":1292},[1065,1322,1297],{"class":1296},[1065,1324,1325],{"class":1292},"HttpFoundation",[1065,1327,1297],{"class":1296},[1065,1329,1330],{"class":1292},"Request",[1065,1332,1309],{"class":1308},[1065,1334,1336],{"class":1067,"line":1335},3,[1065,1337,1338],{"emptyLinePlaceholder":23},"\n",[1065,1340,1342,1346,1349],{"class":1067,"line":1341},4,[1065,1343,1345],{"class":1344},"swB56","final",[1065,1347,1348],{"class":1344}," class",[1065,1350,1351],{"class":1071}," ContactFormLimiter\n",[1065,1353,1355],{"class":1067,"line":1354},5,[1065,1356,1357],{"class":1308},"{\n",[1065,1359,1361,1364,1367,1371,1374,1376,1379,1383,1386,1390],{"class":1067,"line":1360},6,[1065,1362,1363],{"class":1344},"    public",[1065,1365,1366],{"class":1344}," function",[1065,1368,1370],{"class":1369},"s3Ny6"," __construct",[1065,1372,1373],{"class":1308},"(",[1065,1375,921],{"class":1344},[1065,1377,1378],{"class":1344}," readonly",[1065,1380,1382],{"class":1381},"sbW4m"," VisitorIp",[1065,1384,1385],{"class":1308}," $",[1065,1387,1389],{"class":1388},"sPB8G","visitorIp",[1065,1391,1392],{"class":1308},")\n",[1065,1394,1396],{"class":1067,"line":1395},7,[1065,1397,1398],{"class":1308},"    {\n",[1065,1400,1402],{"class":1067,"line":1401},8,[1065,1403,1404],{"class":1308},"    }\n",[1065,1406,1408],{"class":1067,"line":1407},9,[1065,1409,1338],{"emptyLinePlaceholder":23},[1065,1411,1413,1415,1417,1421,1423,1425,1427,1430,1432,1434],{"class":1067,"line":1412},10,[1065,1414,1363],{"class":1344},[1065,1416,1366],{"class":1344},[1065,1418,1420],{"class":1419},"sKpYG"," key",[1065,1422,1373],{"class":1308},[1065,1424,1330],{"class":1381},[1065,1426,1385],{"class":1308},[1065,1428,1429],{"class":1388},"request",[1065,1431,808],{"class":1308},[1065,1433,1276],{"class":1083},[1065,1435,1436],{"class":1288}," string\n",[1065,1438,1440],{"class":1067,"line":1439},11,[1065,1441,1398],{"class":1308},[1065,1443,1445,1449,1452,1455,1457,1459,1462,1465,1467,1469,1472,1474,1477,1480],{"class":1067,"line":1444},12,[1065,1446,1448],{"class":1447},"sm4w6","        return",[1065,1450,1451],{"class":1296}," $this",[1065,1453,1454],{"class":1083},"->",[1065,1456,1389],{"class":1388},[1065,1458,1454],{"class":1083},[1065,1460,1461],{"class":1419},"of",[1065,1463,1464],{"class":1308},"($",[1065,1466,1429],{"class":1388},[1065,1468,808],{"class":1308},[1065,1470,1471],{"class":1083}," ??",[1065,1473,1097],{"class":1096},[1065,1475,1476],{"class":1079},"unknown",[1065,1478,1479],{"class":1096},"'",[1065,1481,1309],{"class":1308},[1065,1483,1485],{"class":1067,"line":1484},13,[1065,1486,1404],{"class":1308},[1065,1488,1490],{"class":1067,"line":1489},14,[1065,1491,1492],{"class":1308},"}\n",[611,1494,1495,1498,1499,1501,1502,861,1504,1507,1508,1511,1512,1514,1515,661],{},[740,1496,1497],{},"of(Request): ?string"," returns the address Caddy's rate limit counts, by the same rules: ",[740,1500,1187],{}," only from Cloudflare's ranges, with ",[740,1503,1205],{},[740,1505,1506],{},"CADDY_TRUSTED_PROXIES"," honoured. Caddy passes it to php as the ",[740,1509,1510],{},"CWA_VISITOR_IP"," server variable, an environment value that no request header can set. Without Caddy in front, in console commands and the functional tests, ",[740,1513,1275],{}," returns ",[740,1516,1232],{},[611,1518,1519,1520,1523],{},"The template's optional ingress-nginx limits (",[740,1521,1522],{},"CWA_CI_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.",[668,1525,1527],{"id":1526},"free-protection-worth-turning-on","Free protection worth turning on",[749,1529,1530,1543],{},[752,1531,1532],{},[755,1533,1534,1537,1540],{},[758,1535,1536],{},"Setting",[758,1538,1539],{},"Where",[758,1541,1542],{},"What it does",[768,1544,1545,1558,1571],{},[755,1546,1547,1552,1555],{},[773,1548,1549],{},[621,1550,1551],{},"Rate limiting rule",[773,1553,1554],{},"Security rules → Rate limiting rules",[773,1556,1557],{},"Blocks an IP that sends too many matching requests.",[755,1559,1560,1565,1568],{},[773,1561,1562],{},[621,1563,1564],{},"Bot Fight Mode",[773,1566,1567],{},"Security → Settings",[773,1569,1570],{},"Challenges traffic that Cloudflare identifies as automated.",[755,1572,1573,1578,1581],{},[773,1574,1575],{},[621,1576,1577],{},"Under Attack Mode",[773,1579,1580],{},"the zone's Overview → Quick Actions",[773,1582,1583],{},"Emergency switch: every visitor gets an interstitial check before the site loads.",[611,1585,1586,1589,1590,1593,1594,1596,1597,1600],{},[621,1587,1588],{},"Rate limiting."," The Free plan allows one rule, counted per IP over 10 seconds, and its expression can match only on the path. A blocked IP stays blocked for 10 seconds. Pro allows two. Matching on the query string (to catch cache-busting requests) needs Pro or above. The rule is optional: the template's Caddy rate limit (",[740,1591,1592],{},"RATE_LIMIT_EVENTS",", 3000 a minute per visitor) already stops floods of cache misses, and edge cache hits cost the origin nothing. If you add one, a rule on paths starting with ",[740,1595,932],{}," with a limit of ",[621,1598,1599],{},"1000 requests per 10 seconds"," is a reasonable start. SSR reaches the API inside the cluster, not through Cloudflare, so the rule counts only browser requests.",[611,1602,1603],{},"The limit has to be much higher than it sounds:",[615,1605,1606,1620,1629],{},[618,1607,1608,1611,1612,1616,1617,1619],{},[621,1609,1610],{},"Cloudflare counts edge cache hits."," Only Business and Enterprise can count requests to the origin alone. With the API rule (see ",[626,1613,1615],{"href":1614},"#api-responses-optional","API responses (optional)","), most ",[740,1618,932],{}," requests never reach the origin, but they still count.",[618,1621,1622,1628],{},[621,1623,1624,1625,1627],{},"Client-side navigation makes many ",[740,1626,932],{}," calls per page:"," the route, the page or page data, the layout, component groups, positions and components. A few quick page changes add up to dozens of requests, and an editor in the admin makes far more.",[618,1630,1631,1634],{},[621,1632,1633],{},"People share addresses."," Editors in one office share one IP, and so one counter.",[611,1636,1637,1638,1641,1642,1645,1646,1648],{},"One downstream project set ",[621,1639,1640],{},"100 per 10 seconds",". A single visitor switching pages quickly was blocked with a ",[740,1643,1644],{},"429"," from Cloudflare, while the origin returned no ",[740,1647,1644],{},"s at all. It moved to 1000.",[611,1650,1651,1653,1654,1657,1658,1661,1662,1051],{},[621,1652,1564],{}," 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 ",[626,1655,1656],{"href":582},"load test",". After you turn it on, check that the next deploy's cache warm still gets ",[740,1659,1660],{},"200","s (see ",[626,1663,1665],{"href":1664},"\u002Fdeployment\u002Fci-cd#warming-the-cache-after-a-deploy","CI\u002FCD › Warming the cache after a deploy",[611,1667,1668,1670],{},[621,1669,1577],{}," 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.",[702,1672,1673],{"icon":723},[611,1674,1675,1677,1678,1680],{},[621,1676,728],{}," the rate limiting rule editor with a path rule for ",[740,1679,932],{},", counted per IP over 10 seconds.",[663,1682,942],{"id":1683},"option-b-proxy-with-edge-caching",[668,1685,1687],{"id":1686},"what-it-needs","What it needs",[615,1689,1690,1702,1712],{},[618,1691,1692,1695,1696,1699,1700,661],{},[621,1693,1694],{},"A current template."," Edge caching needs a patched Souin whose Cloudflare purge works, a ",[740,1697,1698],{},"Cache-Tag"," header on every tagged response, and full flushes and deploys that purge everything at the edge. The template has all three. See ",[626,1701,20],{"href":21},[618,1703,1704,1707,1708,1711],{},[621,1705,1706],{},"A Cloudflare API token"," with only the ",[621,1709,1710],{},"Zone → Cache Purge"," permission, limited to the site's zone.",[618,1713,1714,1717,1718,661],{},[621,1715,1716],{},"A plan whose purge rate suits how often editors save."," Free allows 5 purge requests a minute, shared by every site on the account. See ",[626,1719,1721],{"href":1720},"#rate-limits","Rate limits",[611,1723,1724,1725,661],{},"It was tested end to end on the Free plan; the results are at the end of this section, in ",[626,1726,1728],{"href":1727},"#test-results","Test results",[668,1730,1732],{"id":1731},"turning-edge-caching-on","Turning edge caching on",[611,1734,1735,1736,1738],{},"Cloudflare can only purge a page by tag if the response carried ",[740,1737,1698],{}," 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:",[673,1740,1741,1749,1764,1767,1777],{},[618,1742,1743,1744,1746,1747,661],{},"Run a current template: the patched Souin, the ",[740,1745,1698],{}," rule, and full flushes and deploys that purge everything at the edge. See ",[626,1748,20],{"href":21},[618,1750,1751,1752,861,1755,1758,1759,1763],{},"Create the API token, and set ",[740,1753,1754],{},"CADDY_CACHE_CDN_CONFIG",[740,1756,1757],{},"CLOUDFLARE_API_TOKEN"," (see ",[626,1760,1762],{"href":1761},"#secrets","Secrets",", next).",[618,1765,1766],{},"Deploy.",[618,1768,1769,1770,1774,1775,1051],{},"Only then add the pages Cache Rule (see ",[626,1771,1773],{"href":1772},"#the-cache-rules-must-match-the-templates-exclusions","The cache rules must match the template's exclusions","), and optionally the API rule (see ",[626,1776,1615],{"href":1614},[618,1778,1779,1780,661],{},"Check that pages are cached and that edits purge them: see ",[626,1781,1783],{"href":1782},"#checking-purges","Checking purges",[702,1785,1787],{"icon":1786},"i-heroicons-information-circle",[611,1788,1789,1792],{},[621,1790,1791],{},"If the Cache Rule went in before the deploy, nothing more is needed."," That deploy purges everything at the edge after the new image is rolled out, so the edge refills with tagged pages.",[668,1794,1762],{"id":1795},"secrets",[611,1797,1798,1799,1802,1803,1805,1806,1809],{},"Souin's ",[740,1800,1801],{},"cdn"," block comes from ",[740,1804,1754],{},", which defaults to ",[740,1807,1808],{},"strategy hard"," (no CDN). For Cloudflare it reads:",[1056,1811,1816],{"className":1812,"code":1814,"language":1815,"meta":1061},[1813],"language-text","strategy hard\nprovider cloudflare\nzone_id \u003Cyour zone ID>\napi_key $CLOUDFLARE_API_TOKEN\n","text",[740,1817,1814],{"__ignoreMap":1061},[615,1819,1820,1828,1853],{},[618,1821,1822,1827],{},[621,1823,1824,1825,661],{},"Repeat ",[740,1826,1808],{}," Setting the variable replaces the whole default. Leave it unset on a site without Cloudflare.",[618,1829,1830,1836,1837,1839,1840,1843,1844,1846,1847,1849,1850,1852],{},[621,1831,1832,1833,661],{},"Use a scoped API token, and leave out ",[740,1834,1835],{},"email"," With no ",[740,1838,1835],{}," line, the patched Souin sends ",[740,1841,1842],{},"api_key"," as a bearer token, so a token with only the ",[621,1845,1710],{}," permission, limited to the site's zone, is enough. This works on the Free plan. With an ",[740,1848,1835],{}," line, Souin treats ",[740,1851,1842],{}," 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.",[618,1854,1855,1856,1859],{},"How ",[740,1857,1858],{},"$CLOUDFLARE_API_TOKEN"," gets its value is below.",[611,1861,1862],{},"The API token must not appear in pipeline logs.",[615,1864,1865,1912],{},[618,1866,1867,1234,1870,1872,1873,1875,1876,1879,1880,1882,1883,1886,1887,1890,1891,1894,1897,1898,1901,1902,1904,1905,1907,1908,1911],{},[621,1868,1869],{},"GitLab.",[740,1871,1754],{}," has several lines, and GitLab can't mask a multi-line variable. Put the token in its own masked variable, ",[740,1874,1757],{},". Then write ",[740,1877,1878],{},"api_key $CLOUDFLARE_API_TOKEN"," in ",[740,1881,1754],{}," and turn on ",[621,1884,1885],{},"Expand variable reference"," for that variable. GitLab substitutes the token before the deploy runs. Caddy doesn't expand ",[740,1888,1889],{},"$"," variables itself.",[1892,1893],"br",{},[621,1895,1896],{},"The token must reach the deploy job."," GitLab gives a ",[621,1899,1900],{},"Protected"," variable only to jobs on protected branches and tags. If ",[740,1903,1757],{}," is protected and the deploy branch isn't, ",[740,1906,1878],{}," expands to nothing. Leave the token unprotected and give it the ",[740,1909,1910],{},"production"," environment scope (see the rule below). Or protect it only if the deploy branch is protected too.",[618,1913,1914,1917,1918,1920,1921,1925,1926,1929,1930,661],{},[621,1915,1916],{},"GitHub."," GitHub doesn't expand one variable inside another, so put the token directly in ",[740,1919,1754],{}," and store the whole value as a secret. The workflows export repository ",[1922,1923,1924],"em",{},"variables"," to the deploy scripts automatically, but not secrets, so the deploy jobs' ",[740,1927,1928],{},"env:"," must also map it: ",[740,1931,1932],{},"CADDY_CACHE_CDN_CONFIG: ${{ secrets.CADDY_CACHE_CDN_CONFIG }}",[611,1934,1935,1938,1939,1941,1942,1944,1945,1948,1949,1951],{},[621,1936,1937],{},"Give the Cloudflare settings to production and staging only."," Scope ",[740,1940,1754],{}," and the token to the ",[740,1943,1910],{}," environment. That scope includes staging on purpose: on GitLab, the staging job runs under ",[740,1946,1947],{},"environment: production",", and staging can share production's database, so its edits must purge production's pages. Any deploy that has the config purges the whole zone: it sends a purge everything on every deploy and every fixture load, and tag purges on every edit. A review app given it would empty production's edge each time it deploys or loads fixtures. Without it, a review app gets the default ",[740,1950,1808],{},". The template doesn't check where the variable reaches, so the scope is the project's to get right.",[615,1953,1954,1962],{},[618,1955,1956,1959,1960,661],{},[621,1957,1958],{},"GitLab:"," set the environment scope of both variables to ",[740,1961,1910],{},[618,1963,1964,1967,1968,1971,1972,1975,1976,1978,1979,861,1981,661],{},[621,1965,1966],{},"GitHub:"," the staging job uses a ",[740,1969,1970],{},"staging"," environment and review apps use ",[740,1973,1974],{},"review\u002F\u003Cbranch>",". Map the secret in the ",[740,1977,1928],{}," of the staging and production deploy jobs only, not the review job, or store it as an environment secret of ",[740,1980,1910],{},[740,1982,1970],{},[611,1984,1985],{},"In the cluster, the chart stores the value in the release's Kubernetes Secret, not its ConfigMap.",[611,1987,1988,1993,1994,1996,1997,2000],{},[621,1989,1147,1990,1992],{},[740,1991,1801],{}," setting with no value stops php from starting."," Souin panics on a line such as ",[740,1995,1842],{}," with nothing after it, so php crash-loops while helm reports the release deployed. So the deploy checks first, before anything is rolled out. A setting with no value, or a value that is still a ",[740,1998,1999],{},"$NAME"," reference, fails it:",[1056,2002,2005],{"className":2003,"code":2004,"language":1815,"meta":1061},[1813],"CADDY_CACHE_CDN_CONFIG is incomplete, so php would not start:\n  'api_key' has no value\nCheck the variable it refers to reaches this job (on GitLab, a Protected variable is only given to protected branches) and that its reference is expanded.\n",[740,2006,2004],{"__ignoreMap":1061},[668,2008,1773],{"id":2009},"the-cache-rules-must-match-the-templates-exclusions",[611,2011,2012,2013,2016,2017,2020],{},"Cloudflare doesn't cache HTML unless a Cache Rule (on the ",[621,2014,2015],{},"Cache Rules"," page) marks it ",[621,2018,2019],{},"Eligible for cache",". This is the pages rule that was tested. Replace the host with your own:",[1056,2022,2025],{"className":2023,"code":2024,"language":1815,"meta":1061},[1813],"(http.host eq \"preview.cwa.rocks\" and not starts_with(http.request.uri.path, \"\u002F_api\") and not http.cookie contains \"api_component=\")\n",[740,2026,2024],{"__ignoreMap":1061},[611,2028,2029],{},"Set it up like this:",[749,2031,2032,2041],{},[752,2033,2034],{},[755,2035,2036,2038],{},[758,2037,1536],{},[758,2039,2040],{},"Value",[768,2042,2043,2052,2062],{},[755,2044,2045,2048],{},[773,2046,2047],{},"Cache eligibility",[773,2049,2050],{},[621,2051,2019],{},[755,2053,2054,2057],{},[773,2055,2056],{},"Edge TTL",[773,2058,2059],{},[621,2060,2061],{},"Use cache-control header if present, bypass cache if not",[755,2063,2064,2067],{},[773,2065,2066],{},"Browser TTL",[773,2068,2069],{},[621,2070,2071],{},"Respect origin",[611,2073,2074,2077,2078,2081,2082,2085,2086,2088,2089,2092,2093,2095,2096,2099,2100,2102],{},[621,2075,2076],{},"The Edge TTL setting does most of the work."," The template's ",[740,2079,2080],{},"@use_cache"," matcher in ",[740,2083,2084],{},"api\u002Ffrankenphp\u002FCaddyfile"," decides what Souin may cache, and the origin's responses already say what may be shared: a cacheable page carries ",[740,2087,937],{},", and ",[740,2090,2091],{},"\u002Flogin",", error pages and signed-in renders carry ",[740,2094,921],{}," or ",[740,2097,2098],{},"no-store",", or no ",[740,2101,1020],{}," 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:",[615,2104,2105,2116],{},[618,2106,2107,2111,2112,2115],{},[621,2108,2109],{},[740,2110,932],{},", because Cloudflare ignores ",[740,2113,2114],{},"Vary: Accept"," (see the next section). The optional API rule handles it separately.",[618,2117,2118,2125,2126,2128,2129,2132,2133,861,2135,661],{},[621,2119,2120,2121,2124],{},"Requests with an ",[740,2122,2123],{},"api_component"," cookie",", so editors get their own render. The condition matches any ",[740,2127,2123],{}," 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 ",[740,2130,2131],{},"DYNAMIC",". Signed-in renders are never stored either way, because they carry ",[740,2134,2098],{},[740,2136,2137],{},"Set-Cookie",[611,2139,2140],{},"Other exclusions in the template's matcher, and how they're covered at the edge:",[615,2142,2143,2153,2177],{},[618,2144,2145,2148,2149,2152],{},[740,2146,2147],{},"\u002F.well-known\u002Fmercure"," is a stream (SSE). It sends no cacheable headers, and the test showed ",[740,2150,2151],{},"cf-cache-status: BYPASS"," with live updates arriving.",[618,2154,2155,2157,2158,2157,2161,2157,2164,2157,2167,2157,2170,861,2173,2176],{},[740,2156,2091],{},", ",[740,2159,2160],{},"\u002Fforgot-password",[740,2162,2163],{},"\u002Freset-password\u002F*",[740,2165,2166],{},"\u002Fverify-email",[740,2168,2169],{},"\u002Fconfirm-new-email",[740,2171,2172],{},"\u002Fuser-area",[740,2174,2175],{},"\u002F_cwa\u002F*"," are kept out by their own response headers, through the Edge TTL setting.",[618,2178,2179,2157,2181,861,2184,2187],{},[740,2180,952],{},[740,2182,2183],{},"\u002Fuploads\u002F*",[740,2185,2186],{},"\u002Fbundles\u002F*"," are static files. Cloudflare may cache them as it does in Option A, which is safe for the same reasons.",[611,2189,2190,2191,2194,2195,2198,2199,2202,2203,861,2206,2209],{},"On the Free plan, Cache Rules can't use ",[740,2192,2193],{},"matches"," (regex needs Business) or ",[740,2196,2197],{},"http.request.cookies"," (needs Pro), which is why the cookie condition is a plain ",[740,2200,2201],{},"contains",". Request-header fields do work on Free: the API rule's ",[740,2204,2205],{},"http.request.headers[\"accept\"]",[740,2207,2208],{},"[\"authorization\"]"," conditions were accepted and behave as expected.",[611,2211,2212],{},"When the template's matcher changes, check the rule against it again.",[611,2214,2215,2216,2218,2219,2221],{},"Cloudflare also doesn't cache any response that carries ",[740,2217,2137],{},". If pages stay ",[740,2220,2131],{}," under the rule, check for that first.",[611,2223,2224,2227,2228,2230,2231,2234],{},[621,2225,2226],{},"Tag size."," Cloudflare reads at most 16 KB of ",[740,2229,1698],{}," per response. A page's tags are its ",[740,2232,2233],{},"cwa-html"," 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.",[702,2236,2237],{"icon":723},[611,2238,2239,2241],{},[621,2240,728],{}," 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\".",[668,2243,2245,2247],{"id":2244},"_api-needs-its-own-rule",[740,2246,932],{}," needs its own rule",[611,2249,2250,2251,2254,2255,2258,2259,2262,2263,2265,2266,2271,2272,2274,2275,2277,2278,2280],{},"Cloudflare ignores ",[740,2252,2253],{},"Vary",", apart from ",[740,2256,2257],{},"Accept-Encoding",". The API negotiates on the ",[740,2260,2261],{},"Accept"," header (JSON-LD, JSON, or the HTML docs), and custom cache keys on headers are an Enterprise feature. So a rule that caches ",[740,2264,932],{}," by path alone could serve a response in the wrong format, which is the problem Souin hit in ",[626,2267,2270],{"href":2268,"rel":2269},"https:\u002F\u002Fgithub.com\u002Fcomponents-web-app\u002Fcomponents-web-app\u002Fissues\u002F79",[978],"components-web-app#79",". The pages rule above leaves ",[740,2273,932],{}," out. The optional API rule (see ",[626,2276,1615],{"href":1614},") caches it only for the exact ",[740,2279,2261],{}," the module sends.",[668,2282,1615],{"id":2283},"api-responses-optional",[611,2285,2286],{},"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:",[1056,2288,2291],{"className":2289,"code":2290,"language":1815,"meta":1061},[1813],"(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",[740,2292,2290],{"__ignoreMap":1061},[611,2294,2295,2296,2298,2299,2302,2303,2306,2307,2309,2310,2312,2313,2315,2316,2319,2320,2157,2323,2326,2327,2329,2330,2157,2332,2335,2336,2339],{},"Only the exact ",[740,2297,2261],{}," the module sends (",[740,2300,2301],{},"application\u002Fld+json,application\u002Fjson",") and plain ",[740,2304,2305],{},"application\u002Fld+json"," are eligible. That covers ",[740,2308,2114],{},", which Cloudflare ignores: every other ",[740,2311,2261],{},", such as a browser asking for the HTML docs, goes to Souin. Requests with an ",[740,2314,2123],{}," cookie or an ",[740,2317,2318],{},"Authorization"," header are left out too. In the test, ",[740,2321,2322],{},"text\u002Fhtml",[740,2324,2325],{},"application\u002Fjson"," and no ",[740,2328,2261],{}," were all ",[740,2331,2131],{},[740,2333,2334],{},"\u002F_api\u002Fme"," was ",[740,2337,2338],{},"BYPASS",", and an edit's tag purge dropped the API response at the edge.",[611,2341,2342,2348,2349,2351,2352,2355,2356,2358],{},[621,2343,2344,2345,661],{},"Component positions are left out because their responses also vary by ",[740,2346,2347],{},"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 ",[740,2350,2347],{}," request header the module sends, and marks the response ",[740,2353,2354],{},"Vary: path",". Souin honours that, but Cloudflare ignores ",[740,2357,2253],{}," 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.",[611,2360,2361,2362,2366],{},"A deploy purges everything in the zone (see ",[626,2363,2365],{"href":2364},"#purges-must-reach-cloudflare","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.",[668,2368,2370],{"id":2369},"edge-lifetime","Edge lifetime",[611,2372,2373,2374,2376],{},"Keep the Edge TTL on ",[621,2375,2061],{},". Don't choose an override. Two reasons:",[615,2378,2379,2391],{},[618,2380,2381,2386,2387,2390],{},[621,2382,2383,2384,661],{},"\"Ignore cache-control header and use this TTL\" ignores the origin's ",[740,2385,1020],{}," The override replaces the origin's directives, so ",[740,2388,2389],{},"private, no-store"," on a personalised response or an error page would no longer stop Cloudflare caching it.",[618,2392,2393,2396],{},[621,2394,2395],{},"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.",[611,2398,2399,2400,2402],{},"With purges reaching Cloudflare, the origin's year-long ",[740,2401,937],{}," is what you want.",[668,2404,1783],{"id":2405},"checking-purges",[611,2407,2408,2411],{},[621,2409,2410],{},"Purge failures are logged"," in the php container's log. Nothing else reports them: the admin save still succeeds. The messages are:",[749,2413,2414,2424],{},[752,2415,2416],{},[755,2417,2418,2421],{},[758,2419,2420],{},"Message",[758,2422,2423],{},"Meaning",[768,2425,2426,2438,2450,2460,2470],{},[755,2427,2428,2433],{},[773,2429,2430],{},[740,2431,2432],{},"Cloudflare purge of N tags rate limited, will retry in …",[773,2434,1147,2435,2437],{},[740,2436,1644],{},". It's retried.",[755,2439,2440,2445],{},[773,2441,2442],{},[740,2443,2444],{},"Cloudflare purge of N tags failed with status N, will retry: …",[773,2446,1147,2447,2437],{},[740,2448,2449],{},"5xx",[755,2451,2452,2457],{},[773,2453,2454],{},[740,2455,2456],{},"Cloudflare purge of N tags failed, will retry: …",[773,2458,2459],{},"A connection error or timeout. It's retried.",[755,2461,2462,2467],{},[773,2463,2464],{},[740,2465,2466],{},"Cloudflare purge of N tags failed with status N: … [tags]",[773,2468,2469],{},"Any other failure, such as a bad token. The purge is dropped.",[755,2471,2472,2477],{},[773,2473,2474],{},[740,2475,2476],{},"Cloudflare purge: N tags queued, more than the purge limit can send soon, so purging everything instead",[773,2478,2479],{},"A backlog became a Purge Everything.",[611,2481,2482,2483,2486,2487,2490],{},"The same messages say ",[740,2484,2485],{},"Cloudflare purge everything"," instead of ",[740,2488,2489],{},"Cloudflare purge of N tags"," for a full flush.",[611,2492,2493],{},"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.",[611,2495,2496,2499,2500,2503,2504,2095,2506,2509,2510,2513],{},[621,2497,2498],{},"Checking that purges arrive."," Request a page twice and see ",[740,2501,2502],{},"cf-cache-status: HIT",". Edit something on that page in the admin, then request it again. You should get ",[740,2505,1154],{},[740,2507,2508],{},"EXPIRED",", with the new content. On Free, after many saves, the purge may wait in the queue for a minute or more. If it still doesn't arrive, look for ",[740,2511,2512],{},"Cloudflare purge"," in the php container's log.",[668,2515,2365],{"id":2516},"purges-must-reach-cloudflare",[611,2518,2519,2520,2523,2524,2528,2529,2531,2532,2534,2535,1051],{},"In production, the API tells shared caches to keep responses for a year (",[740,2521,2522],{},"s-maxage=31557600","), and pages inherit it (see ",[626,2525,2527],{"href":2526},"\u002Fnuxt-module\u002Fpage-caching#how-long-a-page-is-kept","How long a page is kept","). Cloudflare honours ",[740,2530,937],{},". 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 ",[740,2533,952],{}," files after a deploy, which then never becomes interactive (see ",[626,2536,2538],{"href":2537},"\u002Fdeployment\u002Fci-cd#why-every-deploy-clears-the-page-cache","Why every deploy clears the page cache",[611,2540,2541],{},"Cloudflare purges by tag, and that needs two things:",[615,2543,2544,2567],{},[618,2545,2546,2552,2553,1028,2556,2558,2559,2562,2563,2566],{},[621,2547,2548,2549,2551],{},"Responses must carry ",[740,2550,1698],{}," when Cloudflare stores them."," Souin never sends that header, on a miss or a hit, so the Caddyfile copies ",[740,2554,2555],{},"Surrogate-Key",[740,2557,1698],{},". Cloudflare indexes it and strips it before the response reaches the visitor, so you can't see it in a browser. Only responses for public hostnames get it. Internal hostnames, used by server-side rendering, don't: a hostname with no dot or ending in ",[740,2560,2561],{},".local",", such as ",[740,2564,2565],{},"php.local",". See the warning below.",[618,2568,2569,2572,2573,2486,2576,2579],{},[621,2570,2571],{},"Souin must send each tag purge to Cloudflare."," Unpatched Souin v1.7.9, the version the template builds, posts to ",[740,2574,2575],{},"\u002Fzones\u002F\u003Cid>\u002Fpurge",[740,2577,2578],{},"\u002Fpurge_cache",", so no purge reaches Cloudflare, with any credential, and the failure is silent. The template patches Souin: it calls the right endpoint, gives each request a 30-second timeout and logs any failure.",[702,2581,2582],{"icon":704,"color":705},[611,2583,2584,2596,2597,2599,2600,2603,2604,2607,2608,2610,2611,2614,2615,2617,2618,2620,2621,2623,2624,2627,2628],{},[621,2585,2586,2587,2589,2590,2592,2593,2595],{},"Internal hostnames get no ",[740,2588,1698],{},", and their ",[740,2591,932],{}," responses no ",[740,2594,2555],{},", because of Node's header limit."," Node refuses a response whose headers pass 16 KB by default. ",[740,2598,2555],{}," lists every resource in a collection, so ",[740,2601,2602],{},"\u002F_api\u002F_\u002Froutes"," alone passes that at about 230 routes; a site with about 1,900 routes sent 184 KB. Server-side rendering's request for the routes would fail, and the sitemap would come out empty (",[740,2605,2606],{},"UND_ERR_HEADERS_OVERFLOW"," in the Nuxt log). So on a hostname with no dot, or one ending in ",[740,2609,2561],{}," (server-side rendering's, and ",[740,2612,2613],{},"localhost"," in development), Caddy removes ",[740,2616,2555],{}," from ",[740,2619,932],{}," responses after Souin has stored their tags. Purges are exact as before. To see an API response's tags, request it on a public host, or purge by the resource's IRI. Page HTML keeps ",[740,2622,2555],{}," on every host. Server-side rendering never goes through Cloudflare, so its hostnames don't need the tag. The app image also sets ",[740,2625,2626],{},"NODE_OPTIONS=--max-http-header-size=65536",", which raises Node's limit to 64 KB. ",[621,2629,2630,2631,2634,2635,2638],{},"A deploy that sets its own ",[740,2632,2633],{},"NODE_OPTIONS"," replaces that, so add ",[740,2636,2637],{},"--max-http-header-size=65536"," to yours.",[611,2640,2641,2642,2644,2645,2647],{},"The ",[740,2643,1801],{}," block is set up in ",[626,2646,1762],{"href":1761},". Which purges reach Cloudflare:",[749,2649,2650,2660],{},[752,2651,2652],{},[755,2653,2654,2657],{},[758,2655,2656],{},"Purge",[758,2658,2659],{},"Reaches Cloudflare?",[768,2661,2662,2670,2684,2702,2718],{},[755,2663,2664,2667],{},[773,2665,2666],{},"Saving content in the admin (tag purge of the changed IRIs)",[773,2668,2669],{},"Yes",[755,2671,2672,2678],{},[773,2673,2674,2675],{},"Site settings → purge page cache, and ",[740,2676,2677],{},"purge-rendered-html",[773,2679,2680,2681,2683],{},"Yes, the ",[740,2682,2233],{}," tag",[755,2685,2686,2699],{},[773,2687,2688,2095,2691,2694,2695,808],{},[740,2689,2690],{},"purge-http-cache --path",[740,2692,2693],{},"--tag",", to purge one page (see ",[626,2696,2698],{"href":2697},"\u002Fnuxt-module\u002Fpage-caching#purging-one-page","Page Caching › Purging One Page",[773,2700,2701],{},"Yes, those tags",[755,2703,2704,2715],{},[773,2705,2706,2157,2709,2712,2713,808],{},[621,2707,2708],{},"Purge all cached data",[740,2710,2711],{},"purge-http-cache",", and every fixture load (it ends with ",[740,2714,2711],{},[773,2716,2717],{},"Yes, everything in the zone",[755,2719,2720,2729],{},[773,2721,2722,2723,1879,2726],{},"The deploy purge, ",[740,2724,2725],{},"purge_rendered_html",[740,2727,2728],{},"k8s.sh",[773,2730,2731,2732,1879,2735,2737,2738,2740,2741,2744,2745,2747,2748,2750,2751,808],{},"Yes, everything in the zone. With ",[740,2733,2734],{},"provider cloudflare",[740,2736,1754],{},", it runs ",[740,2739,2677],{}," and then the template's ",[740,2742,2743],{},"app:edge-cache:purge-everything",", which purges the zone through Cloudflare's API without touching Souin's ",[740,2746,932],{}," cache. If either fails, it flushes with ",[740,2749,2711],{}," instead (see ",[626,2752,2753],{"href":2537},"CI\u002FCD › Why every deploy clears the page cache",[702,2755,2757],{"icon":2756},"i-heroicons-light-bulb",[611,2758,2759,2762,2763,2095,2766,1206,2769,2772,2773,861,2775,2777],{},[621,2760,2761],{},"To clear stale content, purge the page or the tag, not everything."," Use the admin's purge for one page, or ",[740,2764,2765],{},"purge-http-cache --path \u002F\u003Cpath>",[740,2767,2768],{},"--tag \u003Ctag>",[626,2770,2771],{"href":2697},"Purging one page","). Keep ",[621,2774,2708],{},[740,2776,2711],{}," with no options as a last resort: they empty Souin's API responses as well as the edge, so the next visitor to each page waits for a full cold render, about 12 seconds on one CMS-heavy home page.",[611,2779,2780,2781,2783,2784,2787,2788,2791],{},"Every row but the deploy's edge-only purge needs the patched Souin. The three tag rows also need the ",[740,2782,1698],{}," rule; a full flush doesn't, because Souin sends it to Cloudflare as a ",[621,2785,2786],{},"purge everything",". A tag purge removes exactly the pages carrying that tag, and IRI tags, which contain ",[740,2789,2790],{},"\u002F",", match.",[611,2793,2794,2797,2798,2800],{},[621,2795,2796],{},"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 ",[740,2799,1754],{}," 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 cache warm requests every page, and normal traffic does the rest.",[1263,2802,1721],{"id":2803},"rate-limits",[611,2805,2806],{},"Cloudflare limits purges by tag, hostname and prefix, and Purge Everything, all under one limit:",[749,2808,2809,2822],{},[752,2810,2811],{},[755,2812,2813,2816,2819],{},[758,2814,2815],{},"Plan",[758,2817,2818],{},"Purge requests",[758,2820,2821],{},"Burst",[768,2823,2824,2835,2845,2856],{},[755,2825,2826,2829,2832],{},[773,2827,2828],{},"Free",[773,2830,2831],{},"5 per minute",[773,2833,2834],{},"25",[755,2836,2837,2840,2843],{},[773,2838,2839],{},"Pro",[773,2841,2842],{},"5 per second",[773,2844,2834],{},[755,2846,2847,2850,2853],{},[773,2848,2849],{},"Business",[773,2851,2852],{},"10 per second",[773,2854,2855],{},"50",[755,2857,2858,2861,2864],{},[773,2859,2860],{},"Enterprise",[773,2862,2863],{},"50 per second",[773,2865,2866],{},"500",[615,2868,2869,2877,2883,2889],{},[618,2870,2871,2874,2875,661],{},[621,2872,2873],{},"Every purge counts, not only full ones."," A tag purge for one edited component counts the same as a Purge Everything. Each save in the admin is at least one request, and so are every deploy and every ",[621,2876,2708],{},[618,2878,2879,2882],{},[621,2880,2881],{},"It counts requests, not tags."," One request can carry up to 100 tags.",[618,2884,2885,2888],{},[621,2886,2887],{},"The limit is per Cloudflare account, not per zone."," Every site on the account shares it, and so do staging and production.",[618,2890,2891,2894],{},[621,2892,2893],{},"Purging single URLs has its own, much higher limit"," (800 URLs a second on Free), but Souin purges by tag, so it doesn't help.",[611,2896,2897,2898,661],{},"Check the current figures in ",[626,2899,2902],{"href":2900,"rel":2901},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Fcache\u002Fhow-to\u002Fpurge-cache\u002F",[978],"Cloudflare's purge limits",[668,2904,647],{"id":2905},"queued-and-paced-purges",[611,2907,2908],{},"The patched Souin doesn't send each purge to Cloudflare straight away. It queues them and sends them at the rate your plan allows:",[615,2910,2911,2917,2927,2950],{},[618,2912,2913,2916],{},[621,2914,2915],{},"Tags are merged."," Each zone has one sender. It drops duplicate tags and sends up to 100 tags per request, so a burst of saves becomes a few requests.",[618,2918,2919,2922,2923,2926],{},[621,2920,2921],{},"The pace matches the plan."," The sender keeps its own allowance, sized by ",[740,2924,2925],{},"CLOUDFLARE_PURGE_PLAN",", and waits for it to refill instead of sending more than Cloudflare accepts.",[618,2928,2929,2932,2933,2935,2936,2939,2940,2942,2943,2095,2946,2949],{},[621,2930,2931],{},"A refused purge is retried."," After a ",[740,2934,1644],{}," it waits for Cloudflare's ",[740,2937,2938],{},"Retry-After",". A ",[740,2941,2449],{}," or a connection error is retried too, after at least 5 seconds. Any other failure, such as a bad token (",[740,2944,2945],{},"401",[740,2947,2948],{},"403","), is logged and dropped.",[618,2951,2952,2955],{},[621,2953,2954],{},"A big backlog becomes one Purge Everything."," When more tags are queued than the allowance can send within about a minute, the sender purges everything in the zone instead: one request, and nothing stale is left. A full flush also replaces anything queued.",[611,2957,2958],{},"Souin's own cache is still purged at once. Only the edge waits, so under heavy editing on Free an edit can take a minute or more to show at the edge, and backlogs turn into Purge Everything, which empties the edge cache for every page.",[1263,2960,2962],{"id":2961},"settings","Settings",[611,2964,2965,2966,1206,2968,2157,2971,2157,2974,2095,2977,2980,2981,2983,2984,2157,2987,861,2990,2993,2994,2997,2998,3001,3002,661],{},"Four optional settings control the pace: ",[740,2967,2925],{},[740,2969,2970],{},"free",[740,2972,2973],{},"pro",[740,2975,2976],{},"business",[740,2978,2979],{},"enterprise",", which sets the rate and burst to that plan's limits, as in the table under ",[626,2982,1721],{"href":1720},"), and ",[740,2985,2986],{},"CLOUDFLARE_PURGE_REQUESTS",[740,2988,2989],{},"_WINDOW",[740,2991,2992],{},"_BURST"," to set them yourself. They're read from the php container's environment. In a deploy, set each as a CI variable named ",[740,2995,2996],{},"CWA_API_"," plus the name, such as ",[740,2999,3000],{},"CWA_API_CLOUDFLARE_PURGE_PLAN",". Defaults and details are in ",[626,3003,3005],{"href":3004},"\u002Fdeployment\u002Fvariables#cdn-purging","CI\u002FCD Variables › CDN purging",[611,3007,3008,3010],{},[740,3009,2970],{}," is safe on every plan, but on a paid plan it delays purges for no reason, so set the plan you're on.",[702,3012,3013,3029],{"icon":704,"color":705},[611,3014,3015,3018,3019,861,3022,3025,3026,3028],{},[621,3016,3017],{},"Each php process counts on its own, but Cloudflare's limit is per account."," When staging and production, or several sites, share an account, give each a share of the allowance. For example, on Free with two sites, set ",[740,3020,3021],{},"CWA_API_CLOUDFLARE_PURGE_REQUESTS=2",[740,3023,3024],{},"CWA_API_CLOUDFLARE_PURGE_BURST=10"," on each. Going over isn't fatal, because a ",[740,3027,1644],{}," caused by a neighbour is retried, but it delays the purges.",[611,3030,3031,3032,3035,3036,3039,3040,2157,3043,861,3046,3049],{},"To give a site less than one request a minute, make the window longer than ",[740,3033,3034],{},"1m",": ",[740,3037,3038],{},"CWA_API_CLOUDFLARE_PURGE_WINDOW"," takes any positive Go duration. One account with six sites on Free set ",[740,3041,3042],{},"CWA_API_CLOUDFLARE_PURGE_REQUESTS=1",[740,3044,3045],{},"CWA_API_CLOUDFLARE_PURGE_WINDOW=90s",[740,3047,3048],{},"CWA_API_CLOUDFLARE_PURGE_BURST=3"," on each. Together that's 4 requests a minute and a burst of 18, under Free's 5 and 25.",[611,3051,3052,3053,3056],{},"A lower rate also turns a backlog into a Purge Everything sooner, because the sender coalesces once the queue needs more requests than its burst plus about a minute's allowance. At 1 per ",[740,3054,3055],{},"90s"," with a burst of 3, a full allowance covers 3 requests plus two-thirds of one, so more than 300 queued tags become a Purge Everything.",[611,3058,3059,3062],{},[621,3060,3061],{},"The queue is held in memory."," If the php pod crashes, purges still queued are lost, and their pages stay stale at the edge until a later purge clears them. A deploy purges everything, so a rollout is covered.",[702,3064,3065],{"icon":2756},[611,3066,3067,3070],{},[621,3068,3069],{},"Pro or above suits a site that editors update regularly."," On Free, purges aren't lost, but under heavy editing they arrive late, and backlogs become a Purge Everything that empties the edge cache. Free is fine for a site that changes rarely.",[668,3072,1728],{"id":3073},"test-results",[3075,3076,3078,3094],"collapsible",{"name":3077},"test results",[611,3079,3080,3081,3084,3085,3087,3088,3093],{},"The setup above was tested end to end on ",[740,3082,3083],{},"preview.cwa.rocks",", on Cloudflare's ",[621,3086,2828],{}," plan, with a scoped token (",[626,3089,3092],{"href":3090,"rel":3091},"https:\u002F\u002Fgithub.com\u002Fcomponents-web-app\u002Fcomponents-web-app\u002Fissues\u002F108",[978],"components-web-app#108","):",[749,3095,3096,3106],{},[752,3097,3098],{},[755,3099,3100,3103],{},[758,3101,3102],{},"Check",[758,3104,3105],{},"Result",[768,3107,3108,3121,3131,3145,3156,3166,3182,3195],{},[755,3109,3110,3113],{},[773,3111,3112],{},"Pages are cached at the edge",[773,3114,3115,3116,3118,3119,661],{},"Every sitemap page went ",[740,3117,1154],{}," → ",[740,3120,1158],{},[755,3122,3123,3128],{},[773,3124,3125,3127],{},[740,3126,1698],{}," reaches Cloudflare",[773,3129,3130],{},"Yes, and Cloudflare removes it before the visitor sees the response.",[755,3132,3133,3136],{},[773,3134,3135],{},"An edit purges the affected page",[773,3137,3138,3139,3141,3142,3144],{},"That page went ",[740,3140,1154],{},", a sibling page stayed ",[740,3143,1158],{},", and no purge errors were logged.",[755,3146,3147,3150],{},[773,3148,3149],{},"A deploy purges every page",[773,3151,3152,3153,3155],{},"Every page went ",[740,3154,1154],{}," within seconds.",[755,3157,3158,3161],{},[773,3159,3160],{},"Mercure through Cloudflare",[773,3162,3163,3165],{},[740,3164,2151],{},", and live updates arrive.",[755,3167,3168,3172],{},[773,3169,2120,3170,2124],{},[740,3171,2123],{},[773,3173,3174,3176,3177,3179,3180,661],{},[740,3175,2131],{}," with ",[740,3178,2098],{},". A request with an unrelated cookie still gets a ",[740,3181,1158],{},[755,3183,3184,3191],{},[773,3185,3186,3188,3189,808],{},[740,3187,932],{}," without the API rule (see ",[626,3190,1615],{"href":1614},[773,3192,3193],{},[740,3194,2131],{},[755,3196,3197,3202],{},[773,3198,3199,3201],{},[740,3200,932],{}," with the API rule",[773,3203,3204,3205,3207,3208,3118,3210,3212,3213,3215,3216,2315,3218,3220,3221,2157,3223,3225,3226,3228],{},"The module's ",[740,3206,2261],{}," goes ",[740,3209,1154],{},[740,3211,1158],{},". Any other ",[740,3214,2261],{},", an ",[740,3217,2123],{},[740,3219,2318],{}," header is ",[740,3222,2131],{},[740,3224,2334],{}," is ",[740,3227,2338],{},", and an edit's tag purge drops the API response at the edge.",[663,3230,660],{"id":3231},"moving-a-zone-to-another-cloudflare-account",[611,3233,3234,3235,3240,3241,661],{},"Cloudflare has no way to move a zone between accounts, in the dashboard or the API. You add the domain to the new account as a new zone, rebuild it there, and then change the nameservers. Nothing is carried over: not DNS records, settings, rules or certificates (",[626,3236,3239],{"href":3237,"rel":3238},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Ffundamentals\u002Fmanage-domains\u002Fmove-domain\u002F",[978],"Move a domain between Cloudflare accounts","). A domain registered with Cloudflare Registrar has its own ",[626,3242,3245],{"href":3243,"rel":3244},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Fregistrar\u002Faccount-options\u002Finter-account-transfer\u002F",[978],"inter-account transfer",[611,3247,3248],{},"The site can move without downtime, because the template's ingress already serves a valid Let's Encrypt certificate for each hostname. Visitors can go straight to the origin while the new zone waits for its edge certificate.",[673,3250,3251,3257,3267,3273,3279,3288,3310,3323],{},[618,3252,3253,3256],{},[621,3254,3255],{},"Turn off DNSSEC first",", if the domain uses it. Cloudflare requires it before the nameservers change.",[618,3258,3259,3262,3263,3266],{},[621,3260,3261],{},"Add the domain in the new account."," The zone shows ",[621,3264,3265],{},"Pending"," until its nameservers point at it.",[618,3268,3269,3272],{},[621,3270,3271],{},"Copy the DNS records."," Export them from the old zone and import them into the new one, then check each record. Cloudflare's docs warn that skipping the export makes it import proxied records, which can give error 1000.",[618,3274,3275,3278],{},[621,3276,3277],{},"Set every proxied record to DNS only in the new zone."," On Free, a full-setup zone gets its Universal SSL certificate only after it activates, which takes 15 minutes to 24 hours, and it can't be validated in advance. A proxied record before then has no edge certificate. DNS only sends visitors to the ingress's Let's Encrypt certificate instead.",[618,3280,3281,3284,3285,3287],{},[621,3282,3283],{},"Copy the settings and rules by hand:"," the SSL\u002FTLS mode, Always Use HTTPS, Browser Cache TTL, Cache Rules, the rate limiting rule and Bot Fight Mode. The ",[626,3286,655],{"href":654}," lists what each option needs.",[618,3289,3290,3293,3294,1879,3297,3299,3300,3302,3303,3305,3306,3309],{},[621,3291,3292],{},"With edge caching, point purges at the new zone before it goes live."," Create an API token limited to the new zone, put the new ",[740,3295,3296],{},"zone_id",[740,3298,1754],{},", update ",[740,3301,1757],{},", and deploy (see ",[626,3304,1762],{"href":1761},"). A tag purge to a zone that's still Pending is refused with ",[740,3307,3308],{},"403 Unable to purge",". Souin logs it and drops it, and nothing is lost, because the new zone has nothing cached yet.",[618,3311,3312,3315,3316,3319,3320,661],{},[621,3313,3314],{},"Change the nameservers at the registrar",", then select ",[621,3317,3318],{},"Re-check now"," on the new zone's Overview. The old zone shows ",[621,3321,3322],{},"Moved Away",[618,3324,3325,3328],{},[621,3326,3327],{},"Switch the records back to Proxied once the edge certificate is active"," (SSL\u002FTLS → Edge Certificates). Edge caching only starts then.",[702,3330,3331],{"icon":704,"color":705},[611,3332,3333,3336,3337,3340,3341,3343,3344,3349],{},[621,3334,3335],{},"A proxied CNAME to a hostname in another account's zone fails with error 1014"," (CNAME Cross-User Banned). Watch for this with a media hostname such as ",[740,3338,3339],{},"cdn.\u003Csite>"," that points at a shared ",[740,3342,960],{}," hostname in the old account. Point it at the origin directly instead, for example with an A record to the bucket's load balancer, as long as that origin has a certificate for the name. Cloudflare's own fix, ",[626,3345,3348],{"href":3346,"rel":3347},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Fcloudflare-for-platforms\u002Fcloudflare-for-saas\u002F",[978],"Cloudflare for SaaS"," on the target's account, is the other option.",[611,3351,3352,3355,3356,2157,3359,861,3361,3363,3364,661],{},[621,3353,3354],{},"Moving several sites to one account means they share its purge limit."," Free allows 5 purge requests a minute for the whole account. Give each site a share with ",[740,3357,3358],{},"CWA_API_CLOUDFLARE_PURGE_REQUESTS",[740,3360,2989],{},[740,3362,2992],{},", as in ",[626,3365,2962],{"href":3366},"#settings",[702,3368,3369],{"icon":723},[611,3370,3371,3373,3374,3376],{},[621,3372,728],{}," the new zone's DNS records page with every record set to ",[621,3375,717],{},", next to SSL\u002FTLS → Edge Certificates showing the Universal certificate not yet active.",[663,3378,3380],{"id":3379},"per-site-checklist","Per-site checklist",[611,3382,3383],{},[621,3384,3385],{},"Option A (every site):",[673,3387,3388,3391,3404,3410,3416,3419,3433,3445,3450,3456,3462],{},[618,3389,3390],{},"The origin serves a valid certificate on every hostname before its record is proxied.",[618,3392,3393,3394,3397,3398,3400,3401,3403],{},"Each track's record matches ",[626,3395,735],{"href":3396},"#each-deploy-track",": production and staging are ",[621,3399,684],{},", and the review wildcard is ",[621,3402,717],{},". Staging is DNS only if it is two levels below the zone.",[618,3405,3406,3407,3409],{},"SSL\u002FTLS mode is ",[621,3408,694],{}," (SSL\u002FTLS → Overview).",[618,3411,3412,3415],{},[621,3413,3414],{},"Always Use HTTPS"," is off (SSL\u002FTLS → Edge Certificates).",[618,3417,3418],{},"No proxied record points at a hostname that no deploy serves. Delete leftovers.",[618,3420,3421,3422,3424,3425,3427,3428,1151,3430,3432],{},"There are no Cache Rules, not even a bypass-everything one, and a page and an ",[740,3423,932],{}," response both show ",[740,3426,1139],{},", while a ",[740,3429,1150],{},[740,3431,1158],{}," on its second request.",[618,3434,3435,3436,2088,3438,1164,3440,3442,3443,661],{},"Browser Cache TTL is ",[621,3437,1038],{},[740,3439,998],{},[740,3441,1158],{}," and keeps the template's ",[740,3444,1170],{},[618,3446,3447,3448,661],{},"If there's a rate limiting rule, you can browse quickly between pages, and edit in the admin, without a ",[740,3449,1644],{},[618,3451,3452,3453,3455],{},"Bot Fight Mode is on, and the next deploy's cache warm still gets ",[740,3454,1660],{},"s.",[618,3457,3458,3459,3461],{},"Someone on the team knows where ",[621,3460,1577],{}," is (Overview → Quick Actions) and when to use it.",[618,3463,3464],{},"Nothing in the site's own code relies on the client IP, and the ingress-nginx rate limit variables are unset.",[611,3466,3467],{},[621,3468,3469],{},"Option B (if you turn on edge caching):",[673,3471,3472,3499,3507,3538,3556,3573,3591,3594,3601,3612],{},[618,3473,3474,3475,3477,3478,3480,3481,3483,3484,3486,3487,3489,3490,3492,3493,3496,3497,661],{},"Everything in Option A, except the \"no Cache Rules\" check: a page shows ",[740,3476,1158],{}," on its second request. Without the API rule, an ",[740,3479,932],{}," response still shows ",[740,3482,2131],{},". With it, an ",[740,3485,932],{}," request with the module's ",[740,3488,2261],{}," shows ",[740,3491,1158],{}," on its second request, and with ",[740,3494,3495],{},"Accept: text\u002Fhtml"," it shows ",[740,3498,2131],{},[618,3500,3501,3502,3504,3505,661],{},"A page response from the origin carries ",[740,3503,1698],{}," matching its ",[740,3506,2555],{},[618,3508,3509,3511,3512,2157,3514,2157,3516,861,3518,3520,3521,3523,3524,3526,3527,3529,3530,3532,3533,3535,3536,1051],{},[740,3510,1754],{}," has ",[740,3513,1808],{},[740,3515,2734],{},[740,3517,3296],{},[740,3519,1842],{},", and no ",[740,3522,1835],{}," line. The token has only ",[621,3525,1710],{}," for this zone, and is masked (GitLab, with ",[621,3528,1885],{}," on for ",[740,3531,1754],{},") or a secret (GitHub). On GitLab, both variables are scoped to ",[740,3534,1910],{},", and the token is protected only if the deploy branch is. No review deploy receives either (see the scoping rule in ",[626,3537,1762],{"href":1761},[618,3539,3540,3541,3543,3544,3547,3548,3550,3551,3553,3554,661],{},"There's a pages Cache Rule, and optionally the API rule (see ",[626,3542,1615],{"href":1614},"), with the tested expressions for your host (the API rule excludes ",[740,3545,3546],{},"\u002F_api\u002F_\u002Fcomponent_positions\u002F","), each with ",[621,3549,2019],{},", Edge TTL ",[621,3552,2061],{},", Browser TTL ",[621,3555,2071],{},[618,3557,3558,3559,2617,3562,3564,3565,2095,3567,3569,3570,3572],{},"An edit in the admin turns that page's ",[740,3560,3561],{},"cf-cache-status",[740,3563,1158],{}," to ",[740,3566,1154],{},[740,3568,2508],{},", and the php log has no ",[740,3571,2512],{}," errors.",[618,3574,3575,3576,3579,3580,3583,3584,3587,3588,3590],{},"After a deploy, the deploy log says ",[740,3577,3578],{},"Purging rendered HTML, then everything at Cloudflare (the new API pod's cache is already this build's)..."," then ",[740,3581,3582],{},"Purged everything in the Cloudflare zone.",", with no ",[740,3585,3586],{},"The edge-only purge failed"," line, and pages (and, with the API rule, API responses) are ",[740,3589,1154],{}," at the edge and become interactive.",[618,3592,3593],{},"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.",[618,3595,3596,3597,3489,3599,661],{},"Live updates still arrive, and ",[740,3598,2147],{},[740,3600,2151],{},[618,3602,3603,3605,3606,3608,3609,3611],{},[621,3604,2708],{}," in site settings turns a ",[740,3607,1158],{}," page into ",[740,3610,1154],{}," at the edge.",[618,3613,3614,3615,3617,3618,861,3620,661],{},"The plan's purge rate limit suits how often editors save, across every site on the Cloudflare account and including staging: 5 purge requests a minute on Free. ",[740,3616,2925],{}," matches the plan, and sites or environments sharing the account each have a lower ",[740,3619,2986],{},[740,3621,3622],{},"CLOUDFLARE_PURGE_BURST",[3624,3625,3626],"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);}html pre.shiki code .stmX-, html code.shiki .stmX-{--shiki-light:#D73A49;--shiki-default:#F97583;--shiki-dark:#F78C6C}html pre.shiki code .sc2zw, html code.shiki .sc2zw{--shiki-light:#005CC5;--shiki-default:#79B8FF;--shiki-dark:#BABED8}html pre.shiki code .sBtbT, html code.shiki .sBtbT{--shiki-light:#005CC5;--shiki-default:#79B8FF;--shiki-dark:#89DDFF}html pre.shiki code .sOvfz, html code.shiki .sOvfz{--shiki-light:#24292E;--shiki-default:#E1E4E8;--shiki-dark:#89DDFF}html pre.shiki code .swB56, html code.shiki .swB56{--shiki-light:#D73A49;--shiki-default:#F97583;--shiki-dark:#C792EA}html pre.shiki code .s3Ny6, html code.shiki .s3Ny6{--shiki-light:#005CC5;--shiki-default:#79B8FF;--shiki-dark:#82AAFF}html pre.shiki code .sbW4m, html code.shiki .sbW4m{--shiki-light:#005CC5;--shiki-default:#79B8FF;--shiki-dark:#FFCB6B}html pre.shiki code .sPB8G, html code.shiki .sPB8G{--shiki-light:#24292E;--shiki-default:#E1E4E8;--shiki-dark:#BABED8}html pre.shiki code .sKpYG, html code.shiki .sKpYG{--shiki-light:#6F42C1;--shiki-default:#B392F0;--shiki-dark:#82AAFF}html pre.shiki code .sm4w6, html code.shiki .sm4w6{--shiki-light:#D73A49;--shiki-light-font-style:inherit;--shiki-default:#F97583;--shiki-default-font-style:inherit;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}",{"title":1061,"searchDepth":1106,"depth":1106,"links":3628},[3629,3636,3650,3651],{"id":665,"depth":1106,"text":666,"children":3630},[3631,3632,3633,3634,3635],{"id":670,"depth":1335,"text":671},{"id":734,"depth":1335,"text":735},{"id":914,"depth":1335,"text":915},{"id":1180,"depth":1335,"text":1181},{"id":1526,"depth":1335,"text":1527},{"id":1683,"depth":1106,"text":942,"children":3637},[3638,3639,3640,3641,3642,3644,3645,3646,3647,3648,3649],{"id":1686,"depth":1335,"text":1687},{"id":1731,"depth":1335,"text":1732},{"id":1795,"depth":1335,"text":1762},{"id":2009,"depth":1335,"text":1773},{"id":2244,"depth":1335,"text":3643},"\u002F_api needs its own rule",{"id":2283,"depth":1335,"text":1615},{"id":2369,"depth":1335,"text":2370},{"id":2405,"depth":1335,"text":1783},{"id":2516,"depth":1335,"text":2365},{"id":2905,"depth":1335,"text":647},{"id":3073,"depth":1335,"text":1728},{"id":3231,"depth":1106,"text":660},{"id":3379,"depth":1106,"text":3380},"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",{},{"title":573,"description":3652},"FUpleIztympFj2C1ZZi0i4aH7b6T3oraRG690mmN7kE",[3658,3660],{"title":569,"path":570,"stem":571,"description":3659,"children":-1},"Every CI\u002FCD variable the template's pipeline reads, grouped by concern, with its default and the Helm value it sets. Starts with the few a new site must set.",{"title":577,"path":578,"stem":579,"description":3661,"children":-1},"How the template keeps made-up URLs and request floods from getting past the cache - the query allowlist, tracking parameters, the rate limit on cache misses, ingress rate limits and load shedding.",1793565692969]