diff --git a/website/pages/Page.cacheable.test.tsx b/website/pages/Page.cacheable.test.tsx new file mode 100644 index 000000000..2c10fca06 --- /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: () => Promise.resolve({}) 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) =>