Skip to content
Draft
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
2 changes: 1 addition & 1 deletion apps/docs/content/docs/anatomy/aeo-geo.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ The `Vary: Accept` header ensures CDNs cache markdown and HTML responses separat

### What's included in the markdown

- **Product pages** — handle, brand, category, pricing, options, variants, specs, images, tags, and SEO metadata
- **Product pages** — handle, brand, category, pricing, exact variant count, representative selectable variants, options, specs, images, tags, and SEO metadata
- **Collection pages** — collection metadata, description, applied filters, available filters, products, pagination, image, and SEO metadata
- **Search pages** — query metadata, active collection filter, applied filters, available filters, products, and pagination state

Expand Down
23 changes: 13 additions & 10 deletions apps/docs/content/docs/anatomy/agent.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ AgentPanel (client) → POST /api/chat → ToolLoopAgent (Claude) → Shopify to

The agent adapts its instructions based on where the user is browsing. Page context is resolved server-side from the `Referer` header:

- **Product page** - the full product is injected into the prompt with all variants, prices, availability, and variant IDs ready for "add to cart"
- **Product page** - the product, exact variant count, options, and a representative selectable variant set are injected into the prompt; the agent resolves exact choices on demand
- **Collection page** - the collection handle and title are included
- **Search page** - the active search query is passed through
- **Cart page** - the agent knows the user is viewing their cart
Expand All @@ -40,17 +40,18 @@ The system prompt also includes the user's locale so the agent responds in the c

## Tools

The agent has 11 tools organized into three groups. All tools access the agent context (chat ID, cart ID, locale, page) via AsyncLocalStorage.
The agent has 12 tools organized into three groups. All tools access the agent context (chat ID, cart ID, locale, page) via AsyncLocalStorage.

### Product discovery

| Tool | Purpose |
| --------------------------- | ------------------------------------------------------------------------------------- |
| `searchProducts` | Keyword search with sorting (best matches, price low/high) - returns up to 10 results |
| `getProductDetails` | Full product info: title, description, price, variants, availability, images |
| `getProductRecommendations` | AI-powered similar products (up to 5) |
| `listCollections` | All store categories |
| `browseCollection` | Products within a collection with sorting |
| Tool | Purpose |
| --------------------------- | ----------------------------------------------------------------------------------------------------------- |
| `searchProducts` | Keyword search with sorting (best matches, price low/high) - returns up to 10 results |
| `getProductDetails` | Product info, exact variant count, mapped options, representative variants, and bundle relationships |
| `resolveProductVariant` | Resolves the user's exact option choices to a purchasable Shopify variant without downloading every variant |
| `getProductRecommendations` | AI-powered similar products (up to 5) |
| `listCollections` | All store categories |
| `browseCollection` | Products within a collection with sorting |

### Cart management

Expand All @@ -74,10 +75,12 @@ The agent doesn't just return text - it renders rich UI components via json-rend

- **AgentProductCard** - clickable card with image, title, price, compare-at price, and availability
- **AgentProductGrid** - 2-column responsive grid wrapping multiple product cards
- **AgentCartSummary** - full cart breakdown with line item thumbnails, subtotal, tax, total, and checkout button
- **AgentCartSummary** - full cart breakdown with line item thumbnails, bundle components, subtotal, total, checkout-time tax note, and checkout button
- **AgentCartConfirmation** - success card shown after adding an item to cart
- **AgentVariantPicker** - display-only variant selector with option groups and availability status

For high-variant products and Combined Listings, the picker is intentionally representative. After the shopper specifies options, the agent calls `resolveProductVariant` to obtain the exact variant ID and owning product handle before adding it to cart. Customized bundle parents that require shopper-selected components are not added by the generic agent flow.

These components are registered in a json-render registry and rendered client-side from specs embedded in the assistant's streamed messages.

## Cart reconciliation
Expand Down
8 changes: 7 additions & 1 deletion apps/docs/content/docs/anatomy/cart.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,9 @@ The overlay content in `overlay-content.tsx` composes three sections: an empty s

## Overlay items

Each line item in `overlay-item.tsx` reads from `cartWithPending` to reflect optimistic state. It renders the product image, title, variant options (color, size, etc.), quantity controls (+/- buttons), a remove button, and the line total. Quantity changes and removals dispatch through the cart context, which handles debouncing and server action calls.
Each line item in `overlay-item.tsx` reads from `cartWithPending` to reflect optimistic state. It renders the product image, title, variant options (color, size, etc.), bundle components, quantity controls (+/- buttons), a remove button, and the line total. Quantity changes and removals dispatch through the cart context, which handles debouncing and server action calls.

The controls honor Shopify's `CartLine.instructions.canUpdateQuantity` and `canRemove` flags instead of assuming every line is editable.

## Cart page

Expand All @@ -72,6 +74,10 @@ Below the items, a `RelatedProductsSection` shows product recommendations based

