From 509dc733c37d0782193d5d2876157327bfc42362 Mon Sep 17 00:00:00 2001 From: guitavano Date: Wed, 26 Nov 2025 10:56:35 -0300 Subject: [PATCH 1/5] feat: enhance router to skip catch-all routes for paths with file extensions * Added a utility function to check for file extensions in the pathname. * Updated the router logic to skip catch-all routes when a file extension is detected. --- website/handlers/router.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/website/handlers/router.ts b/website/handlers/router.ts index 95c3f6b10..fa2865cea 100644 --- a/website/handlers/router.ts +++ b/website/handlers/router.ts @@ -38,6 +38,10 @@ const rankRoute = (pattern: string): number => return acc + 3; }, 0); const urlPatternCache: Record = {}; +const hasFileExtension = (pathname: string): boolean => { + const lastSegment = pathname.split("/").pop() || ""; + return lastSegment.includes(".") && !lastSegment.startsWith("."); +}; export const router = ( routes: Route[], hrefRoutes: Record> = {}, @@ -78,6 +82,10 @@ export const router = ( return route(handler, `${url.pathname}${url.search || ""}`); } for (const { pathTemplate: routePath, handler } of routes) { + // Skip catch-all routes for paths with file extensions + if (routePath.endsWith("*") && hasFileExtension(url.pathname)) { + continue; + } const pattern = urlPatternCache[routePath] ??= (() => { let url; if (URL.canParse(routePath)) { From 1312d98dc2bee062170608b160e0f7f775850da7 Mon Sep 17 00:00:00 2001 From: guitavano Date: Wed, 26 Nov 2025 15:30:53 -0300 Subject: [PATCH 2/5] add supportedExtensions --- website/handlers/fresh.ts | 20 ++++++++++++++++++++ website/handlers/router.ts | 8 -------- website/loaders/pages.ts | 11 +++++++++-- website/mod.ts | 6 ++++-- 4 files changed, 33 insertions(+), 12 deletions(-) diff --git a/website/handlers/fresh.ts b/website/handlers/fresh.ts index 7e61896a8..6310db3a7 100644 --- a/website/handlers/fresh.ts +++ b/website/handlers/fresh.ts @@ -20,6 +20,11 @@ type ConnInfo = Deno.ServeHandlerInfo; */ export interface FreshConfig { page: Page; + /** + * @title Supported extensions + * @description Extensions that will be supported by the page. + */ + supportedExtensions?: string[]; } export const isFreshCtx = ( ctx: ConnInfo | HandlerContext, @@ -109,6 +114,21 @@ export default function Fresh( const timing = appContext?.monitoring?.timings?.start?.("load-data"); let didFinish = false; const url = new URL(req.url); + + const lastPathname = url.pathname.split("/").pop(); + const extension = lastPathname?.includes(".") ? lastPathname?.split(".").pop() : undefined; + + // Only check if there's an extension + if (extension) { + const supportedExtensions = freshConfig.supportedExtensions ?? []; + + // If no extensions are configured, block all extensions (default behavior) + // If extensions are configured, only allow those + if (!supportedExtensions.includes(extension)) { + return new Response(null, { status: 404 }); + } + } + const startedAt = Date.now(); const asJson = url.searchParams.get("asJson"); const delayFromProps = appContext.firstByteThresholdMS ? 1 : 0; diff --git a/website/handlers/router.ts b/website/handlers/router.ts index fa2865cea..95c3f6b10 100644 --- a/website/handlers/router.ts +++ b/website/handlers/router.ts @@ -38,10 +38,6 @@ const rankRoute = (pattern: string): number => return acc + 3; }, 0); const urlPatternCache: Record = {}; -const hasFileExtension = (pathname: string): boolean => { - const lastSegment = pathname.split("/").pop() || ""; - return lastSegment.includes(".") && !lastSegment.startsWith("."); -}; export const router = ( routes: Route[], hrefRoutes: Record> = {}, @@ -82,10 +78,6 @@ export const router = ( return route(handler, `${url.pathname}${url.search || ""}`); } for (const { pathTemplate: routePath, handler } of routes) { - // Skip catch-all routes for paths with file extensions - if (routePath.endsWith("*") && hasFileExtension(url.pathname)) { - continue; - } const pattern = urlPatternCache[routePath] ??= (() => { let url; if (URL.canParse(routePath)) { diff --git a/website/loaders/pages.ts b/website/loaders/pages.ts index 347fff625..063f6e329 100644 --- a/website/loaders/pages.ts +++ b/website/loaders/pages.ts @@ -3,7 +3,7 @@ import { Route } from "../flags/audience.ts"; import { AppContext } from "../mod.ts"; import Page from "../pages/Page.tsx"; -async function getAllPages(ctx: AppContext): Promise { +async function getAllPages(ctx: AppContext, props: Props): Promise { const allPages = await ctx.get< Record[0]> >({ @@ -27,6 +27,7 @@ async function getAllPages(ctx: AppContext): Promise { page: { __resolveType: pageId, }, + supportedExtensions: props.supportedExtensions, }, }, }); @@ -45,6 +46,12 @@ export interface Props { * @description Deco routes that will ignore the previous rule. If the same route exists on other routes loader, the deco page will be used. */ alwaysVisiblePages?: SiteRoute[]; + + /** + * @title Supported extensions + * @description Extensions that will be supported by pages routes. + */ + supportedExtensions?: string[]; } /** @@ -58,7 +65,7 @@ export default async function Pages( const allPages = await ctx.get< Route[] >({ - func: () => getAllPages(ctx), + func: () => getAllPages(ctx, props), __resolveType: "once", }); diff --git a/website/mod.ts b/website/mod.ts index db07a2783..825709fff 100644 --- a/website/mod.ts +++ b/website/mod.ts @@ -12,7 +12,6 @@ import { type FnContext, type Site, } from "@deco/deco"; -import { VTEX_PATHS_THAT_REQUIRES_SAME_REFERER } from "../vtex/loaders/proxy.ts"; declare global { interface Window { @@ -77,6 +76,7 @@ export interface AbTesting { includeScriptsToBody?: { includes?: Script[]; }; + pathsThatRequireSameReferer?: string[]; } /** @titleBy framework */ interface Fresh { @@ -209,7 +209,9 @@ const getAbTestAudience = (abTesting: AbTesting) => { includeScriptsToHead: abTesting.includeScriptsToHead, includeScriptsToBody: abTesting.includeScriptsToBody, replaces: abTesting.replaces, - pathsThatRequireSameReferer: [...VTEX_PATHS_THAT_REQUIRES_SAME_REFERER], + pathsThatRequireSameReferer: [ + ...(abTesting.pathsThatRequireSameReferer ?? []), + ], }, }; if (abTesting.enabled) { From 92a62bfbcc49834c70ff0dd2e81762d6f08fb5d8 Mon Sep 17 00:00:00 2001 From: guitavano Date: Wed, 26 Nov 2025 17:14:13 -0300 Subject: [PATCH 3/5] change tto router.ts --- website/flags/audience.ts | 1 + website/handlers/fresh.ts | 19 ------------------- website/handlers/router.ts | 16 +++++++++++++--- website/loaders/pages.ts | 2 +- 4 files changed, 15 insertions(+), 23 deletions(-) diff --git a/website/flags/audience.ts b/website/flags/audience.ts index 8e1ac9774..2d921c224 100644 --- a/website/flags/audience.ts +++ b/website/flags/audience.ts @@ -22,6 +22,7 @@ export interface Route { * @description higher priority means that this route will be used in favor of other routes with less or none priority */ highPriority?: boolean; + supportedExtensions?: string[]; } /** * @title Routes diff --git a/website/handlers/fresh.ts b/website/handlers/fresh.ts index 6310db3a7..210c6fa3a 100644 --- a/website/handlers/fresh.ts +++ b/website/handlers/fresh.ts @@ -20,11 +20,6 @@ type ConnInfo = Deno.ServeHandlerInfo; */ export interface FreshConfig { page: Page; - /** - * @title Supported extensions - * @description Extensions that will be supported by the page. - */ - supportedExtensions?: string[]; } export const isFreshCtx = ( ctx: ConnInfo | HandlerContext, @@ -115,20 +110,6 @@ export default function Fresh( let didFinish = false; const url = new URL(req.url); - const lastPathname = url.pathname.split("/").pop(); - const extension = lastPathname?.includes(".") ? lastPathname?.split(".").pop() : undefined; - - // Only check if there's an extension - if (extension) { - const supportedExtensions = freshConfig.supportedExtensions ?? []; - - // If no extensions are configured, block all extensions (default behavior) - // If extensions are configured, only allow those - if (!supportedExtensions.includes(extension)) { - return new Response(null, { status: 404 }); - } - } - const startedAt = Date.now(); const asJson = url.searchParams.get("asJson"); const delayFromProps = appContext.firstByteThresholdMS ? 1 : 0; diff --git a/website/handlers/router.ts b/website/handlers/router.ts index 95c3f6b10..2ca60af92 100644 --- a/website/handlers/router.ts +++ b/website/handlers/router.ts @@ -19,6 +19,7 @@ export interface SelectionConfig { } interface MaybePriorityHandler { func: Resolvable; + supportedExtensions?: string[]; highPriority?: boolean; } const HIGH_PRIORITY_ROUTE_RANK_BASE_VALUE = 1000; @@ -38,6 +39,10 @@ const rankRoute = (pattern: string): number => return acc + 3; }, 0); const urlPatternCache: Record = {}; +const hasFileExtension = (pathname: string): boolean => { + const lastSegment = pathname.split("/").pop() || ""; + return lastSegment.includes(".") && !lastSegment.startsWith("."); +}; export const router = ( routes: Route[], hrefRoutes: Record> = {}, @@ -77,7 +82,11 @@ export const router = ( if (handler) { return route(handler, `${url.pathname}${url.search || ""}`); } - for (const { pathTemplate: routePath, handler } of routes) { + for (const { pathTemplate: routePath, handler, supportedExtensions } of routes) { + // Skip catch-all routes for paths with file extensions + if (hasFileExtension(url.pathname) && !supportedExtensions?.includes(url.pathname.split(".").pop() || "")) { + continue; + } const pattern = urlPatternCache[routePath] ??= (() => { let url; if (URL.canParse(routePath)) { @@ -110,12 +119,12 @@ export const buildRoutes = (audiences: Routes[]): [ // We should tackle this problem elsewhere // check if the audience matches with the given context considering the `isMatch` provided by the cookies. for (const audience of audiences.filter(Boolean).flat()) { - const { pathTemplate, isHref, highPriority, handler: { value: handler } } = + const { pathTemplate, isHref, highPriority, handler: { value: handler }, supportedExtensions } = audience; if (isHref) { hrefRoutes[pathTemplate] = handler; } else { - routeMap[pathTemplate] = { func: handler, highPriority }; + routeMap[pathTemplate] = { func: handler, highPriority, supportedExtensions }; } } return [routeMap, hrefRoutes]; @@ -155,6 +164,7 @@ const prepareRoutes = (audiences: Routes[], ctx: AppContext) => { routes: builtRoutes.map((route) => ({ pathTemplate: route[0], handler: { value: route[1].func }, + supportedExtensions: route[1].supportedExtensions, })), hrefRoutes, }; diff --git a/website/loaders/pages.ts b/website/loaders/pages.ts index 063f6e329..e416b0737 100644 --- a/website/loaders/pages.ts +++ b/website/loaders/pages.ts @@ -21,13 +21,13 @@ async function getAllPages(ctx: AppContext, props: Props): Promise { } routes.push({ pathTemplate, + supportedExtensions: props.supportedExtensions, handler: { value: { __resolveType: "website/handlers/fresh.ts", page: { __resolveType: pageId, }, - supportedExtensions: props.supportedExtensions, }, }, }); From a5ef9065cb29046888f4f5dafbf8a3dfe206407d Mon Sep 17 00:00:00 2001 From: guitavano Date: Wed, 26 Nov 2025 17:14:25 -0300 Subject: [PATCH 4/5] fmt --- website/handlers/fresh.ts | 2 +- website/handlers/router.ts | 24 +++++++++++++++++++----- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/website/handlers/fresh.ts b/website/handlers/fresh.ts index 210c6fa3a..69f30312f 100644 --- a/website/handlers/fresh.ts +++ b/website/handlers/fresh.ts @@ -109,7 +109,7 @@ export default function Fresh( const timing = appContext?.monitoring?.timings?.start?.("load-data"); let didFinish = false; const url = new URL(req.url); - + const startedAt = Date.now(); const asJson = url.searchParams.get("asJson"); const delayFromProps = appContext.firstByteThresholdMS ? 1 : 0; diff --git a/website/handlers/router.ts b/website/handlers/router.ts index 2ca60af92..0e0677b48 100644 --- a/website/handlers/router.ts +++ b/website/handlers/router.ts @@ -82,9 +82,14 @@ export const router = ( if (handler) { return route(handler, `${url.pathname}${url.search || ""}`); } - for (const { pathTemplate: routePath, handler, supportedExtensions } of routes) { + for ( + const { pathTemplate: routePath, handler, supportedExtensions } of routes + ) { // Skip catch-all routes for paths with file extensions - if (hasFileExtension(url.pathname) && !supportedExtensions?.includes(url.pathname.split(".").pop() || "")) { + if ( + hasFileExtension(url.pathname) && + !supportedExtensions?.includes(url.pathname.split(".").pop() || "") + ) { continue; } const pattern = urlPatternCache[routePath] ??= (() => { @@ -119,12 +124,21 @@ export const buildRoutes = (audiences: Routes[]): [ // We should tackle this problem elsewhere // check if the audience matches with the given context considering the `isMatch` provided by the cookies. for (const audience of audiences.filter(Boolean).flat()) { - const { pathTemplate, isHref, highPriority, handler: { value: handler }, supportedExtensions } = - audience; + const { + pathTemplate, + isHref, + highPriority, + handler: { value: handler }, + supportedExtensions, + } = audience; if (isHref) { hrefRoutes[pathTemplate] = handler; } else { - routeMap[pathTemplate] = { func: handler, highPriority, supportedExtensions }; + routeMap[pathTemplate] = { + func: handler, + highPriority, + supportedExtensions, + }; } } return [routeMap, hrefRoutes]; From a77479e6c7c8ce326604786a47b1798f030960e4 Mon Sep 17 00:00:00 2001 From: guitavano Date: Wed, 26 Nov 2025 17:28:15 -0300 Subject: [PATCH 5/5] use extname from std --- website/handlers/router.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/website/handlers/router.ts b/website/handlers/router.ts index 0e0677b48..d8700d124 100644 --- a/website/handlers/router.ts +++ b/website/handlers/router.ts @@ -8,6 +8,7 @@ import { RouteContext, } from "@deco/deco"; import { isAwaitable } from "@deco/deco/utils"; +import { extname } from "@std/path/extname"; import { weakcache } from "../../utils/weakcache.ts"; import { Route, Routes } from "../flags/audience.ts"; import { isFreshCtx } from "../handlers/fresh.ts"; @@ -39,10 +40,7 @@ const rankRoute = (pattern: string): number => return acc + 3; }, 0); const urlPatternCache: Record = {}; -const hasFileExtension = (pathname: string): boolean => { - const lastSegment = pathname.split("/").pop() || ""; - return lastSegment.includes(".") && !lastSegment.startsWith("."); -}; + export const router = ( routes: Route[], hrefRoutes: Record> = {}, @@ -86,10 +84,8 @@ export const router = ( const { pathTemplate: routePath, handler, supportedExtensions } of routes ) { // Skip catch-all routes for paths with file extensions - if ( - hasFileExtension(url.pathname) && - !supportedExtensions?.includes(url.pathname.split(".").pop() || "") - ) { + const ext = extname(url.pathname).slice(1); + if (ext && !supportedExtensions?.includes(ext)) { continue; } const pattern = urlPatternCache[routePath] ??= (() => {