Skip to content
Open
Show file tree
Hide file tree
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
3 changes: 2 additions & 1 deletion apps/docs/content/docs/getting-started/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,11 @@ Create a `.env.local` file in your project root:
```bash
SHOPIFY_STORE_DOMAIN="your-store.myshopify.com"
SHOPIFY_STOREFRONT_ACCESS_TOKEN="your-storefront-access-token"
SHOPIFY_DEFAULT_CURRENCY="USD"
NEXT_PUBLIC_SITE_NAME="Your Store Name"
```

You can find the storefront token in **Settings → Apps and sales channels → Headless**. For the full variable list, see [Environment Variables](/docs/reference/env-vars).
You can find the storefront token in **Settings → Apps and sales channels → Headless**. Set `SHOPIFY_DEFAULT_CURRENCY` to your shop currency, such as `GBP`, when it differs from USD. For the full variable list, see [Environment Variables](/docs/reference/env-vars).

## Step 4: Run locally

Expand Down
1 change: 1 addition & 0 deletions apps/docs/content/docs/reference/env-vars.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ type: reference

| Variable | Description |
|----------|-------------|
| `SHOPIFY_DEFAULT_CURRENCY` | Base storefront currency for single-locale filter UI and markdown fallbacks, e.g. `GBP` or `USD`. Set this when your shop currency is not USD so empty search/collection states still render the correct symbol. |
| `SHOPIFY_STOREFRONT_PRIVATE_TOKEN` | Private Storefront API token for server-side requests. Enables higher rate limits and access to draft content. |
| `SHOPIFY_CUSTOMER_ACCOUNT_URL` | Customer Account API URL. Required when using the enable-shopify-auth skill. |
| `SHOPIFY_CUSTOMER_CLIENT_ID` | Customer Account API client ID. Required when using the enable-shopify-auth skill. |
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/content/docs/reference/troubleshooting.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ To get near-instant updates, set up Shopify webhooks pointed at `/api/webhooks/s

## Locale or currency not changing

Multi-locale support requires Shopify Markets to be enabled. The template ships as single-locale by default. Run [`/vercel-shop:enable-shopify-markets`](/docs/skills/enable-shopify-markets) to add multi-locale and multi-currency routing.
Single-locale storefronts use Shopify pricing data where available, but empty search/collection states rely on `SHOPIFY_DEFAULT_CURRENCY` for filter chrome and markdown fallbacks. Set that env var to your shop currency, for example `GBP`, if your store is not USD. Multi-locale support still requires Shopify Markets to be enabled. Run [`/vercel-shop:enable-shopify-markets`](/docs/skills/enable-shopify-markets) to add multi-locale and multi-currency routing.

## Agent can't find context files

Expand Down
3 changes: 3 additions & 0 deletions apps/template/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,8 @@
SHOPIFY_STORE_DOMAIN="your-store.myshopify.com"
SHOPIFY_STOREFRONT_ACCESS_TOKEN="your-storefront-access-token"

# Optional — default currency used for single-locale filter chrome and markdown fallbacks
SHOPIFY_DEFAULT_CURRENCY="USD"

# Store display name (shown in header, metadata, etc.)
NEXT_PUBLIC_SITE_NAME="Your Store Name"
3 changes: 3 additions & 0 deletions apps/template/app/collections/md/[handle]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
buildProductFiltersFromParams,
getCollectionProducts,
} from "@/lib/shopify/operations/products";
import { getShopDefaultCurrencyCode } from "@/lib/shopify/operations/shop";
import { transformShopifyFilters } from "@/lib/shopify/transforms/filters";
import { RESULTS_PER_PAGE, parseFiltersFromSearchParams, searchParamsToRecord } from "@/lib/utils";

Expand Down Expand Up @@ -52,11 +53,13 @@ export async function GET(request: Request, { params }: { params: Promise<{ hand

const transformedFilters = transformShopifyFilters(result.filters, { activeFilters });
const hasPriceRange = result.filters.some((filter) => filter.type === "PRICE_RANGE");
const currencyCode = result.products[0]?.price.currencyCode ?? (await getShopDefaultCurrencyCode());
const markdown = collectionToMarkdown({
collection,
products: result.products,
filters: transformedFilters.filters,
priceRange: hasPriceRange ? transformedFilters.priceRange : undefined,
currencyCode,
activeFilters,
pageInfo: result.pageInfo,
locale,
Expand Down
3 changes: 3 additions & 0 deletions apps/template/app/search/md/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
buildProductFiltersFromParams,
getProducts,
} from "@/lib/shopify/operations/products";
import { getShopDefaultCurrencyCode } from "@/lib/shopify/operations/shop";
import { transformShopifyFilters } from "@/lib/shopify/transforms/filters";
import { RESULTS_PER_PAGE, parseFiltersFromSearchParams, searchParamsToRecord } from "@/lib/utils";

Expand Down Expand Up @@ -40,13 +41,15 @@ export async function GET(request: Request) {

const transformedFilters = transformShopifyFilters(result.filters, { activeFilters });
const hasPriceRange = result.filters.some((filter) => filter.type === "PRICE_RANGE");
const currencyCode = result.products[0]?.price.currencyCode ?? (await getShopDefaultCurrencyCode());
const markdown = searchResultsToMarkdown({
query,
collection,
products: result.products,
total: result.total,
filters: transformedFilters.filters,
priceRange: hasPriceRange ? transformedFilters.priceRange : undefined,
currencyCode,
activeFilters,
pageInfo: result.pageInfo,
locale,
Expand Down
3 changes: 3 additions & 0 deletions apps/template/app/search/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import type { Locale } from "@/lib/i18n";
import { getLocale } from "@/lib/params";
import { buildAlternates, buildOpenGraph } from "@/lib/seo";
import { buildProductFiltersFromParams, getProducts } from "@/lib/shopify/operations/products";
import { getShopDefaultCurrencyCode } from "@/lib/shopify/operations/shop";
import { transformShopifyFilters } from "@/lib/shopify/transforms/filters";
import { RESULTS_PER_PAGE, parseFiltersFromSearchParams } from "@/lib/utils";

Expand Down Expand Up @@ -202,11 +203,13 @@ async function SearchFilterContent({
const transformedFilters = transformShopifyFilters(result.filters, {
activeFilters,
});
const currencyCode = result.products[0]?.price.currencyCode ?? (await getShopDefaultCurrencyCode());

return (
<CollectionFilterSidebarClient
filters={transformedFilters.filters}
priceRange={transformedFilters.priceRange}
currencyCode={currencyCode}
activeFilters={activeFilters}
/>
);
Expand Down
14 changes: 11 additions & 3 deletions apps/template/components/cart/context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,15 +96,23 @@ function computeCartWithPending(
}

if (pendingQuantity > 0) {
const currencyCode =
pendingLines[0]?.cost.totalAmount.currencyCode ??
pendingLines[0]?.merchandise.price?.currencyCode;

if (!currencyCode) {
return null;
}

return {
id: undefined,
checkoutUrl: "",
totalQuantity: pendingQuantity,
note: null,
cost: {
subtotalAmount: { amount: "0", currencyCode: "USD" },
totalAmount: { amount: "0", currencyCode: "USD" },
totalTaxAmount: { amount: "0", currencyCode: "USD" },
subtotalAmount: { amount: "0", currencyCode },
totalAmount: { amount: "0", currencyCode },
totalTaxAmount: { amount: "0", currencyCode },
},
lines: pendingLines,
shippingCost: null,
Expand Down
61 changes: 49 additions & 12 deletions apps/template/components/collections/filter-sidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { useTranslations } from "next-intl";
import { useLocale, useTranslations } from "next-intl";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { useEffect, useOptimistic, useRef, useState } from "react";

Expand All @@ -24,30 +24,64 @@ import {
} from "@/components/ui/filter-sidebar";
import { getActiveFilterBadges } from "@/lib/shopify/transforms/filters";
import type { Filter, PriceRange } from "@/lib/types";
import { getCurrencySymbol } from "@/lib/utils";

interface CollectionFilterSidebarClientProps {
filters: Filter[];
priceRange?: PriceRange;
currencyCode: string;
activeFilters: Record<string, string | string[] | undefined>;
}

type FilterState = Record<string, string | string[] | undefined>;

const PRICE_PRESETS = [
{ label: "Under $50", min: 0, max: 50 },
{ label: "$50 - $100", min: 50, max: 100 },
{ label: "$100 - $200", min: 100, max: 200 },
{ label: "Over $200", min: 200, max: undefined },
{ min: 0, max: 50 },
{ min: 50, max: 100 },
{ min: 100, max: 200 },
{ min: 200, max: undefined },
];

function formatPriceRangeLabel(min: number | null, max: number | null): string {
function formatCurrencyAmount(amount: number, locale: string, currencyCode: string): string {
return new Intl.NumberFormat(locale, {
style: "currency",
currency: currencyCode,
currencyDisplay: "narrowSymbol",
minimumFractionDigits: Number.isInteger(amount) ? 0 : 2,
maximumFractionDigits: Number.isInteger(amount) ? 0 : 2,
}).format(amount);
}

function formatPriceRangeLabel(
min: number | null,
max: number | null,
locale: string,
currencyCode: string,
): string {
if (min !== null && max !== null) {
return `$${min} - $${max}`;
return `${formatCurrencyAmount(min, locale, currencyCode)} - ${formatCurrencyAmount(max, locale, currencyCode)}`;
}
if (min !== null) {
return `From $${min}`;
return `From ${formatCurrencyAmount(min, locale, currencyCode)}`;
}
return `Up to ${formatCurrencyAmount(max ?? 0, locale, currencyCode)}`;
}

function formatPricePresetLabel(
min: number,
max: number | undefined,
locale: string,
currencyCode: string,
): string {
if (max === undefined) {
return `Over ${formatCurrencyAmount(min, locale, currencyCode)}`;
}

if (min === 0) {
return `Under ${formatCurrencyAmount(max, locale, currencyCode)}`;
}
return `Up to $${max}`;

return `${formatCurrencyAmount(min, locale, currencyCode)} - ${formatCurrencyAmount(max, locale, currencyCode)}`;
}

function getFilterValues(value: string | string[] | undefined): string[] {
Expand Down Expand Up @@ -117,8 +151,10 @@ function applyPriceParams(params: URLSearchParams, min: number | null, max: numb
export function CollectionFilterSidebarClient({
filters,
priceRange,
currencyCode,
activeFilters,
}: CollectionFilterSidebarClientProps) {
const locale = useLocale();
const pathname = usePathname();
const router = useRouter();
const searchParams = useSearchParams();
Expand Down Expand Up @@ -236,7 +272,7 @@ export function CollectionFilterSidebarClient({
))}
{hasPriceFilter && (
<FilterBadge variant="primary" onRemove={removePriceRange}>
{formatPriceRangeLabel(urlPriceMin, urlPriceMax)}
{formatPriceRangeLabel(urlPriceMin, urlPriceMax, locale, currencyCode)}
</FilterBadge>
)}
</FilterSidebarActiveFilters>
Expand All @@ -252,6 +288,7 @@ export function CollectionFilterSidebarClient({
onMinChange={setMinInput}
onMaxChange={setMaxInput}
onApply={applyPriceRange}
currencySymbol={getCurrencySymbol(currencyCode, locale)}
>
<FilterOptionList>
{PRICE_PRESETS.map((preset) => {
Expand All @@ -263,11 +300,11 @@ export function CollectionFilterSidebarClient({

return (
<FilterPricePreset
key={preset.label}
key={`${preset.min}-${preset.max ?? "plus"}`}
selected={isSelected}
onClick={() => applyPricePreset(preset.min, preset.max)}
>
{preset.label}
{formatPricePresetLabel(preset.min, preset.max, locale, currencyCode)}
</FilterPricePreset>
);
})}
Expand Down
3 changes: 2 additions & 1 deletion apps/template/components/collections/filters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,13 @@ async function Render({
}: {
collectionResultsDataPromise: Promise<CollectionResultsData>;
}) {
const { activeFilters, transformedFilters } = await collectionResultsDataPromise;
const { activeFilters, currencyCode, transformedFilters } = await collectionResultsDataPromise;

return (
<CollectionFilterSidebarClient
filters={transformedFilters.filters}
priceRange={transformedFilters.priceRange}
currencyCode={currencyCode}
activeFilters={activeFilters}
/>
);
Expand Down
3 changes: 3 additions & 0 deletions apps/template/components/search/results.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { ProductCard, ProductCardSkeleton } from "@/components/product-card";
import { Skeleton } from "@/components/ui/skeleton";
import type { Locale } from "@/lib/i18n";
import { buildProductFiltersFromParams, getProducts } from "@/lib/shopify/operations/products";
import { getShopDefaultCurrencyCode } from "@/lib/shopify/operations/shop";
import { transformShopifyFilters } from "@/lib/shopify/transforms/filters";
import { RESULTS_PER_PAGE } from "@/lib/utils";

Expand Down Expand Up @@ -69,6 +70,7 @@ export async function Results({
const transformedFilters = transformShopifyFilters(result.filters, {
activeFilters,
});
const currencyCode = result.products[0]?.price.currencyCode ?? (await getShopDefaultCurrencyCode());
const products = result.products;

return (
Expand All @@ -78,6 +80,7 @@ export async function Results({
<CollectionFilterSidebarClient
filters={transformedFilters.filters}
priceRange={transformedFilters.priceRange}
currencyCode={currencyCode}
activeFilters={activeFilters}
/>
</FilterPendingScope>
Expand Down
3 changes: 3 additions & 0 deletions apps/template/lib/collections/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
buildProductFiltersFromParams,
getCollectionProducts,
} from "@/lib/shopify/operations/products";
import { getShopDefaultCurrencyCode } from "@/lib/shopify/operations/shop";
import { type TransformedFilters, transformShopifyFilters } from "@/lib/shopify/transforms/filters";
import { RESULTS_PER_PAGE, parseFiltersFromSearchParams } from "@/lib/utils";

Expand All @@ -15,6 +16,7 @@ export interface CollectionSearchState {
export interface CollectionResultsData {
activeFilters: Record<string, string | string[] | undefined>;
cursor?: string;
currencyCode: string;
result: Awaited<ReturnType<typeof getCollectionProducts>>;
transformedFilters: TransformedFilters;
}
Expand Down Expand Up @@ -57,6 +59,7 @@ export async function getCollectionResultsData({
return {
activeFilters,
cursor,
currencyCode: result.products[0]?.price.currencyCode ?? (await getShopDefaultCurrencyCode()),
result,
transformedFilters: transformShopifyFilters(result.filters, {
activeFilters,
Expand Down
3 changes: 2 additions & 1 deletion apps/template/lib/i18n/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ export function resolveLocale(value: string | null | undefined): Locale {
return value && isEnabledLocale(value) ? value : defaultLocale;
}

// Currency data per locale
// Explicit locale → currency mappings are only for configured multi-locale
// storefronts. Single-locale pricing should come from Shopify responses.
const localeCurrency: Record<Locale, { currency: string; symbol: string }> = {
"en-US": { currency: "USD", symbol: "$" },
};
Expand Down
9 changes: 4 additions & 5 deletions apps/template/lib/markdown/catalog.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { getCurrencyCode } from "@/lib/i18n";
import type { Filter, PageInfo, PriceRange, ProductCard } from "@/lib/types";
import { getActiveFilterBadges } from "@/lib/shopify/transforms/filters";

Expand All @@ -24,9 +23,7 @@ function getSingleValue(value: string | string[] | undefined): string | undefine
return Array.isArray(value) ? value[0] : value;
}

function formatPriceRange(priceRange: PriceRange, locale: string): string {
const currencyCode = getCurrencyCode(locale);

function formatPriceRange(priceRange: PriceRange, locale: string, currencyCode: string): string {
return `${formatPrice({ amount: priceRange.min.toString(), currencyCode }, locale)} - ${formatPrice(
{ amount: priceRange.max.toString(), currencyCode },
locale,
Expand Down Expand Up @@ -83,10 +80,12 @@ export function appendAvailableFiltersSection(
{
filters,
priceRange,
currencyCode,
locale,
}: {
filters: Filter[];
priceRange?: PriceRange;
currencyCode: string;
locale: string;
},
): void {
Expand All @@ -98,7 +97,7 @@ export function appendAvailableFiltersSection(
sections.push("");

if (priceRange) {
sections.push(`- **Price Range**: ${formatPriceRange(priceRange, locale)}`);
sections.push(`- **Price Range**: ${formatPriceRange(priceRange, locale, currencyCode)}`);
}

for (const filter of filters) {
Expand Down
4 changes: 3 additions & 1 deletion apps/template/lib/markdown/collection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export function collectionToMarkdown({
products,
filters,
priceRange,
currencyCode,
activeFilters,
pageInfo,
locale,
Expand All @@ -23,6 +24,7 @@ export function collectionToMarkdown({
products: ProductCard[];
filters: Filter[];
priceRange?: PriceRange;
currencyCode: string;
activeFilters: Record<string, string | string[] | undefined>;
pageInfo: PageInfo;
locale: string;
Expand Down Expand Up @@ -50,7 +52,7 @@ export function collectionToMarkdown({
}

appendAppliedFiltersSection(sections, { activeFilters, filters });
appendAvailableFiltersSection(sections, { filters, priceRange, locale });
appendAvailableFiltersSection(sections, { filters, priceRange, currencyCode, locale });
appendProductsSection(sections, { products, locale });
appendPaginationSection(sections, pageInfo);

Expand Down
Loading