From b139b302736c58d5138c31e72d03bb3f579845cb Mon Sep 17 00:00:00 2001 From: Rami Bitar Date: Tue, 4 Aug 2026 13:20:47 -0400 Subject: [PATCH] Hide the synthetic Default Title option on single-SKU products Shopify gives every single-variant product one option named Title with the lone value Default Title. It isn't a real choice, so filter it out of the product detail options, the cart line summaries, and the payload the store assistant sees. Variant matching still reads the raw selectedOptions, so single-SKU products keep resolving their variant. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01EuRVxuyMUTzWKXW8P5Tn8A --- app/api/chat/route.ts | 16 +++++++++++----- components/shopify/cart-drawer.tsx | 6 +++++- .../product-detail/product-detail-info.tsx | 8 +++++--- services/shopify/catalog.ts | 19 +++++++++++++++++++ 4 files changed, 40 insertions(+), 9 deletions(-) diff --git a/app/api/chat/route.ts b/app/api/chat/route.ts index cce2cdd..a9ea3e2 100644 --- a/app/api/chat/route.ts +++ b/app/api/chat/route.ts @@ -7,6 +7,8 @@ import { getProductsPage, getCollections, getCollectionProductsPage, + isDefaultTitleOption, + isDefaultTitleSelection, } from '@/services/shopify/catalog'; // Streaming needs the Node runtime here because the Storefront helpers run @@ -63,10 +65,12 @@ const summariseProduct = (product: { description: product.description?.slice(0, 300) ?? null, productType: product.productType || null, tags: product.tags ?? [], - options: product.options?.map((option) => ({ - name: option.name, - values: option.values, - })), + options: product.options + ?.filter((option) => !isDefaultTitleOption(option)) + .map((option) => ({ + name: option.name, + values: option.values, + })), inStock: product.variants?.edges.some((edge) => edge.node.availableForSale), }); @@ -132,7 +136,9 @@ export async function POST(req: Request) { title: node.title, available: node.availableForSale, price: `${node.price.amount} ${node.price.currencyCode}`, - options: node.selectedOptions, + options: node.selectedOptions?.filter( + (option) => !isDefaultTitleSelection(option) + ), })), }; }, diff --git a/components/shopify/cart-drawer.tsx b/components/shopify/cart-drawer.tsx index 81ace6f..c8f7c63 100644 --- a/components/shopify/cart-drawer.tsx +++ b/components/shopify/cart-drawer.tsx @@ -25,6 +25,7 @@ import { EmptyDescription, EmptyContent, } from '@/components/ui/empty'; +import { isDefaultTitleSelection } from '@/services/shopify/catalog'; const CartDrawer: React.FC = () => { const isOpen = useCartStore((s) => s.isOpen); @@ -102,7 +103,10 @@ const CartDrawer: React.FC = () => { }; const getSelectedOptions = (item: (typeof items)[0]) => { - return item.merchandise.selectedOptions ?? []; + // Single-SKU items carry a synthetic `Title: Default Title` — not worth a line. + return (item.merchandise.selectedOptions ?? []).filter( + (option) => !isDefaultTitleSelection(option) + ); }; return ( diff --git a/components/shopify/product-detail/product-detail-info.tsx b/components/shopify/product-detail/product-detail-info.tsx index aadea71..b8318aa 100644 --- a/components/shopify/product-detail/product-detail-info.tsx +++ b/components/shopify/product-detail/product-detail-info.tsx @@ -5,6 +5,7 @@ import { RiSubtractLine, RiAddLine } from '@remixicon/react'; import ShopPayButton from '@/components/shopify/shop-pay-button'; import type { ProductOption, ProductOptionValue } from '@/hooks/use-shopify-products'; import { isSwatchOptionName, swatchColorForName } from '@/config/swatches'; +import { isDefaultTitleOption } from '@/services/shopify/catalog'; interface ProductPrice { amount: string; @@ -103,9 +104,10 @@ const ProductDetailInfo: React.FC = ({ isSwatchOptionName(option.name); // Some products return Size before Color; show the swatches first either way. - const orderedOptions = [...(product.options ?? [])].sort( - (a, b) => Number(isSwatchOption(b)) - Number(isSwatchOption(a)) - ); + // Single-SKU products expose a synthetic `Title: Default Title` option — drop it. + const orderedOptions = [...(product.options ?? [])] + .filter((option) => !isDefaultTitleOption(option)) + .sort((a, b) => Number(isSwatchOption(b)) - Number(isSwatchOption(a))); // `optionValues` carries the swatch data; fall back to plain `values`. const optionValuesFor = (option: ProductOption): ProductOptionValue[] => diff --git a/services/shopify/catalog.ts b/services/shopify/catalog.ts index 3b1b497..9c8461e 100644 --- a/services/shopify/catalog.ts +++ b/services/shopify/catalog.ts @@ -64,6 +64,25 @@ export interface ProductOption { optionValues?: ProductOptionValue[]; } +/** + * Single-variant products still carry one synthetic option — `Title` with the + * lone value `Default Title`. It isn't a real choice, so keep it out of the UI. + */ +export const isDefaultTitleOption = (option: { + name: string; + values: string[]; +}): boolean => + option.name === 'Title' && + option.values.length === 1 && + option.values[0] === 'Default Title'; + +/** Same synthetic option, as it appears on a variant's `selectedOptions`. */ +export const isDefaultTitleSelection = (selection: { + name: string; + value: string; +}): boolean => + selection.name === 'Title' && selection.value === 'Default Title'; + export interface Product { id: string; title: string;