Cart operations in `lib/shopify/operations/cart.ts` use GraphQL mutations: `cartLinesAdd`, `cartLinesUpdate`, and `cartLinesRemove`. The cart ID is stored in a `shopify_cartId` HTTP-only cookie with a 7-day expiry. Every mutation calls `invalidateCartCache()` to ensure subsequent reads reflect the latest state.

The cart fragment reads both `CartLine` and `ComponentizableCartLine`, preserving `lineComponents` as nested domain cart lines. This lets fixed and transformed bundles render as one parent with their included products. The `addToCart()` operation also accepts Shopify's optional `CartLineInput.parent` relationship as a foundation for app-specific customized bundle flows.

The fragment intentionally does not query deprecated cart tax and duty amounts. Shopify finalizes those values at checkout, so storefront summaries show a taxes-and-shipping-at-checkout note instead of a misleading pre-checkout tax amount.

The transform layer in `lib/shopify/transforms/cart.ts` converts Shopify's GraphQL response shape into a domain `Cart` type used throughout the application. This isolates the rest of the codebase from Shopify's API structure.

## Key files
Expand Down
30 changes: 20 additions & 10 deletions apps/docs/content/docs/anatomy/pages/pdp.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -10,28 +10,36 @@ The Product Detail Page renders a single product with variant selection, an imag

## Rendering strategy

The PDP runs under Next.js 16 Cache Components. Product data is fetched through `"use cache"` operations with `cacheLife("max")` and a `product-{handle}` tag, so the page renders from cache on every request and is only rebuilt when a Shopify webhook invalidates the matching tag — ISR-style freshness with no per-request fetch (see [Shopify Integration](/docs/shopify) for the webhook setup).
The PDP runs under Next.js 16 Cache Components. Base product data is fetched through a `"use cache: remote"` operation with `cacheLife("max")` and a `product-{handle}` tag. Its cache key contains only the product handle and locale, so Shopify webhooks can invalidate the matching low-cardinality product entry (see [Shopify Integration](/docs/shopify) for the webhook setup).

The only dynamic input is the `?variant=` query parameter. Rather than block the whole page on that lookup, the route passes the `searchParams` promise — unawaited — down into `ProductDetailSection`. Anything that doesn't depend on the chosen variant (title, shared gallery images, uniformly-priced variants, the description, related products) renders straight from the cached product. Anything that does (the color-specific gallery slot, variant-specific price, option highlight, buy buttons) sits inside its own Suspense boundary so a `?variant=` change streams the affected slot without skeletonizing the rest of the page.
The dynamic input is the selected option values in `searchParams`. The route starts the cached base-product read and a compact `getProductSelection()` request in parallel. Selection responses use `cache: "no-store"` so variant combinations do not consume persistent runtime-cache entries. `ProductDetailSection` receives both as promises and keeps variant-dependent regions in Suspense boundaries without creating a request waterfall.

## Variant selection

Variants are selected via `searchParams`. Each option (color, size, etc.) renders as a `<Link>` pointing to the same product with Shopify's `?variant` query parameter:
Variants are selected via option-name query parameters:

```
/products/classic-tee?variant=123456
/products/classic-tee?Color=Black&Size=Medium
```

The page receives `searchParams` as a promise, threads the variant ID through to `computeSelection()` on the server, and that function returns `{ selectedOptions, selectedVariant, colorImages }` in one pass — matching the numeric variant ID to the full variant object. The result is threaded down as props with no client-side variant context or `useState`. This means:
The route passes those values to `getProductSelection()`, whose compact fragment resolves the current variant and option state with Shopify's `selectedOrFirstAvailableVariant`, `adjacentVariants`, and encoded variant fields. `computeSelection()` combines that response with the cached base product and derives the selected option record and color images.

- Every variant selection is a navigation, giving you browser back/forward for free
- URLs are shareable - opening a link loads the exact variant
- The page is fully server-rendered with zero layout shift
- Variant links use `scroll={false}` to prevent the page from jumping to the top on selection
- Combined Listing option links can navigate to the owning child product handle
- Existing but out-of-stock combinations remain navigable; combinations that do not exist render as inert elements

The `getVariantUrl()` helper in `lib/product.ts` computes the target URL for each option. When a user selects a new option value, it finds the matching variant (or falls back to the first variant with that option) and returns the URL with the correct `variant` query parameter.
### Liquid storefront migration

Unavailable options render as inert `<span>` elements instead of links.
Shopify Liquid themes use `/products/:handle?variant=:variantId` for variant deep links. A query-matched product proxy accepts those URLs as migration inputs, resolves the variant through a small `cache: "no-store"` query, and issues a permanent redirect to the normalized option-name URL before the PDP renders. This preserves indexed links, backlinks, ads, emails, and bookmarks while moving shoppers onto the headless URL format. Campaign and ad-click query parameters are preserved through the redirect.

Requests without `variant` do not match the compatibility proxy, so normal PDPs remain prerenderable and pay no additional lookup. The compatibility lookup is never stored in Runtime Cache, so imported Liquid links do not create high-cardinality variant entries. Product metadata keeps the base product path as the canonical URL, consolidating variant combinations as one product for search engines.

