Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
5e586d0
feat: internationalization support for shopify loaders and queries
yuriassuncx Sep 10, 2025
5cc3c6d
chore: files generated
yuriassuncx Sep 10, 2025
ce35c85
feat: exclude prop added to sitemap
yuriassuncx Sep 10, 2025
f0178dd
fix: new improvements suggested by coderrabit
yuriassuncx Sep 30, 2025
0ae284a
refactor: improve code readability and maintainability across multipl…
yuriassuncx Sep 30, 2025
0220c31
fix: remove unnecessary countryCode variable from cart loader
yuriassuncx Oct 30, 2025
1110e3a
Merge branch 'main' into feat-shopify/inContext
yuriassuncx Nov 19, 2025
5f8bb0b
chore: remove unused Product import from loaders
yuriassuncx Nov 19, 2025
fff74a8
feat: update Cart and GetCart types to include buyerIdentity field
yuriassuncx Nov 19, 2025
879386d
Merge branch 'deco-cx:main' into feat-shopify/inContext
yuriassuncx Feb 26, 2026
23ac61e
feat: update Shopify API version to 2026-04 in GraphQL client endpoints
yuriassuncx Feb 26, 2026
7b3310f
feat: update cache strategy and enhance cacheKey with language and co…
yuriassuncx Feb 26, 2026
07475f5
feat: enhance ProductList loader with improved error handling and log…
yuriassuncx Feb 26, 2026
d69fb4f
feat: update ProductDetailsPage loader to use slug directly and impro…
yuriassuncx Feb 26, 2026
079e1a6
feat: format GraphQL query variables for better readability in Produc…
yuriassuncx Feb 26, 2026
6b4356f
feat: improve error logging in fetchSafe function to include response…
yuriassuncx Feb 26, 2026
1dca706
feat: improve error handling in fetchSafe by logging response text on…
yuriassuncx Feb 26, 2026
614bc45
fix: ensure newline at end of file in fetch.ts
yuriassuncx Feb 26, 2026
dfb7401
feat: enhance CreateCart mutation to include languageCode and country…
yuriassuncx Feb 26, 2026
22a54d4
feat: simplify CreateCart mutation by removing unnecessary variables …
yuriassuncx Feb 26, 2026
8261a27
fix: correct cart ID handling in loader and ensure proper encoding in…
yuriassuncx Feb 26, 2026
c9ff9c2
feat: enhance cache key generation by including metafields and sortin…
yuriassuncx Mar 2, 2026
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
137 changes: 85 additions & 52 deletions shopify/handlers/sitemap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,74 +3,107 @@ import { AppContext } from "../mod.ts";
import { withDigestCookie } from "../utils/password.ts";

type ConnInfo = Deno.ServeHandlerInfo;
const xmlHeader =

const XML_HEADER =
'<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">';

const includeSiteMaps = (
currentXML: string,
origin: string,
includes?: string[],
) => {
const siteMapIncludeTags = [];

for (const include of (includes ?? [])) {
siteMapIncludeTags.push(`
<sitemap>
<loc>${include.startsWith("/") ? `${origin}${include}` : include}</loc>
<lastmod>${new Date().toISOString().substring(0, 10)}</lastmod>
</sitemap>`);
}
return siteMapIncludeTags.length > 0
? currentXML.replace(
xmlHeader,
`${xmlHeader}\n${siteMapIncludeTags.join("\n")}`,
)
: currentXML;
};
// Helper function to get current date in YYYY-MM-DD format
const getToday = (): string => new Date().toISOString().substring(0, 10);

function buildIncludeSitemaps(origin: string, includes?: string[]) {
if (!includes?.length) return "";

const today = getToday();
const esc = (s: string) =>
s
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&apos;");

return includes
.map((include) => {
const loc = include.startsWith("/") ? `${origin}${include}` : include;
const safeLoc = esc(loc);
return ` <sitemap>\n <loc>${safeLoc}</loc>\n <lastmod>${today}</lastmod>\n </sitemap>`;
})
.join("\n");
}

function excludeSitemaps(xml: string, origin: string, excludes?: string[]) {
if (!excludes?.length) return xml;

// Ensure all exclude prefixes start with a slash
const normalized = excludes.map((ex) => (ex.startsWith("/") ? ex : `/${ex}`));

return xml.replace(
/<sitemap>\s*<loc>(.*?)<\/loc>[\s\S]*?<\/sitemap>/g,
(match, loc) => {
let locPath: string;
try {
// Use origin as base to support both absolute and relative URLs
const u = new URL(loc, origin);
locPath = u.pathname;
} catch {
// If URL parsing fails, leave the sitemap entry untouched
return match;
}

return normalized.some((ex) => locPath.startsWith(ex)) ? "" : match;
},
);
}

export interface Props {
include?: string[];
exclude?: string[];
}

