Skip to content

Feat [Shopify]: Internationalization Support for Shopify Loaders and Queries #1100

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
1a4a9e1
feat: add languageCode and countryCode props to product loaders and u…
yuriassuncx Apr 11, 2025
7bafc87
fix: update queries so codegen doesn't complain
yuriassuncx Apr 11, 2025
8f52f1c
feat: refactor loaders to use LanguageContextArgs for language and co…
yuriassuncx Apr 11, 2025
089294f
refactor: replace languageCode and countryCode with LanguageContextAr…
yuriassuncx Apr 11, 2025
1d2b01d
feat: add languageCode and countryCode to GetShopInfo query and loade…
yuriassuncx Apr 15, 2025
735ee41
feat: update GetCart and ListProducts queries to include languageCode…
yuriassuncx Apr 16, 2025
230ae60
feat: update CreateCart mutation to accept countryCode and adjust Get…
yuriassuncx May 21, 2025
238c659
feat: enhance loader to support multiple path segments for collection…
yuriassuncx May 27, 2025
0cea7e8
refactor(temporary): remove countryCode variable from CreateCart muta…
yuriassuncx Jun 11, 2025
51af45f
feat(revert): update CreateCart mutation to include countryCode for b…
yuriassuncx Jun 16, 2025
2af8217
feat: refactor sitemap and proxy loaders to support exclusion of path…
yuriassuncx Jun 18, 2025
3c90bea
feat: add collectionId to loader and update ProductsByCollection quer…
yuriassuncx Jun 26, 2025
3242237
refactor: remove countryCode variable from CreateCart mutation
yuriassuncx Jun 26, 2025
a378f6a
feat: add CartBuyerIdentityUpdate mutation and integrate into cart ac…
yuriassuncx Jun 30, 2025
8d860f1
feat: update proxy handler resolution to differentiate between sitema…
yuriassuncx Jul 2, 2025
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
36 changes: 36 additions & 0 deletions shopify/actions/cart/cartBuyerIdentityUpdate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { AppContext } from "../../mod.ts";
import { getCartCookie, setCartCookie } from "../../utils/cart.ts";
import { CartBuyerIdentityUpdate } from "../../utils/storefront/queries.ts";
import {
CartFragment,
CartBuyerIdentityUpdatePayload,
MutationCartBuyerIdentityUpdateArgs,
CartBuyerIdentityInput,
} from "../../utils/storefront/storefront.graphql.gen.ts";

const action = async (
props: CartBuyerIdentityInput,
req: Request,
ctx: AppContext,
): Promise<CartFragment | null> => {
const { storefront } = ctx;
const cartId = getCartCookie(req.headers);

if (!cartId) {
throw new Error("Missing cart id");
}

const { cartBuyerIdentityUpdate } = await storefront.query<
{ cartBuyerIdentityUpdate: CartBuyerIdentityUpdatePayload },
MutationCartBuyerIdentityUpdateArgs
>({
variables: { cartId, buyerIdentity: props },
...CartBuyerIdentityUpdate,
});

setCartCookie(ctx.response.headers, cartId);

return cartBuyerIdentityUpdate.cart ?? null;
};

export default action;
104 changes: 51 additions & 53 deletions shopify/handlers/sitemap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,74 +3,72 @@ import { AppContext } from "../mod.ts";
import { withDigestCookie } from "../utils/password.ts";

type ConnInfo = Deno.ServeHandlerInfo;
const xmlHeader =
'<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">';

const includeSiteMaps = (
currentXML: string,
origin: string,
includes?: string[],
) => {
const siteMapIncludeTags = [];
const XML_HEADER = '<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">';
const TODAY = new Date().toISOString().substring(0, 10);

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;
};
function buildIncludeSitemaps(origin: string, includes?: string[]) {
if (!includes?.length) return "";

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

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

return xml.replace(
/<sitemap>\s*<loc>(.*?)<\/loc>[\s\S]*?<\/sitemap>/g,
(match, loc) => {
const locPath = loc.startsWith(origin)
? loc.slice(origin.length)
: new URL(loc).pathname;

return excludes.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 (!proxyResponse.ok) return proxyResponse;

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);

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

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,
},
);
return new Response(finalXml, {
headers: proxyResponse.headers,
status: proxyResponse.status,
});
};
}
22 changes: 18 additions & 4 deletions shopify/loaders/ProductDetailsPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ import {
GetProductQuery,
GetProductQueryVariables,
HasMetafieldsMetafieldsArgs,
LanguageCode,
CountryCode
} 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 Down
26 changes: 24 additions & 2 deletions shopify/loaders/ProductList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import {
QueryRootCollectionArgs,
QueryRootSearchArgs,
SearchResultItemConnection,
LanguageCode,
CountryCode
} from "../utils/storefront/storefront.graphql.gen.ts";
import { toProduct } from "../utils/transform.ts";
import {
Expand All @@ -21,7 +23,7 @@ import {
searchSortShopify,
sortShopify,
} from "../utils/utils.ts";
import { Metafield } from "../utils/types.ts";
import { LanguageContextArgs, Metafield } from "../utils/types.ts";

export interface QueryProps {
/** @description search term to use on search */
Expand Down Expand Up @@ -70,6 +72,18 @@ export type 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;
};

// deno-lint-ignore no-explicit-any
Expand All @@ -92,6 +106,8 @@ const loader = async (

const count = props.count ?? 12;
const metafields = expandedProps.metafields || [];
const languageCode = expandedProps?.languageCode ?? "PT";
const countryCode = expandedProps?.countryCode ?? "BR";

let shopifyProducts:
| SearchResultItemConnection
Expand Down Expand Up @@ -119,15 +135,18 @@ const loader = async (
});

if (isQueryList(props)) {

const data = await storefront.query<
QueryRoot,
QueryRootSearchArgs & HasMetafieldsMetafieldsArgs
QueryRootSearchArgs & HasMetafieldsMetafieldsArgs & LanguageContextArgs
>({
variables: {
first: count,
query: props.query,
productFilters: filters,
identifiers: metafields,
languageCode,
countryCode,
...searchSortShopify[sort],
},
...SearchProducts,
Expand All @@ -139,12 +158,15 @@ const loader = async (
& QueryRootCollectionArgs
& CollectionProductsArgs
& HasMetafieldsMetafieldsArgs
& LanguageContextArgs
>({
variables: {
first: count,
handle: props.collection,
filters,
identifiers: metafields,
languageCode,
countryCode,
...sortShopify[sort],
},
...ProductsByCollection,
Expand Down
Loading
Loading