From 97502eb7c424557088664d07e9cc11b9e2ae0068 Mon Sep 17 00:00:00 2001 From: igoramf Date: Mon, 3 Aug 2026 15:01:12 -0300 Subject: [PATCH 1/2] feat(website): add `cacheable` prop to Page to opt CMS pages into CDN cache MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CMS-only pages (a home built purely from website sections, no commerce loader) never get PAGE_CACHE_ALLOWED_KEY set, so the runtime never emits a public Cache-Control and they stay uncached. Commerce apps (VTEX) set that key from their middleware, but an app middleware is a resolver middleware and only runs when a block from that app is resolved. Add an opt-in `cacheable` boolean to the Page block. When enabled, the Page loader sets PAGE_CACHE_ALLOWED_KEY (same API/mechanism as the VTEX middleware), routing the page through the existing, guarded caching pipeline. Safe by construction: the runtime still forces no-store on a foreign Set-Cookie, vary.shouldCache=false, or a non-cacheable matcher/flag, and never overrides a Cache-Control already set by another block. Default (unset/false) is fully backwards compatible — no page changes behavior unless explicitly toggled. Verified: Page.cacheable.test.tsx (loader sets the key iff cacheable=true) plus deco's runtime/middleware.test.ts (key ⇒ public, guards ⇒ no-store). Co-Authored-By: Claude Opus 4.8 --- website/pages/Page.cacheable.test.tsx | 43 +++++++++++++++++++++++++++ website/pages/Page.tsx | 18 ++++++++++- 2 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 website/pages/Page.cacheable.test.tsx diff --git a/website/pages/Page.cacheable.test.tsx b/website/pages/Page.cacheable.test.tsx new file mode 100644 index 000000000..e0daebed2 --- /dev/null +++ b/website/pages/Page.cacheable.test.tsx @@ -0,0 +1,43 @@ +import { assert, assertEquals } from "@std/assert"; +import { PAGE_CACHE_ALLOWED_KEY } from "@deco/deco/blocks"; +import { loader } from "./Page.tsx"; + +// Minimal fake AppContext exercising exactly what the loader touches. +const fakeCtx = () => + ({ + bag: new Map(), + global: [], + theme: undefined, + errorPage: undefined, + resolverId: "root", + // deno-lint-ignore no-explicit-any + get: async () => ({}) as any, + avoidRedirectingToEditor: undefined, + defaultImageQuality: undefined, + // deno-lint-ignore no-explicit-any + }) as any; + +const baseProps = { name: "home", sections: [] }; +const req = () => new Request("https://farm.example/"); + +Deno.test("cacheable=true opts the page into caching", async () => { + const ctx = fakeCtx(); + await loader({ ...baseProps, cacheable: true }, req(), ctx); + assert( + ctx.bag.has(PAGE_CACHE_ALLOWED_KEY), + "expected PAGE_CACHE_ALLOWED_KEY to be set", + ); + assertEquals(ctx.bag.get(PAGE_CACHE_ALLOWED_KEY), true); +}); + +Deno.test("cacheable=false does NOT opt in (default behavior preserved)", async () => { + const ctx = fakeCtx(); + await loader({ ...baseProps, cacheable: false }, req(), ctx); + assert(!ctx.bag.has(PAGE_CACHE_ALLOWED_KEY)); +}); + +Deno.test("cacheable omitted does NOT opt in", async () => { + const ctx = fakeCtx(); + await loader({ ...baseProps }, req(), ctx); + assert(!ctx.bag.has(PAGE_CACHE_ALLOWED_KEY)); +}); diff --git a/website/pages/Page.tsx b/website/pages/Page.tsx index c142c28d9..320a9ae53 100644 --- a/website/pages/Page.tsx +++ b/website/pages/Page.tsx @@ -11,6 +11,7 @@ import { type ComponentFunc, type ComponentMetadata, type Page, + PAGE_CACHE_ALLOWED_KEY, type Section, } from "@deco/deco/blocks"; import { logger } from "@deco/deco/o11y"; @@ -51,6 +52,15 @@ export interface Props { /** @hide true */ seo?: Section; sections: Sections; + /** + * @title Allow CDN caching + * @description Opt this page into CDN page caching. Only enable for public, + * non-personalized pages: the response is still forced to `no-store` if a + * loader sets a cookie, vetoes caching (cache "no-store" / null cache key), + * or a non-cacheable matcher/flag is active. Leave off for anything that + * renders per-user content without one of those signals. + */ + cacheable?: boolean; /** @hide true */ unindexedDomain?: boolean; } @@ -155,10 +165,16 @@ function Page( } export const loader = async ( - { sections, ...restProps }: Props, + { sections, cacheable, ...restProps }: Props, req: Request, ctx: AppContext, ) => { + // Opt the page into CDN caching. Same mechanism the VTEX middleware uses; + // the runtime still applies the no-store guards (set-cookie, vary, flags). + if (cacheable) { + ctx.bag?.set(PAGE_CACHE_ALLOWED_KEY, true); + } + const url = new URL(req.url); const devMode = url.searchParams.has("__d"); const unindexedDomain = noIndexedDomains.some((domain) => From e67634d4eabf7e2eb8b9c7e1bd70207c789058ce Mon Sep 17 00:00:00 2001 From: igoramf Date: Tue, 4 Aug 2026 15:50:48 -0300 Subject: [PATCH 2/2] test(website): drop unnecessary async in Page.cacheable mock (require-await) Co-Authored-By: Claude Opus 4.8 --- website/pages/Page.cacheable.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/pages/Page.cacheable.test.tsx b/website/pages/Page.cacheable.test.tsx index e0daebed2..2c10fca06 100644 --- a/website/pages/Page.cacheable.test.tsx +++ b/website/pages/Page.cacheable.test.tsx @@ -11,7 +11,7 @@ const fakeCtx = () => errorPage: undefined, resolverId: "root", // deno-lint-ignore no-explicit-any - get: async () => ({}) as any, + get: () => Promise.resolve({}) as any, avoidRedirectingToEditor: undefined, defaultImageQuality: undefined, // deno-lint-ignore no-explicit-any