### High-variant products

The product query intentionally does not request `variants(first: 50)` or any other exhaustive variant connection. Shopify supports up to 2,048 variants, so the template uses `options.optionValues.firstSelectableVariant`, `adjacentVariants`, and the selected variant as a representative set. `variantsCount` remains the exact count for structured data and agent output.

Options with a single value render as a plain `Name: Value` label without a picker — there is no choice to surface, but the attribute is still useful information to the shopper (e.g. `Finish: Natural White Oak` on a one-variant product). Shopify's synthetic `Title: Default Title` option (emitted for products with no variant axes) is the only single-value option that is hidden entirely.

Expand All @@ -47,7 +55,7 @@ Both layouts receive pre-filtered images as props from the server.

### Color-based image filtering

When a product has color variants, the gallery shows only images relevant to the selected color. Shared images render immediately, while the color-specific slot streams behind a small Suspense fallback so changing `?variant` does not skeletonize the whole gallery. The `getPartitionedImagesForSelectedColor()` function in `lib/product.ts`:
When a product has color variants, the gallery shows only images relevant to the selected color. Shared images render immediately, while the color-specific slot streams behind a small Suspense fallback so changing an option does not skeletonize the whole gallery. The `getPartitionedImagesForSelectedColor()` function in `lib/product.ts`:

1. Identifies the color option via swatch data or option name
2. Collects variant images assigned to each color
Expand All @@ -64,7 +72,9 @@ The buy section includes stock status and two action buttons:
- **Buy with Shop** - creates a Shopify checkout and redirects to it, styled per [Shopify's brand guidelines](https://help.shopify.com/en/manual/payments/shop-pay/assets)
- **Add to Cart** - adds the item optimistically to the cart drawer

A single `BuyButtons` component renders identically on both mobile and desktop. It receives the `selectedVariant` as a prop and is a client component for cart interactivity.
A single `BuyButtons` component renders identically on both mobile and desktop. It receives only the selected variant fields needed for cart interactivity; bundle relationship arrays stay in server components instead of being serialized to the client.

Fixed bundle variants render their component products above the buy buttons. Component products also show the bundles returned by Shopify's `groupedBy` relationship. Customized bundle parents with `requiresComponents: true` and no fixed components show a disabled "Choose bundle items" action because their picker and component-line inputs are app-specific.

## Related Products

Expand Down Expand Up @@ -95,6 +105,6 @@ The page emits two JSON-LD schemas:
| `components/product-detail/product-detail-section.tsx` | Orchestrates media, options, pricing, and buy buttons with per-section Suspense; emits JSON-LD |
| `components/product-detail/product-media.tsx` | Responsive image gallery (mobile carousel, desktop grid + lightbox) |
| `components/product-detail/buy-buttons.tsx` | Buy with Shop + add-to-cart client component |
| `lib/product.ts` | `computeSelection`, variant resolution, URL generation, color-image partitioning |
| `lib/product.ts` | `computeSelection`, option-param parsing, and color-image partitioning |

The rest of the PDP — option pickers, price display, lightbox, schema, and so on — lives in `components/product-detail/`. Browse the directory when you need them.
12 changes: 4 additions & 8 deletions apps/docs/content/docs/anatomy/product-card.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Used in the home featured grid, collection (PLP) and search grids, the related-p

## Composition

`ProductCard` is a server component that wraps the product in a `Link` to `/products/[handle]` (appending `?variant=` when a default variant id is known). Each part — image container, image, content, title, price, optional badge — exposes a `data-slot` attribute (`product-card`, `product-card-image`, etc.) as a stable styling hook.
`ProductCard` is a server component that wraps the product in a `Link` to `/products/[handle]`. When Shopify provides default selected options, the link appends option-name query parameters such as `?Color=Black&Size=Medium`. Each part — image container, image, content, title, price, optional badge — exposes a `data-slot` attribute (`product-card`, `product-card-image`, etc.) as a stable styling hook.

## Variants

Expand Down Expand Up @@ -48,14 +48,10 @@ Add a hover image carousel to the product card.

Data:
- In lib/shopify/fragments.ts, add an `images(first: 5)` block (using ...ImageFields)
to PRODUCT_CARD_FRAGMENT, and add a `variants(first: 50)` block selecting each
variant's `image { url }`.
to PRODUCT_CARD_FRAGMENT.
- In lib/shopify/transforms/product.ts, restore an `images: Image[]` value on the
object returned by transformShopifyProductCard. Compute it with a
`filterVariantImages(product)` helper that flattens product.images, then excludes
any image whose URL (ignoring query params) matches a non-default variant's image -
so color-swatch variant images don't leak into the carousel. Add the matching
`images?` and `variants?` fields back to the ShopifyProductCard response interface.
object returned by transformShopifyProductCard by flattening `product.images`. Add
the matching `images?` field to the ShopifyProductCard response interface.
- In lib/types.ts, add `images: Image[]` back to the ProductCard interface.

Component:
Expand Down
Loading