Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 66 additions & 50 deletions vnda/loaders/productListingPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import {
getSEOFromTag,
toFilters,
toProduct,
typeTagExtractor,
} from "../utils/transform.ts";

export const VNDA_SORT_OPTIONS: SortOption[] = [
Expand Down Expand Up @@ -87,6 +86,42 @@ const handleOperator = (
[`${key}_operator`]: filterOperators?.[key] ?? defaultValue ?? "and",
});

const fetchTag = (
api: AppContext["api"],
name: string,
): Promise<Tag | undefined> =>
api["GET /api/v2/tags/:name"]({ name }, STALE)
.then((res) => res.json() as Promise<Tag>)
.catch((): undefined => undefined);

interface TypeTag {
key: string;
value: string;
isProperty: boolean;
}

const parseTypeTagsFromUrl = (url: URL): { typeTags: TypeTag[]; cleanUrl: URL } => {
const TYPE_TAG_PATTERN = /^type_tags\[(.+)\]\[\]$/;

Comment on lines +103 to +105

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

deno fmt --check is failing for this file.

CI is red due to formatting diffs in these changed ranges. Please run formatter before merge.

Also applies to: 153-157, 161-167, 333-333

🧰 Tools
🪛 GitHub Actions: ci

[error] 103-167: deno fmt --check failed for this file. Deno reported formatting diffs near lines 103-105, 153-157, and 161-167 (e.g., function signature/arguments and split .split("/" )/filter formatting).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vnda/loaders/productListingPage.ts` around lines 103 - 105, CI is failing due
to formatting issues in this file (e.g., the parseTypeTagsFromUrl function block
and other nearby ranges); run the code formatter and apply its changes—execute
`deno fmt` (or your project's formatter) on vnda/loaders/productListingPage.ts
and commit the resulting formatting changes so the parseTypeTagsFromUrl function
signature, surrounding blocks, and the other affected ranges are formatted
correctly.

const typeTags = [...url.searchParams.entries()]
.filter(([key]) => TYPE_TAG_PATTERN.test(key))
.map(([key, value]) => {
const keyName = key.match(TYPE_TAG_PATTERN)?.[1] ?? "";
return {
key,
value,
isProperty: /^property\d+$/.test(keyName),
};
});

const cleanUrl = new URL(url.href);

@cubic-dev-ai cubic-dev-ai Bot Mar 26, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: cleanUrl only strips type_tags params but retains page. When passed to toFilters(...), the generated filter toggle URLs will carry a stale page number, so toggling a filter may land the user on page N instead of page 1. Consider also deleting page from cleanUrl.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At vnda/loaders/productListingPage.ts, line 117:

<comment>`cleanUrl` only strips `type_tags` params but retains `page`. When passed to `toFilters(...)`, the generated filter toggle URLs will carry a stale page number, so toggling a filter may land the user on page N instead of page 1. Consider also deleting `page` from `cleanUrl`.</comment>

<file context>
@@ -87,6 +86,42 @@ const handleOperator = (
+      };
+    });
+
+  const cleanUrl = new URL(url.href);
+  [...cleanUrl.searchParams.keys()]
+    .filter((k) => k.startsWith("type_tags"))
</file context>
Fix with Cubic

[...cleanUrl.searchParams.keys()]
.filter((k) => k.startsWith("type_tags"))
.forEach((k) => cleanUrl.searchParams.delete(k));

return { typeTags, cleanUrl };
};
Comment on lines +117 to +123

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Reset page in cleanUrl to preserve filter navigation behavior.

cleanUrl is passed to toFilters(...), but this parser no longer removes page. That can keep users on stale page numbers after toggling filters.

Suggested fix
 const cleanUrl = new URL(url.href);
 [...cleanUrl.searchParams.keys()]
   .filter((k) => k.startsWith("type_tags"))
   .forEach((k) => cleanUrl.searchParams.delete(k));
+cleanUrl.searchParams.delete("page");

 return { typeTags, cleanUrl };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const cleanUrl = new URL(url.href);
[...cleanUrl.searchParams.keys()]
.filter((k) => k.startsWith("type_tags"))
.forEach((k) => cleanUrl.searchParams.delete(k));
return { typeTags, cleanUrl };
};
const cleanUrl = new URL(url.href);
[...cleanUrl.searchParams.keys()]
.filter((k) => k.startsWith("type_tags"))
.forEach((k) => cleanUrl.searchParams.delete(k));
cleanUrl.searchParams.delete("page");
return { typeTags, cleanUrl };
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vnda/loaders/productListingPage.ts` around lines 117 - 123, cleanUrl
currently retains the page query param which causes stale pagination after
filter changes; in the function that builds cleanUrl (the block creating new
URL(url.href) and before returning { typeTags, cleanUrl }), remove the "page"
search param (e.g., cleanUrl.searchParams.delete("page")) so that the cleanUrl
passed into toFilters(...) has pagination reset and filter navigation behaves
correctly.


/**
* @title VNDA Integration
* @description Product Listing Page loader
Expand All @@ -108,44 +143,40 @@ const searchLoader = async (
const isSearchPage = ctx.searchPagePath
? ctx.searchPagePath === url.pathname
: url.pathname === "/busca" || url.pathname === "/s";

const qQueryString = url.searchParams.get("q");
const term = props.term || props.slug || qQueryString ||
undefined;
const term = props.term || props.slug || qQueryString || undefined;

const priceFilterRegex = /de-(\d+)-a-(\d+)/;
const filterMatch = url.href.match(priceFilterRegex) ?? [];

const categoryTagName = (props.term || url.pathname.slice(1) || "").split(
"/",
);
const categoryTagName = (props.term || url.pathname.slice(1) || "").split("/");

const properties1 = url.searchParams.getAll("type_tags[property1][]");
const properties2 = url.searchParams.getAll("type_tags[property2][]");
const properties3 = url.searchParams.getAll("type_tags[property3][]");

const categoryTagNames = Array.from(url.searchParams.values());
const uniquePathNames = [
...new Set(
categoryTagName.filter((item): item is string => typeof item === "string"),

@cubic-dev-ai cubic-dev-ai Bot Mar 26, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The filter typeof item === "string" does not exclude empty strings. A trailing slash in the pathname (e.g., /feminino/) produces an empty segment that passes through and triggers a wasted fetchTag(api, "") call. Filter with item.length > 0 as well.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At vnda/loaders/productListingPage.ts, line 161:

<comment>The filter `typeof item === "string"` does not exclude empty strings. A trailing slash in the pathname (e.g., `/feminino/`) produces an empty segment that passes through and triggers a wasted `fetchTag(api, "")` call. Filter with `item.length > 0` as well.</comment>

<file context>
@@ -108,44 +143,40 @@ const searchLoader = async (
-  const categoryTagNames = Array.from(url.searchParams.values());
+  const uniquePathNames = [
+    ...new Set(
+      categoryTagName.filter((item): item is string => typeof item === "string"),
+    ),
+  ];
</file context>
Suggested change
categoryTagName.filter((item): item is string => typeof item === "string"),
categoryTagName.filter((item): item is string => typeof item === "string" && item.length > 0),
Fix with Cubic

),
];
Comment on lines +153 to +163

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Use slug/path for category tag lookup, not props.term, and drop empty segments.

This segment currently derives tag names from props.term, which can be a free-text search term and trigger unnecessary /api/v2/tags/:name calls. Also, empty segments are not filtered before fetch.

Suggested fix
-const categoryTagName = (props.term || url.pathname.slice(1) || "").split("/");
+const categoryTagName = (props.slug || url.pathname.slice(1) || "").split("/");

 const uniquePathNames = [
   ...new Set(
-    categoryTagName.filter((item): item is string => typeof item === "string"),
+    categoryTagName.filter(
+      (item): item is string => typeof item === "string" && item.trim().length > 0,
+    ),
   ),
 ];
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const categoryTagName = (props.term || url.pathname.slice(1) || "").split("/");
const properties1 = url.searchParams.getAll("type_tags[property1][]");
const properties2 = url.searchParams.getAll("type_tags[property2][]");
const properties3 = url.searchParams.getAll("type_tags[property3][]");
const categoryTagNames = Array.from(url.searchParams.values());
const uniquePathNames = [
...new Set(
categoryTagName.filter((item): item is string => typeof item === "string"),
),
];
const categoryTagName = (props.slug || url.pathname.slice(1) || "").split("/");
const properties1 = url.searchParams.getAll("type_tags[property1][]");
const properties2 = url.searchParams.getAll("type_tags[property2][]");
const properties3 = url.searchParams.getAll("type_tags[property3][]");
const uniquePathNames = [
...new Set(
categoryTagName.filter(
(item): item is string => typeof item === "string" && item.trim().length > 0,
),
),
];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vnda/loaders/productListingPage.ts` around lines 153 - 163, The code
currently builds categoryTagName from props.term (a free-text search) and
url.pathname, causing incorrect tag lookups and unnecessary API calls; change
the logic that computes categoryTagName/uniquePathNames to use only the URL path
segments (e.g., url.pathname.slice(1).split("/")), remove any empty segments
(filter(Boolean) or item => item !== ""), and use those filtered segments
(optionally decodeURIComponent) when calling the tags endpoint so lookups use
the slug/path rather than props.term; update occurrences of categoryTagName and
uniquePathNames to reflect this filtered-path-only source.


const tags = await Promise.all([
...categoryTagNames,
...categoryTagName.filter((item): item is string =>
typeof item === "string"
const tagByName = new Map<string, Tag | undefined>(
await Promise.all(
uniquePathNames.map(
async (name) => [name, await fetchTag(api, name)] as const,
),
),
].map((name) =>
api["GET /api/v2/tags/:name"]({ name }, STALE)
.then((res) => res.json())
.catch(() => undefined)
));

const categories = tags
.slice(-categoryTagName.length)
);

const categories = categoryTagName
.map((name) => tagByName.get(name))
.filter((tag): tag is Tag =>
typeof tag !== "undefined" && typeof tag.name !== "undefined"
);

const filteredTags = tags
.filter((tag): tag is Tag => typeof tag !== "undefined");

const { cleanUrl, typeTags } = typeTagExtractor(url, filteredTags);
const { typeTags, cleanUrl } = parseTypeTagsFromUrl(url);

const initialTags = props.tags && props.tags?.length > 0
? props.tags
Expand All @@ -165,7 +196,7 @@ const searchLoader = async (
const tag = categories.at(-1);

const [response, seo = []] = await Promise.all([
await api["GET /api/v2/products/search"]({
api["GET /api/v2/products/search"]({
term: term ?? preference,
sort,
page,
Expand Down Expand Up @@ -211,19 +242,15 @@ const searchLoader = async (
) as ProductSearchResult["pagination"] | null;

const search = await response.json();

const { results: searchResults = [] } = search;

const validProducts = searchResults.filter(({ variants }) => {
return variants.length !== 0;
});
const validProducts = searchResults.filter(({ variants }) =>
variants.length !== 0
);

const products = validProducts.map((product) => {
return toProduct(product, null, {
url,
priceCurrency: "BRL",
});
});
const products = validProducts.map((product) =>
toProduct(product, null, { url, priceCurrency: "BRL" })
);

const nextPage = new URLSearchParams(url.searchParams);
const previousPage = new URLSearchParams(url.searchParams);
Expand All @@ -247,11 +274,7 @@ const searchLoader = async (
"@type": "ProductListingPage",
seo: getSEOFromTag(categories, url, seo.at(-1), hasTypeTags, isSearchPage),
breadcrumb: isSearchPage
? {
"@type": "BreadcrumbList",
itemListElement: [],
numberOfItems: 0,
}
? { "@type": "BreadcrumbList", itemListElement: [], numberOfItems: 0 }
: getBreadcrumbList(categories, url),
filters: toFilters(search.aggregations, typeTags, cleanUrl),
products,
Expand All @@ -270,12 +293,9 @@ export const cache = "stale-while-revalidate";
export const cacheKey = (props: Props, req: Request, _ctx: AppContext) => {
const url = new URL(props.pageHref || req.url);
const qQueryString = url.searchParams.get("q");
const term = props.term || qQueryString ||
undefined;
const term = props.term || qQueryString || undefined;

if (term) {
return null;
}
if (term) return null;

const typeTags = [...url.searchParams.entries()]
.filter(([key]) => key.includes("type_tags"))
Expand All @@ -299,19 +319,15 @@ export const cacheKey = (props: Props, req: Request, _ctx: AppContext) => {
["sort", url.searchParams.get("sort") ?? props.sort ?? ""],
["type_tags", typeTags],
["tags", props?.tags?.join("\\") ?? ""],
[
"price",
filterMatch ? `min:${filterMatch[1]}_max:${filterMatch[2]}` : "",
],
["price", filterMatch ? `min:${filterMatch[1]}_max:${filterMatch[2]}` : ""],
["filterByTags", props.filterByTags ? "true" : "false"],
["filterOperator", filterOperators.join("\\")],
["page", (url.searchParams.get("page") ?? 1).toString()],
]);

params.sort();

url.search = params.toString();
return url.href;
};

export default searchLoader;
export default searchLoader;
Loading