← ReploCONTENT HISTORY

Update to Replo

Snapshot Oct 30, 2026 · 00:05 UTC · version 2.0.4

Collection source: downloaded plugin package.

WHAT CHANGED

First saved snapshot

No earlier snapshot is available to establish a change.

Save Replo and check future changes in Following.

Up to 3 plugins free · no card required.

Compare saved observations

Download comparison JSON
Full technical diff · 0 changed fields
Full snapshot data
{
  "description": "REQUIRED before using any Shopify tools or generating pages with product data. Contains mandatory data loading patterns — code generated without reading this skill will use incorrect architecture. Use when user asks about Shopify, Shop Pay, Shopify products, metafields or metaobjects, etc",
  "included_files": [
    {
      "relative_path": "references/add-to-cart-from-collection-grid.md",
      "size_in_bytes": 2649
    },
    {
      "relative_path": "references/shop-pay-button.md",
      "size_in_bytes": 5237
    },
    {
      "relative_path": "references/shopify-markets.md",
      "size_in_bytes": 6623
    },
    {
      "relative_path": "references/translating-shopify-content.md",
      "size_in_bytes": 4359
    }
  ],
  "name": "shopify",
  "skill_md_contents": "---\nname: shopify\ntitle: Shopify Data & Cart\nsummary: Load a store's catalog, metafields, and cart from Shopify.\ndescription: \"REQUIRED before using any Shopify tools or generating pages with product data. Contains mandatory data loading patterns — code generated without reading this skill will use incorrect architecture. Use when user asks about Shopify, Shop Pay, Shopify products, metafields or metaobjects, etc\"\n---\n\n# Shopify Storefront Data & Cart\n\n## Reading the store's catalog and custom data\n\nThe loaders below render Shopify data on your pages and need no Shopify\ncredentials — the site resolves them server-side. **Discovery** reads — searching\nproducts, listing metafield or metaobject definitions, inspecting a real\nmetafield value — run against the store's Admin API, which you reach through the\nintegrations tools:\n\n- Search your tool list for the `shopify_` prefix. Each read operation is its\n  own tool with its own input schema — the product search, collection get, and\n  metafield-definition list operations are the usual starting points. Every one\n  takes a `reploProjectId`.\n- Tools depend on the available integration surface; absence from the tool list\n  is not evidence that Replo cannot connect Shopify. Check integration status\n  when the operation is missing or the call reports a connection error.\n- When a call fails with a connection error, Shopify is not connected yet. Call\n  `get_integration_status` with `integrationKey: \"shopify\"`, give the user the\n  `connectUrl` it returns, and poll until it reports connected.\n\nUse the discovered public read operation for each lookup described below, following its current input schema and passing `reploProjectId`. If an operation remains unavailable after checking connection status, explain that limitation and ask for the required source data.\n\nWrites to the store are not on the public surface. Direct the user to the\ncorresponding supported action in Replo.\n\nFor Replo-managed products (a separate catalog from Shopify), use the\n`find_products` and `get_product` tools directly.\n\n## Translating Shopify catalog content\n\nWhen the site uses locale routing / dictionaries **and** Shopify product or collection loaders, read [translating-shopify-content.md](references/translating-shopify-content.md). Dictionary files do not translate Shopify titles, descriptions, or metafields — pass `language` on the loaders.\n\n## Shopify Markets (currency / country)\n\nLanguage and Markets are different concepts. `language` translates catalog text; loader `country` selects market currency and international pricing. When the user asks about currency, Markets, regional prices, cart currency, or market-specific catalog, read [shopify-markets.md](references/shopify-markets.md). Pass `country` on the product/collection loaders **and** the same code on `ReploProvider` as `markets={{ shopify: { country } }}` — loaders alone leave cart and checkout in the shop default. Do not fold Markets work into the translation reference.\n\n## File Structure\n\nData loaders live in the `@replohq/sdk` package under `loaders/`:\n\n- **Generic infrastructure:** `@replohq/sdk/loaders/invoke-loader-server`, `@replohq/sdk/loaders/prefetch-loaders`\n- **Loader components:** `@replohq/sdk/loaders/product-loader`, `@replohq/sdk/loaders/collection-loader`, `@replohq/sdk/loaders/collection-products-loader`\n\nImport each loader component directly from its own file (e.g. `@replohq/sdk/loaders/product-loader`).\nImport `PrefetchedLoaders` (server component) directly from `@replohq/sdk/loaders/prefetch-loaders`.\n\nLoader components are generic — they work with any integration. The `loaderKey` prop tells the dependency resolver which integration handler to use. Always import constants from `@replohq/sdk/loaders/loader-keys`:\n\n- **`DATA_LOADER_KEYS`** for loader keys (e.g., `DATA_LOADER_KEYS.SHOPIFY_PRODUCT`) — used in `loaderKey` props and `PrefetchedLoaders` queries.\n\nReact Query cache keys are derived internally as `[loaderKey, args]` — you never need to specify them manually.\n\nNever use raw strings for loader keys.\n\n## Non-Active Products\n\nSearch tools return every product, but loaders only render `ACTIVE` products published to the Online Store — a `DRAFT` or `ARCHIVED` product errors in pages (`UNLISTED` still renders when referenced directly by GID or handle). You can still build pages with non-active products; just tell the user the page won't show real data until they publish the product.\n\n## Data Loading Pattern\n\n**Always use the prefetch pattern by default.** The only exception is when data must be loaded dynamically in response to a client-side input or interaction.\n\nThe pattern has two layers:\n\n1. **Server component (page):** Uses `PrefetchedLoaders` to fetch data during SSR and seed the React Query cache via `HydrationBoundary`. This ensures the first paint includes the data with no loading state.\n2. **Client component:** Uses a loader component (e.g., `ProductLoader`) with `useSuspenseQuery` under the hood. On initial render it reads the prefetched cache — instant. On subsequent client-side navigations or prop changes, it fetches fresh data automatically.\n\n### Example: Product Page with Prefetch (GID-based)\n\nThis is the default pattern. Use the product's Shopify GID (returned by search tools as `id`) to load the product via `productId`. The file locations below mirror the existing scaffold structure: `page.tsx` lives in the route folder, and client components live in `app/components/`. Build the client component first.\n\n**Client component** (`app/components/FeaturedProductDetail.tsx`):\n\n```tsx\n\"use client\";\n\nimport Image from \"next/image\";\n\nimport { DATA_LOADER_KEYS } from \"@replohq/sdk/loaders/loader-keys\";\nimport { ProductLoader } from \"@replohq/sdk/loaders/product-loader\";\n\nexport function FeaturedProductDetail({ productId }: { productId: string }) {\n  return (\n    <ProductLoader\n      loaderKey={DATA_LOADER_KEYS.SHOPIFY_PRODUCT}\n      productId={productId}\n      fallback={<div>Loading product…</div>}\n    >\n      {(product) => (\n        <div>\n          <h1>{product.title}</h1>\n          {product.featuredImage && (\n            <Image\n              src={product.featuredImage.url}\n              alt={product.featuredImage.altText ?? \"\"}\n              width={product.featuredImage.width ?? 800}\n              height={product.featuredImage.height ?? 800}\n            />\n          )}\n          <p>{product.availableForSale ? \"In stock\" : \"Out of stock\"}</p>\n          <ul>\n            {product.variants.map((variant) => (\n              <li key={variant.id}>\n                {variant.title} — {variant.price.amount}{\" \"}\n                {variant.price.currencyCode}\n              </li>\n            ))}\n          </ul>\n        </div>\n      )}\n    </ProductLoader>\n  );\n}\n```\n\n**Server component** (`app/products/featured/page.tsx`):\n\n```tsx\nimport { FeaturedProductDetail } from \"@/app/components/FeaturedProductDetail\";\nimport { DATA_LOADER_KEYS } from \"@replohq/sdk/loaders/loader-keys\";\nimport { PrefetchedLoaders } from \"@replohq/sdk/loaders/prefetch-loaders\";\n\nconst FEATURED_PRODUCT_GID = \"gid://shopify/Product/123456789\";\n\nexport default function FeaturedProductPage() {\n  return (\n    <PrefetchedLoaders\n      queries={[\n        {\n          loaderKey: DATA_LOADER_KEYS.SHOPIFY_PRODUCT,\n          args: { productId: FEATURED_PRODUCT_GID },\n        },\n      ]}\n    >\n      <FeaturedProductDetail productId={FEATURED_PRODUCT_GID} />\n    </PrefetchedLoaders>\n  );\n}\n```\n\n### Alternative: Dynamic Route with Slug\n\nUse this pattern **only** when the product is determined dynamically from the URL — for example, a dynamic page that can render any product by its URL slug. The folder name uses Next.js dynamic route syntax where brackets are literal in the folder name (the folder is literally named `[slug]`), and Next.js supplies the URL segment through the page's asynchronous `params`. For example, visiting `/products/cool-sneakers` yields `slug: \"cool-sneakers\"` after awaiting `params`. Pass that value to the loader as `handle` (Shopify resolves products by handle); the `[slug]` segment name is the standard dynamic-route shape shared with Replo Products.\n\n**Server component** (`app/products/[slug]/page.tsx`):\n\n```tsx\nimport { ProductDetail } from \"@/app/components/ProductDetail\";\nimport { DATA_LOADER_KEYS } from \"@replohq/sdk/loaders/loader-keys\";\nimport { PrefetchedLoaders } from \"@replohq/sdk/loaders/prefetch-loaders\";\n\nexport default async function ProductPage({ params }: { params: Promise<{ slug: string }> }) {\n  const { slug } = await params;\n  return (\n    <PrefetchedLoaders\n      queries={[\n        {\n          loaderKey: DATA_LOADER_KEYS.SHOPIFY_PRODUCT,\n          args: { handle: slug },\n        },\n      ]}\n    >\n      <ProductDetail handle={slug} />\n    </PrefetchedLoaders>\n  );\n}\n```\n\n**Client component** (`app/components/ProductDetail.tsx`):\n\n```tsx\n\"use client\";\n\nimport { DATA_LOADER_KEYS } from \"@replohq/sdk/loaders/loader-keys\";\nimport { ProductLoader } from \"@replohq/sdk/loaders/product-loader\";\n\nexport function ProductDetail({ handle }: { handle: string }) {\n  return (\n    <ProductLoader\n      loaderKey={DATA_LOADER_KEYS.SHOPIFY_PRODUCT}\n      handle={handle}\n      fallback={<div>Loading product…</div>}\n    >\n      {(product) => (\n        <div>\n          <h1>{product.title}</h1>\n          {/* ... render product details ... */}\n        </div>\n      )}\n    </ProductLoader>\n  );\n}\n```\n\n**Previewing and verifying a dynamic route:** the literal bracket URL (`/products/[slug]`) is not a real page, and a dev server may substitute sample data for it — seeing \"Sample product\" there is expected and does not prove real data works. Always verify with a concrete URL built from a real handle (from the connected store or a listing-page link), never the bracket route.\n\n### Key rules for the pattern\n\n- `PrefetchedLoaders` derives query keys internally as `[loaderKey, args]`, matching what loader components use. You only need to provide `loaderKey` and `args`.\n- Every loader component renders via a **render-prop** (`children` is a function receiving typed data).\n- Loader components use React Query hooks → they **must** live in `\"use client\"` files. Never pass a render-prop function from a server component.\n- `PrefetchedLoaders` is a **server component** → import it directly from `@replohq/sdk/loaders/prefetch-loaders`, never through a barrel file that also exports client code.\n- Provide a `fallback` prop for the loading state that displays while data is being fetched on client-side transitions.\n\n### Collection Grids (CollectionProductsLoader)\n\n`CollectionProductsLoader` returns each product with enough data to render variant-aware cards without a per-product fan-out. For every product you get:\n\n- `minPrice` and `maxPrice` — render `$X` or `$X – $Y` price ranges.\n- `compareAtMinPrice` and `compareAtMaxPrice` — render strike-through pricing. Both fields are always present; Storefront returns `amount: \"0.0\"` when there is no compare-at price, so check `Number(compareAtMinPrice.amount) > 0` before rendering.\n- `options` and `options[].optionValues` (with swatch color / image) — render variant pickers and swatches on the card.\n- `variants` — per-variant `availableForSale`, `sku`, `price`, `compareAtPrice`, `image`, `selectedOptions`. Drive sold-out states and add-to-cart buttons from these.\n\nThe product-level `availableForSale` is `true` whenever **any** variant is purchasable, so always inspect `variants[i].availableForSale` for accurate sold-out UI.\n\nVariant lists are capped at the first 100 variants per product; products with more variants will trigger a server-side warning and the extras will be omitted. Use `ProductLoader` for products that need every variant.\n\n```tsx\n\"use client\";\n\nimport { CollectionProductsLoader } from \"@replohq/sdk/loaders/collection-products-loader\";\nimport { DATA_LOADER_KEYS } from \"@replohq/sdk/loaders/loader-keys\";\n\nexport function CollectionGrid({ collectionGid }: { collectionGid: string }) {\n  return (\n    <CollectionProductsLoader\n      loaderKey={DATA_LOADER_KEYS.SHOPIFY_COLLECTION_PRODUCTS}\n      collectionGid={collectionGid}\n      first={24}\n      fallback={<div>Loading collection…</div>}\n    >\n      {({ products }) => (\n        <ul>\n          {products.map((product) => {\n            const firstAvailableVariant =\n              product.variants?.find((variant) => variant.availableForSale) ??\n              product.variants?.[0];\n            const hasCompareAt =\n              product.compareAtMinPrice != null &&\n              Number(product.compareAtMinPrice.amount) > 0;\n            const priceLabel =\n              product.minPrice &&\n              product.maxPrice &&\n              product.minPrice.amount !== product.maxPrice.amount\n                ? `${product.minPrice.amount} – ${product.maxPrice.amount}`\n                : product.minPrice?.amount;\n            return (\n              <li key={product.id}>\n                <h3>{product.title}</h3>\n                <p>\n                  {hasCompareAt && <s>{product.compareAtMinPrice!.amount}</s>}{\" \"}\n                  {priceLabel}\n                </p>\n                {firstAvailableVariant == null && <span>Sold out</span>}\n              </li>\n            );\n          })}\n        </ul>\n      )}\n    </CollectionProductsLoader>\n  );\n}\n```\n\n## Unified Object Types\n\nProduct, Collection, and Variant are **canonical types** shared across the entire system. The same `Product` type is returned by search tools (sparsely populated) and data loaders (fully populated). This means you never need to convert between different representations.\n\nA Replo session can report the available object types for the connected store. Each entry provides:\n\n- `objectTypeKey` — canonical type name (e.g., `\"product\"`)\n- `tags` — categories used for browsing and grouping objects\n- `searchToolKey` — which tool to call to search for instances\n- `suggestedLoaderKeys` — which loaders to use for rendering\n\n### Completeness: partial vs full\n\nEvery tool and loader declares a `completeness` level:\n\n- **`\"partial\"`** — returns a subset of canonical fields. Check `guaranteedFields` to know which are present. Other fields may be absent because they were not fetched, not because they don't exist.\n- **`\"full\"`** — returns all canonical fields. If a field is absent or null, the underlying object genuinely lacks that data.\n\nSearch tools are `completeness: \"partial\"` (they return id, title, handle, image). The `products.get` tool and data loaders are `completeness: \"full\"`.\n\n### Getting full product details in conversation\n\nWhen the user asks about variant information, option names, pricing, or other details not available from search, use the public product lookup operation with the product's GID. This returns a full `Product` object (including `variants`, `options`, `descriptionHtml`) without needing to involve data loaders or page code. Limit this to one product at a time to avoid context explosion.\n\nExample flow:\n\n1. Search the catalog for \"hoodie\" with the public product search operation → partial results with id, title, handle, image.\n2. User asks \"what variants does this have?\" → the public product lookup operation by GID → full product with variants and options.\n\n### Discovery-then-load pattern (for page rendering)\n\n1. Use a search tool to find the right object (returns partial data, including `id` which is the GID).\n2. Use `ProductLoader` with `loaderKey={DATA_LOADER_KEYS.SHOPIFY_PRODUCT}` and `productId={product.id}` from the search result to render full product data on the page.\n\n## Discount Codes\n\nDiscount codes are the promo codes a customer types at checkout (e.g. `SUMMER20`). Two read-only tools expose them; both use the Admin API and cover code-based discounts only (not automatic discounts).\n\n- the public discount search operation — list or search discounts. Call with **no `query`** to list every discount code. Pass a keyword or a field qualifier like `status:active`, `status:expired`, `status:scheduled`, or `title:summer` to filter. Returns partial `DiscountCode` objects (`id`, `codes`, `title`, `status`, `summary`). Never pass placeholders like `\"all\"` or `\"*\"` — they are treated as literal search terms.\n- the public discount lookup operation — full detail for one discount. Look it up by its GID (`discountGid`, the `id` from search) **or** by the exact `code` string a customer would enter. Provide exactly one. Returns the codes, status, human-readable `summary`, discount type, start/end dates, usage limit, times used, and whether it applies once per customer.\n\nThese are for **discovering and inspecting** discount codes in conversation. Applying a code to a cart is a separate, storefront-side concern handled by `useCart().updateDiscountCodes` in page code — see **Cart Operations**.\n\n## Metafields\n\nMetafields hold a store's custom data — ingredients, size charts, care instructions, spec tables. Two separate things are involved and confusing them is the most common mistake:\n\n- A **definition** is the store-wide declaration that a metafield exists (namespace, key, type). Read with the public metafield-definitions list operation.\n- A **value** is what a specific product/variant/collection actually stores in it. Read with the public metafield-value read operation, or inline via the public product lookup operation / the public collection lookup operation.\n\n### Always read a value before rendering one\n\nA definition's `type` does not tell you the shape of the data. `rich_text_field` is a JSON document, `list.product_reference` is a JSON array of GID strings, `dimension` is `{\"value\":5,\"unit\":\"cm\"}`. **Never write rendering code from the type name alone** — fetch a real value first and look at it.\n\n```\nPublic metafield-definitions list: ownerType = product\n  → { namespace: \"custom\", key: \"ingredients\", type: { name: \"list.single_line_text_field\" }, storefrontAccess: \"PUBLIC_READ\" }\n\nPublic metafield-value read: owner GID = gid://shopify/Product/123,\n  namespace = custom, key = ingredients\n  → { metafields: [{ namespace: \"custom\", key: \"ingredients\", type: \"list.single_line_text_field\", value: \"[\\\"Cocoa\\\",\\\"Sugar\\\"]\" }] }\n```\n\n`value` is **always a string**, regardless of type. Parse it according to `type`.\n\nFor `file_reference` metafields, `value` remains Shopify's resource GID — never\npass it to an image or download URL. The resolved asset arrives on `reference`,\na discriminated union you switch on:\n\n| `reference` | Shape | Render as |\n| --- | --- | --- |\n| image | `{ kind: \"image\", url }` | `<img src={url}>` |\n| file | `{ kind: \"file\", url }` | a download link |\n| video | `{ kind: \"video\", url, previewUrl }` | `<video src={url} poster={previewUrl}>` |\n\n`reference` is `null` when the metafield holds no reference (any non-file type)\nor points at a kind we do not resolve, such as `Model3d`. `url` is `null` when\nShopify withholds the asset, so handle that without discarding the raw\nmetafield. Only the video variant has `previewUrl`.\n\n```tsx\nif (metafield.reference?.kind === \"image\" && metafield.reference.url) {\n  return <img src={metafield.reference.url} alt=\"\" />;\n}\n```\n\n### Which tool to use\n\n| Need                                                      | Tool                                                                                                       |\n| --------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |\n| What metafields exist on this store?                      | public metafield-definitions list operation for that `ownerType` — `ownerType` is `product`, `variant`, or `collection` |\n| Values for a product, alongside its other data            | public product lookup with metafield identifiers                                               |\n| Values for a collection, alongside its other data         | public collection lookup with metafield identifiers                                         |\n| Values for a **variant**, or just metafields on their own | public metafield-value read by owner GID                                                        |\n\nOmit `metafieldIdentifiers` / `identifiers` to get every metafield on the owner (capped at 50). Pass them to fetch a targeted subset — do this once you know which keys you want, since it is cheaper and keeps responses small.\n\nVariant metafields are deliberately **not** returned by the public product lookup operation. A product can have 100 variants, and fetching metafields for all of them in one query exceeds Shopify's query cost limit. Use the public metafield-value read operation with a variant GID instead.\n\n### `storefrontAccess` — read this before promising the user a metafield on a page\n\nEach definition returns `storefrontAccess`, which is either `\"PUBLIC_READ\"` or `\"NONE\"`. It defaults to `NONE` in Shopify.\n\nThese tools use Shopify's **Admin** API, but pages render through the **Storefront** API. A definition with `storefrontAccess: \"NONE\"` is fully readable by these tools and **invisible to a page**. If you build a page around a `NONE` metafield, it will render empty with no error.\n\nIf the user wants a `NONE` metafield on a page, tell them it must first be exposed to the Storefront API in Shopify admin (Settings → Custom data → the definition → enable Storefront API access). Do not silently build against it.\n\n### Rendering metafields on a page\n\n`ProductLoader` takes `productMetafieldIdentifiers` and `variantMetafieldIdentifiers`. Pass the **same identifiers, in the same order**, to both `PrefetchedLoaders` and the loader component — the prefetch cache is keyed on the args, so a mismatch silently refetches on the client without metafields.\n\nProduct metafields arrive on `product.metafields`; variant metafields on `product.variants[N].metafields`. Both are `{ namespace, key, type, value, reference? }`, and `value` is always a string. For `file_reference`, switch on `reference.kind` before rendering (see above) — handle a null `reference` or `url` without discarding the raw metafield.\n\n```tsx\n// app/products/[handle]/page.tsx  (server component)\nimport { DATA_LOADER_KEYS } from \"@replohq/sdk/loaders/loader-keys\";\nimport { PrefetchedLoaders } from \"@replohq/sdk/loaders/prefetch-loaders\";\n\nimport { ProductDetail } from \"./product-detail\";\n\nconst PRODUCT_METAFIELDS = [{ namespace: \"custom\", key: \"ingredients\" }];\n\nexport default async function Page({\n  params,\n}: {\n  params: Promise<{ handle: string }>;\n}) {\n  const { handle } = await params;\n  return (\n    <PrefetchedLoaders\n      queries={[\n        {\n          loaderKey: DATA_LOADER_KEYS.SHOPIFY_PRODUCT,\n          args: { handle, productMetafieldIdentifiers: PRODUCT_METAFIELDS },\n        },\n      ]}\n    >\n      <ProductDetail handle={handle} />\n    </PrefetchedLoaders>\n  );\n}\n```\n\n```tsx\n// app/products/[handle]/product-detail.tsx  (client component)\n\"use client\";\n\nimport { DATA_LOADER_KEYS } from \"@replohq/sdk/loaders/loader-keys\";\nimport { ProductLoader } from \"@replohq/sdk/loaders/product-loader\";\n\nconst PRODUCT_METAFIELDS = [{ namespace: \"custom\", key: \"ingredients\" }];\n\nexport function ProductDetail({ handle }: { handle: string }) {\n  return (\n    <ProductLoader\n      loaderKey={DATA_LOADER_KEYS.SHOPIFY_PRODUCT}\n      handle={handle}\n      productMetafieldIdentifiers={PRODUCT_METAFIELDS}\n    >\n      {(product) => {\n        const ingredients = product.metafields.find(\n          (metafield) =>\n            metafield.namespace === \"custom\" && metafield.key === \"ingredients\",\n        );\n        if (!ingredients) {\n          return null;\n        }\n        // `list.single_line_text_field` serializes as a JSON array of strings.\n        const items: string[] = JSON.parse(ingredients.value);\n        return (\n          <ul>\n            {items.map((item) => (\n              <li key={item}>{item}</li>\n            ))}\n          </ul>\n        );\n      }}\n    </ProductLoader>\n  );\n}\n```\n\nRules specific to metafields on pages:\n\n- **Check `storefrontAccess` first.** Loaders read through the Storefront API. A definition with `storefrontAccess: \"NONE\"` returns nothing here no matter what you pass. Confirm it is `PUBLIC_READ` before building.\n- **Always handle the missing case.** A metafield that is defined but unset on a given product is simply absent from the array — that is normal, not an error.\n- **Parse according to `type`.** Read a real value with the public metafield-value read operation first so you know the shape you are parsing.\n\n`CollectionLoader` does **not** accept metafield identifiers yet — collection metafields are readable with the public collection lookup operation in conversation, but cannot be rendered on a page.\n\n## Metaobjects\n\nMetaobjects are how merchants model **reusable structured content** that is not a product or collection — size charts, ingredient panels, care instructions, FAQ blocks, brand modules. Where a metafield hangs a single custom value off an existing product, a metaobject is a standalone record with its own fields, reusable across many products.\n\nSame definition/value split as metafields, with one hard constraint on top: **Shopify has no query that lists metaobjects across types.** Every read needs a `type` handle, and the only way to learn what types a store has is the public metaobject-definitions list operation. Always start there.\n\n```\nPublic metaobject-definitions list\n  → { type: \"size_chart\", displayNameKey: \"title\", storefrontAccess: \"PUBLIC_READ\",\n      fieldDefinitions: [{ key: \"title\", type: \"single_line_text_field\", required: true },\n                         { key: \"chart\", type: \"json\", required: false }] }\n\nPublic metaobject list: type = size_chart\n  → { metaobjects: [{ handle: \"hoodie-sizing\", displayName: \"Hoodie Sizing\",\n                      fields: [{ key: \"title\", type: \"single_line_text_field\", value: \"Hoodie\" }] }] }\n```\n\nAs with metafields, `value` is **always a string** — parse it according to `type`, and read a real entry before writing rendering code.\n\n### Which tool to use\n\n| Need                                              | Tool                                                       |\n| ------------------------------------------------- | ------------------------------------------------------------ |\n| What metaobject types does this store have?       | public metaobject-definitions list operation                  |\n| All entries of one type                           | public metaobject list operation with that `type`                       |\n| One entry, by GID or by type + handle             | public metaobject lookup by GID, or by type + handle |\n\n### Resolving a `metaobject_reference` metafield\n\nA metafield whose type is `metaobject_reference` stores a bare metaobject GID as its string value; `list.metaobject_reference` stores a JSON array of them. Neither tells you what is inside. Resolve each GID with the public metaobject lookup operation.\n\n### Two gates before a metaobject can render on a page\n\nThese tools use the **Admin** API; pages render through the **Storefront** API. A metaobject readable here can still come back empty at render time, for two separate reasons:\n\n1. **The definition's storefront access.** Same rule as metafields — `storefrontAccess` must be `\"PUBLIC_READ\"`, and it defaults to `NONE`. Check it before building.\n2. **Draft entries.** If the definition has the `publishable` capability, the Storefront API only returns entries that are `ACTIVE`. A newly created entry defaults to `DRAFT`, so an entry visible to these tools may be missing from the page.\n\nNeither failure raises an error — the loader just reports the entry as not found. If either gate is closed, tell the user what to change in Shopify admin (Settings → Custom data → the metaobject definition) rather than silently building against it.\n\n### Rendering metaobjects on a page\n\n`MetaobjectLoader` (loaderKey `shopify_storefront.metaobject`) takes `type` + `handle`, or `id`. `MetaobjectsLoader` (loaderKey `shopify_storefront.metaobjects`) takes a `type` and paginates. Both follow the same prefetch + loader-component pattern as `ProductLoader` — see **Data Loading Pattern**.\n\nStorefront metaobjects have no `displayName` (that is Admin-only), and `fields` only includes keys that have a value — an absent key means null, not an error.\n\n## Usage Rules\n\n- **Always prefer the prefetch pattern** (server component with `PrefetchedLoaders` wrapping a client loader component). Only skip prefetching when the data is entirely driven by client-side interaction with no server-known initial value.\n- **Default to ID-based loading** (`productId`) when you know which product to display. Only use handle-based loading for dynamic routes where the product is determined by the URL.\n- Use public Shopify read operations for catalog discovery in conversation. Admin searches return partial Product/Collection objects with `id` (GID), `title`, `handle`, and `featuredImage`.\n- Use the public product lookup operation when you need full product details (variants, options, pricing) for conversational answers — not for page rendering.\n- Never write code that renders a metafield without first reading an actual value with the public metafield-value read operation — the type name does not tell you the shape of the data. See **Metafields**.\n- For structured content that is not a product or collection (size charts, ingredient lists, FAQ blocks), check the public metaobject-definitions list operation before telling the user the data is not available. See **Metaobjects**.\n- Use Storefront loaders for rendering page data (`ProductLoader`, `CollectionLoader`, `CollectionProductsLoader` with the appropriate `loaderKey`). These return full objects.\n- For cart operations, use `product.variants[N].id` as the merchandiseId — this is the same Shopify variant GID returned by loaders.\n- For dynamic pages, read route params with `useParams()` or the page `params` prop and pass them into loader component props.\n- Treat `availableForSale` as availability; do not infer inventory quantities. Product-level `availableForSale` is `true` if **any** variant is purchasable, so use the per-variant `availableForSale` on `product.variants[]` (returned by both `ProductLoader` and `CollectionProductsLoader`) for accurate sold-out UI.\n\n## Cart Operations\n\n### Architecture\n\nCart operations go through the **dependency-resolver** (server-to-server). The scaffold never calls Shopify directly for cart mutations. This means:\n\n- The site has **no Shopify Storefront token** — there is no `SHOPIFY_STOREFRONT_ACCESS_TOKEN` / `SHOPIFY_URL` in `wrangler.jsonc` or the worker env, and a route that calls `*.myshopify.com/api/*/graphql.json` directly will fail on the published site. Every cart mutation you need is on `useCart` below; if one is genuinely missing, use `report_bug` instead of calling Shopify yourself.\n- Cart hooks call server actions, which call the resolver, which calls Shopify.\n- Cart state is persisted via a cookie (`replo_cart_id`) containing only the Shopify cart GID.\n- Markets currency is not automatic. Pass the same country you use on product loaders to `ReploProvider` as `markets={{ shopify: { country } }}` so cart create and checkout use that market. See [shopify-markets.md](references/shopify-markets.md).\n\n### Hooks\n\nImport cart hooks from `@replohq/sdk/cart/hooks/`:\n\n- **`useAddToCart`** — adds items to the cart using merchandise IDs from loader output. Returns `{ addToCart, isAdding }`; `addToCart` never throws and resolves `{ adjustments, error }` — `adjustments` lists what the server changed about a saved add (a quantity clamped to stock, an unavailable line removed), `error` is set when the cart can't be created or saved.\n- **`useBuyNow`** — creates a cart and redirects directly to checkout (skips the cart UI).\n- **`useCart`** — cart UI state and order-level writes: `itemsCount`, `isCartOpen`, `openCart`, `closeCart`, `updateDiscountCodes`, `updateNote`, `replaceAttributes`, plus `error` / `clearError` for the last cart change that failed to save.\n  - `updateNote(note)` sets the Shopify cart note (the order's \"special instructions\"); pass `\"\"` to clear it. Each call is one Shopify mutation: save on blur or after the shopper stops typing, never on every `onChange`.\n  - `replaceAttributes(attributes)` replaces the cart-level attributes (`[{ key, value }]`), the fields gift-message and fulfillment-routing apps (Giftship-style \"gift\" / \"Separate Gifts\" flags) read off the order. The full list is replaced each call, so a key left out is removed; values are stored as strings. Per-line properties still go on `addToCart` items.\n  - Both are Shopify-checkout only. On a Replo Products (Stripe) checkout the SDK keeps the note and attributes on the cart but does not forward them to the order, so do not build gift-note UI on those hooks there.\n\n### Add to Cart\n\nUse `useAddToCart` with the variant's `id` from a data loader. The variant `id` is the Shopify variant GID — pass it directly as `merchandiseId`. The hook auto-resolves product display data (title, price, image) from the React Query cache for optimistic cart UI — no need to pass it manually. Cache resolution works for both single-product loaders (`ProductLoader`) and product-list loaders (`CollectionProductsLoader`, `RebuyRecommendationsLoader`).\n\n```tsx\n\"use client\";\n\nimport { useAddToCart } from \"@replohq/sdk/cart/hooks/use-add-to-cart\";\nimport { DATA_LOADER_KEYS } from \"@replohq/sdk/loaders/loader-keys\";\nimport { ProductLoader } from \"@replohq/sdk/loaders/product-loader\";\n\nfunction ProductCard({ productId }: { productId: string }) {\n  const { addToCart, isAdding } = useAddToCart();\n\n  return (\n    <ProductLoader\n      loaderKey={DATA_LOADER_KEYS.SHOPIFY_PRODUCT}\n      productId={productId}\n      fallback={<div>Loading…</div>}\n    >\n      {(product) => {\n        const firstVariant = product.variants[0];\n        // Pre-order and subscription-only products need a plan on every line.\n        const sellingPlanId = product.requiresSellingPlan ? (firstVariant?.sellingPlanIds[0] ?? null) : null;\n        return (\n          <div>\n            <h2>{product.title}</h2>\n            <button\n              disabled={!firstVariant?.availableForSale || isAdding}\n              onClick={() => {\n                if (firstVariant) {\n                  void addToCart([{ merchandiseId: firstVariant.id, quantity: 1, sellingPlanId }]);\n                }\n              }}\n            >\n              {isAdding ? \"Adding…\" : \"Add to Cart\"}\n            </button>\n          </div>\n        );\n      }}\n    </ProductLoader>\n  );\n}\n```\n\n**Failures are handled by the SDK, not the button.** When the cart can't be created or saved, `addToCart` resolves with `error` set, the provider stores the same shopper-ready message on `useCart().error`, and the slide-out cart opens (or stays open) so the shopper sees it. The slide-out cart must render `useCart().error` — see Cart UI below. Do not wrap `addToCart` in `try/catch` or render a per-button error; read the result only when the caller needs `adjustments` or wants to branch on `error`.\n\n### Subscriptions and pre-orders (selling plans)\n\nSupported natively when the checkout provider is `shopify` (on `replo` the plan is dropped at checkout, so switch the provider first). Subscription apps (Skio, Recharge, Loop, …) and pre-order apps (Timesact, PreProduct, …) create Shopify selling plans, which the loaders and cart carry through to Shopify Checkout. Never tell the user subscriptions or pre-orders can't be sold from a Replo page, and never route subscribe or pre-order traffic to the Shopify PDP.\n\n- `product.sellingPlanGroups[].sellingPlans[]` lists the plans; `variant.sellingPlanIds` lists which of them the variant is eligible for.\n- `product.requiresSellingPlan` is `true` when Shopify sells the product only with a plan (pre-orders, subscription-only products). Shopify rejects any line for it without a `sellingPlanId`, so there is no one-time purchase: select the first eligible plan by default and send a plan with every add.\n- Pass `sellingPlanId` on the line to `addToCart` / `buyNow`; when a plan is optional, pass `null` for one-time purchase. The saved line carries `sellingPlanAllocation` with the adjusted price.\n\n```tsx\nconst plans = product.sellingPlanGroups.flatMap((group) =>\n  group.sellingPlans.filter((plan) => variant.sellingPlanIds.includes(plan.id)),\n);\nconst sellingPlanId = product.requiresSellingPlan ? (selectedPlanId ?? plans[0]?.id ?? null) : selectedPlanId;\nvoid addToCart([{ merchandiseId: variant.id, quantity: 1, sellingPlanId }]);\n```\n\nSelector UX rules live in the `product-display` skill.\n\n### Bundle apps (Kaching Bundles)\n\nWhen the user mentions Kaching, bundle deals, volume discounts, or quantity breaks, call `shopify_apps_detect` first. If it reports Kaching Bundles as detected, render the deal with `KachingBundlesLoader` (loaderKey `shopify_storefront.kaching_bundles`) instead of hand-building quantity cards: it mounts Kaching's own widget, styled from the Kaching app, and renders your `children` under it with the shopper's current pick as cart lines that already carry the Kaching line attributes. Pass those lines to `addToCart`; Kaching applies the discount at Shopify Checkout, and the cart shows it as a discount allocation. Requires the `shopify` checkout provider.\n\n```tsx\nimport { KachingBundlesLoader } from \"@replohq/sdk/loaders/kaching-bundles-loader\";\n\n<KachingBundlesLoader\n  loaderKey={DATA_LOADER_KEYS.SHOPIFY_KACHING_BUNDLES}\n  productId={product.id}\n  selectedVariantId={selectedVariant.id}\n  noDealFallback={<QuantitySelector />}\n>\n  {({ lines }) => (\n    <button disabled={lines.length === 0} onClick={() => void addToCart(lines)}>\n      Add to cart\n    </button>\n  )}\n</KachingBundlesLoader>\n```\n\nThe lines belong to the loader instance for that product, so put the button inside `children`, not in your own state. For Buy Now, map each line to `{ variantId: line.merchandiseId, quantity, sellingPlanId, properties }`; `buyNow` keeps `properties`, and dropping them loses the discount. `noDealFallback` holds your normal buy box for products Kaching has no deal for. Kaching's fetch reports a Storefront failure as \"no deal\", so a transient error shows the fallback for up to a minute rather than an error.\n\nNever add Kaching's theme snippet or `<kaching-bundle>` element as a page script, and do not re-implement the discount in page code. If Kaching is not installed, build the buy box normally.\n\n### Add to Cart from a Collection Grid\n\nWhen wiring an Add to Cart button inside a `CollectionProductsLoader` grid, see [add-to-cart-from-collection-grid.md](references/add-to-cart-from-collection-grid.md). Key rule: pass the variant GID (`product.variants[N].id`), never `product.id`.\n\n### Buy Now\n\nUse `useBuyNow` for express checkout. Pass variant IDs the same way, plus the selected `sellingPlanId`, which a product with `requiresSellingPlan` always needs (`null` for a one-time purchase).\n\n```tsx\n\"use client\";\n\nimport { useBuyNow } from \"@replohq/sdk/cart/hooks/use-buy-now\";\n\nfunction BuyNowButton({ variantId, sellingPlanId }: { variantId: string; sellingPlanId: string | null }) {\n  const { buyNow } = useBuyNow();\n\n  return (\n    <button\n      onClick={() => {\n        buyNow([{ variantId, quantity: 1, sellingPlanId }]);\n      }}\n    >\n      Buy Now\n    </button>\n  );\n}\n```\n\n`useBuyNow` creates a Shopify cart and redirects to **Shopify Checkout**. Wallet options (Shop Pay, Apple Pay, Google Pay) can appear **inside** that hosted checkout. `useBuyNow` is **not** an on-page \"Buy with Shop Pay\" button in itself.\n\n### Checkout provider\n\nConnecting Shopify gives the project Shopify **data**. It does **not** decide what powers checkout; that is the project's `checkoutProvider`, read with `get_project`:\n\n- `shopify` — the cart is a Shopify cart and Checkout sends shoppers to Shopify Checkout. `useBuyNow` and the cart's Checkout link only reach Shopify Checkout in this state.\n- `replo` — Replo checkout. Shopify products cannot be bought this way.\n- `null` — checkout is off.\n\n**Before promising Shopify checkout, or when Checkout fails on a Shopify-connected site, check the provider first.** A project still on `replo` after connecting Shopify is the usual cause: the store is connected but shoppers cannot check out yet. Tell the user plainly, then offer to switch. Once they agree, call `update_project` with `checkoutProvider: \"shopify\"`, and publish the site again afterwards because publish bakes the provider into the site. Switching changes the Checkout button on every published site of the project, so never switch without the user's explicit yes. The call is refused when Shopify is not connected, or when switching to `replo` before the project's payments onboarding is complete.\n\n**Checkout 404s on the site's own domain** (`/cart/c/…`): Shopify runs checkout on its primary domain, and that domain points at the Replo site. Have the user change the primary in Shopify admin (Settings → Domains) back to their `myshopify.com` address or a subdomain that points at Shopify. Site code can't fix it.\n\n### Shop Pay (on-page Buy with Shop Pay)\n\nWhen the user asks for Shop Pay / Buy with Shop Pay on the page (Shopify-checkout projects only), see [shop-pay-button.md](references/shop-pay-button.md). Do not hardcode it onto every PDP by default. Do not confuse it with `useBuyNow`.\n\n### Cart UI\n\n**Do not assume a slide-out cart component exists — verify it, and build it if it's missing.** Cart _context_ is always available (any template wrapped in `ReploProvider` gives every page `useCart()` / `useAddToCart` / `useBuyNow`), but the visual cart is an ordinary component of the site — in `components/cart/`, so it is not a surfaced Site Component — that only some templates ship. The blank starter includes it; branded and other templates may not. Before wiring any \"Add to Cart\" button or cart icon to `openCart()`, confirm the component actually exists.\n\n**1. Check for the cart component and its render:**\n\n- Look for the component file at `@/components/cart/slideout-cart.tsx` (conventional location).\n- Confirm it is rendered once in the root layout — a `<SlideoutCart />` inside `ReploProvider` in `app/layout.tsx`.\n\n**2. If it exists,** use it as-is via `openCart()` (see below). Do not render it again in individual pages.\n\n**3. If it is missing, build it and wire it into the layout yourself:**\n\n- Create a client component (`\"use client\"`) at `@/components/cart/slideout-cart.tsx` that reads UI state from `useCart()` (`isCartOpen`, `openCart`, `closeCart`, `itemsCount`) plus the cart lines, and renders a right-side drawer with an overlay backdrop.\n- Render it exactly once inside `ReploProvider` in `app/layout.tsx` (e.g. right after `{children}`), never per-page.\n- If the layout has no `ReploProvider` at all (some plain templates), the cart hooks won't work until that runtime layer exists — add `ReploProvider` from `@replohq/sdk/providers/replo-provider` (wrapping `SandboxRuntime` from `@replohq/sandbox-runtime`) around the app first.\n- Use the cart display utilities (`getCartLinePricing`, `getCartLineBundleInfo`, `getCartLineSpecialInfo`, `getCartLineSubscriptionInfo`) from `@replohq/sdk/cart/utils/variant-to-cart-line` for line-item pricing and badges.\n\nA complete slide-out cart handles:\n\n- Line item display with images, options, and pricing\n- Quantity controls (increment/decrement/remove)\n- Discount code application and removal\n- Bundle and subscription badge display\n- Subtotal, discounts, and total calculation\n- Checkout link\n- The last change that failed to save: `useCart().error` carries a shopper-ready `message` (set when an add, quantity change, removal, or discount can't be saved; cleared when the next change starts). Render it near the top of the cart with the line items still visible, and call `clearError()` from a dismiss control.\n\nTo open the cart from any component (e.g., an \"Add to Cart\" button or a cart icon in a header):\n\n```tsx\n\"use client\";\n\nimport { useCart } from \"@replohq/sdk/cart/cart-provider\";\n\nfunction CartButton() {\n  const { itemsCount, openCart } = useCart();\n  return <button onClick={openCart}>Cart ({itemsCount})</button>;\n}\n```\n\nThe slide-out cart opens from the right side of the viewport with an overlay backdrop. Clicking outside the cart or pressing the X button closes it.\n\n**Customizing the cart UI:** The `SlideoutCart` component is a starting point. You can freely modify its styling (it uses standard Tailwind classes), add sections, or restructure it. The cart display utilities used by the component (`getCartLinePricing`, `getCartLineBundleInfo`, `getCartLineSpecialInfo`, `getCartLineSubscriptionInfo`) are imported from `@replohq/sdk/cart/utils/variant-to-cart-line` and can be used in any custom cart UI.\n\n### Cart Rules\n\n- **Never call resolver cart endpoints directly from client code.** Always use the cart hooks which go through server actions.\n- The `merchandiseId` for add-to-cart is `product.variants[N].id` — the Shopify variant GID from loader output. No ID conversion needed.\n- `useCart()` provides UI state only. For mutations use `useAddToCart` (add items) or `useBuyNow` (express checkout).\n- On-page **Buy with Shop Pay** is separate from cart hooks — see [shop-pay-button.md](references/shop-pay-button.md). Do not confuse it with `useBuyNow`.\n- `ReploProvider` in the root layout provides cart context to every page. Cart _hooks_ work automatically wherever it is present; the cart _UI component_ does not — verify it exists (and build it if not) before wiring buttons to `openCart()`.\n- The `SlideoutCart` (when present, or once you've added it) is rendered once in the root layout — do not render it again in individual pages.\n"
}

SHA-256 of public snapshot: da9bdb0a8e7652abcb3978119da87094d8a95bf261080e1031f4e0ad689a8d59