/**
* @title Sitemap Proxy
*/
export default function Sitemap(
{ include }: Props,
{ include, exclude }: Props,
appCtx: AppContext,
) {
const url = `https://${appCtx.storeName}.myshopify.com`;
return async (
req: Request,
ctx: ConnInfo,
) => {
if (!url) {
throw new Error("Missing publicUrl");
}

const publicUrl =
new URL(url?.startsWith("http") ? url : `https://${url}`).href;
const shopifyUrl = `https://${appCtx.storeName}.myshopify.com`;

const response = await Proxy({
url: publicUrl,
return async (req: Request, conn: ConnInfo) => {
const reqOrigin = new URL(req.url).origin;
const proxyResponse = await Proxy({
url: shopifyUrl,
customHeaders: withDigestCookie(appCtx),
})(req, ctx);
})(req, conn);

if (!response.ok) {
return response;
}
if (!proxyResponse.ok) return proxyResponse;

const reqUrl = new URL(req.url);
const text = await response.text();
return new Response(
includeSiteMaps(
text.replaceAll(publicUrl, `${reqUrl.origin}/`),
reqUrl.origin,
include,
),
{
headers: response.headers,
status: response.status,
},
const originalXml = await proxyResponse.text();
const originWithSlash = reqOrigin.endsWith("/")
? reqOrigin.slice(0, -1)
: reqOrigin;
const originReplacedXml = originalXml.replaceAll(
shopifyUrl,
originWithSlash,
);
const excludedXml = excludeSitemaps(originReplacedXml, reqOrigin, exclude);

const includeBlock = buildIncludeSitemaps(reqOrigin, include);
const finalXml = includeBlock
? excludedXml.replace(XML_HEADER, `${XML_HEADER}\n${includeBlock}`)
: excludedXml;

const headers = new Headers(proxyResponse.headers);
headers.delete("content-length");
headers.delete("content-encoding");
headers.delete("etag");
headers.delete("accept-ranges");
if (!headers.get("content-type")?.includes("xml")) {
headers.set("content-type", "application/xml; charset=utf-8");
}
return new Response(finalXml, {
status: proxyResponse.status,
headers,
});
};
}
50 changes: 41 additions & 9 deletions shopify/loaders/ProductDetailsPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ import { AppContext } from "../../shopify/mod.ts";
import { toProductPage } from "../../shopify/utils/transform.ts";
Copy link

@cubic-dev-ai cubic-dev-ai bot Feb 26, 2026

Choose a reason for hiding this comment

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

P2: Trailing numeric segments in product handles are always stripped and treated as SKU ids, which will break lookups for handles that legitimately end with numbers.

Prompt for AI agents
Check if this issue is valid β€” if so, understand the root cause and fix it. At shopify/loaders/ProductDetailsPage.ts, line 52:

<comment>Trailing numeric segments in product handles are always stripped and treated as SKU ids, which will break lookups for handles that legitimately end with numbers.</comment>

<file context>
@@ -49,16 +49,13 @@ const loader = async (
   const splitted = slug?.split("-");
   const maybeSkuId = Number(splitted[splitted.length - 1]);
 
+  const handle = splitted.slice(0, maybeSkuId ? -1 : undefined).join("-");
+
   const data = await storefront.query<
</file context>
Fix with Cubic

import type { RequestURLParam } from "../../website/functions/requestToParam.ts";
import {
CountryCode,
GetProductQuery,
GetProductQueryVariables,
HasMetafieldsMetafieldsArgs,
LanguageCode,
} from "../utils/storefront/storefront.graphql.gen.ts";
import { GetProduct } from "../utils/storefront/queries.ts";
import { Metafield } from "../utils/types.ts";
import { LanguageContextArgs, Metafield } from "../utils/types.ts";

export interface Props {
slug: RequestURLParam;
Expand All @@ -17,6 +19,18 @@ export interface Props {
* @description search for metafields
*/
metafields?: Metafield[];
/**
* @title Language Code
* @description Language code for the storefront API
* @example "EN" for English, "FR" for French, etc.
*/
languageCode?: LanguageCode;
/**
* @title Country Code
* @description Country code for the storefront API
* @example "US" for United States, "FR" for France, etc.
*/
countryCode?: CountryCode;
}

/**
Expand All @@ -29,7 +43,7 @@ const loader = async (
ctx: AppContext,
): Promise<ProductDetailsPage | null> => {
const { storefront } = ctx;
const { slug } = props;
const { slug, languageCode = "PT", countryCode = "BR" } = props;
const metafields = props.metafields || [];

const splitted = slug?.split("-");
Expand All @@ -39,9 +53,9 @@ const loader = async (

const data = await storefront.query<
GetProductQuery,
GetProductQueryVariables & HasMetafieldsMetafieldsArgs
GetProductQueryVariables & HasMetafieldsMetafieldsArgs & LanguageContextArgs
>({
variables: { handle, identifiers: metafields },
variables: { handle, identifiers: metafields, languageCode, countryCode },
...GetProduct,
});

Expand All @@ -52,15 +66,33 @@ const loader = async (
return toProductPage(data.product, new URL(_req.url), maybeSkuId);
};

export const cache = "no-cache";
export const cache = "stale-while-revalidate";

export const cacheKey = (props: Props, req: Request): string => {
const { slug } = props;
const searchParams = new URLSearchParams({
slug,
});
const { slug, languageCode = "PT", countryCode = "BR", metafields } = props;

const searchParams = new URLSearchParams();

// Core parameters
searchParams.append("slug", slug);
searchParams.append("languageCode", languageCode);
searchParams.append("countryCode", countryCode);

// Add metafields to cache key if they exist
if (metafields?.length) {
const metafieldsKey = metafields
.map((m) => `${m.namespace}.${m.key}`)
.sort()
.join(",");
searchParams.append("metafields", metafieldsKey);
}

// Sort parameters for consistent cache keys
searchParams.sort();

const url = new URL(req.url);
url.search = searchParams.toString();

return url.href;
};

Expand Down
Loading
Loading