Ir para o conteúdo
decodecodeveloper docs
Storefront → Blocks → Apps

Shopify

Connect a site to the Shopify Storefront API for product pages, listings, shelves, carts and customer sign-in.

Esta página ainda não foi traduzida para o português — o conteúdo abaixo está em inglês.

@decocms/apps-shopify connects a site to Shopify through the Storefront API, Shopify's GraphQL API for custom storefronts. It provides loaders for product pages, listing and search pages, shelves, related products and shop details, all returning the shared commerce types, plus cart and customer actions that keep their state in cookies.

It's a smaller app than VTEX: it runs on the server only and ships no React hooks, no request middleware, no checkout proxy and no response cache. Your site calls its actions from its own server code.

bun add @decocms/apps-shopify @decocms/apps-commerce @decocms/apps-website

Configuring

Editors configure the app in a deco-shopify block:

FieldWhat it does
storeNameRequired. Your store's subdomain: acme for acme.myshopify.com.
storefrontAccessTokenRequired. A Storefront API access token, as plain text or an encrypted secret. Falls back to the SHOPIFY_STOREFRONT_TOKEN environment variable.
publicUrlOptional. Your storefront's public URL.

configure returns null, and the app isn't installed, when storeName or the token is missing. Install it with the registry entry, passing the module statically (see Apps):

src/setup/apps.ts
import { autoconfigApps, type AppRegistry } from "@decocms/blocks-admin/apps";
import { loadBlocks } from "@decocms/blocks/cms";
import { SHOPIFY_REGISTRY_ENTRY } from "@decocms/apps-shopify/registry";
import * as shopifyMod from "@decocms/apps-shopify/mod";
 
const APP_REGISTRY: AppRegistry = [{ ...SHOPIFY_REGISTRY_ENTRY, module: async () => shopifyMod }];
 
await autoconfigApps(loadBlocks(), APP_REGISTRY);

You can also configure by hand with configureShopify({ storeName, storefrontAccessToken, publicUrl }) from the package root, or initShopifyFromBlocks(blocks) in createSiteSetup's initPlatform. initShopifyFromBlocks only accepts a token stored as plain text and configures once per process; use autoconfig when the token is encrypted or lives in the environment.

The app talks to https://<storeName>.myshopify.com/api/2025-04/graphql.json.

Instrumented fetch

Call setShopifyFetch(createShopifyFetch()) once, at module scope in your setup:

src/setup.ts
import { setShopifyFetch, createShopifyFetch } from "@decocms/apps-shopify";
 
setShopifyFetch(createShopifyFetch());

Every GraphQL call is then measured and traced, with the span named after the GraphQL operation in the query (such as shopify.GetProduct). Without it, calls use a plain fetch with a timeout and aren't measured. The order doesn't matter: setting the fetch after the app is configured rebuilds its client. See Observability.

Loaders

When the app is installed, its loaders are registered under these keys, which content refers to with __resolveType:

KeyReturnsProps
shopify/loaders/ProductDetailsPageProductDetailsPageslug, metafields?
shopify/loaders/ProductListingPageProductListingPagequery?, collectionName?, count (12), page?, pageOffset? (1), startCursor?, endCursor?, pageHref?, metafields?
shopify/loaders/ProductListProduct[]props: either { query, count, sort? } or { collection, count, sort? }; plus filters? (tags, product types, vendors, price range, variant options) and metafields?
shopify/loaders/RelatedProductsProduct[]slug, count? (10)
shopify/loaders/shopShopmetafields?

Older content may refer to them with a .ts suffix (shopify/loaders/ProductDetailsPage.ts). That still resolves: when an exact key isn't registered, the resolver tries the key without its extension.

A few rules worth knowing:

  • Product slugs. The product page loader treats the last - segment of slug as a variant id when it's a number; the rest is the product handle. running-shoe-4412 loads handle running-shoe, variant 4412.
  • Listing pages. With a query (or a ?q= in the page URL) the listing loader searches; otherwise it loads the collection named by collectionName. Shopify paginates with cursors, so the loader reads ?page, ?startCursor and ?endCursor from the page URL, along with ?sort and filter parameters, and returns the next and previous links in pageInfo.
  • Page URL. The listing and product loaders take the page URL as a second argument, and build product links from it. When content resolves them, they don't receive one: links are then built on https://localhost, so render them through relative() from @decocms/apps-commerce/sdk/url. The resolver does copy the page's query parameters (except page) into the loader's props, so ?startCursor and ?endCursor still arrive; but ?q, ?sort, ?page and the filter parameters are read only from the URL argument (or from pageHref).

To give a listing page the real URL, register a small wrapper of your own and point content at its key. The resolver adds the current URL to every commerce loader's props as __pageUrl:

src/setup/commerce-loaders.ts
import { registerCommerceLoaders } from "@decocms/blocks/cms";
import { productListingPageLoader } from "@decocms/apps-shopify";
 
registerCommerceLoaders({
  "site/loaders/shopifyListingPage.ts": (props) =>
    productListingPageLoader(props, props.__pageUrl ? new URL(props.__pageUrl) : undefined),
});

The app has no response cache of its own. Wrap frequently called read loaders, such as shelves and listing pages, with createCachedLoader(name, loader, profile) from @decocms/blocks/sdk/cachedLoader, and register the wrapper under a key of your own the same way. See Cache a loader.

The package root also exports the loaders as functions: productDetailsPageLoader, productListingPageLoader, productListLoader, relatedProductsLoader, shopLoader and userLoader, and each is reachable at @decocms/apps-shopify/loaders/<Name>.

Cart and customers

Cart and customer state live in two cookies, both HttpOnly, Secure, SameSite=Lax and valid for a week:

CookieHolds
cartThe Shopify cart id (without its gid://shopify/Cart/ prefix)
secure_customer_sigThe customer access token after sign-in

The cart and customer functions don't read the request on their own. Each takes the incoming request headers to read these cookies, and optionally a Headers object to write Set-Cookie into:

FunctionImportWhat it does
getCart(requestHeaders, responseHeaders?)@decocms/apps-shopifyReturns the visitor's cart, creating one (and setting the cookie) if there's none.
createCart()@decocms/apps-shopifyCreates an empty cart and returns its id.
addItems({ lines, requestHeaders, responseHeaders? })@decocms/apps-shopify/actions/cart/addItemsAdds lines (merchandiseId, quantity?, attributes?, sellingPlanId?).
updateItems({ lines, requestHeaders, responseHeaders? })@decocms/apps-shopify/actions/cart/updateItemsChanges quantities by line id.
updateCoupons({ discountCodes, requestHeaders, responseHeaders? })@decocms/apps-shopify/actions/cart/updateCouponsReplaces the discount codes.
signIn({ email, password, requestHeaders, responseHeaders? })@decocms/apps-shopify/actions/user/signInSigns in and sets secure_customer_sig. Returns null if the visitor is already signed in or the request fails; check customerAccessTokenCreate.customerUserErrors for wrong credentials.
signUp({ email, password, firstName?, lastName?, acceptsMarketing? })@decocms/apps-shopify/actions/user/signUpCreates a customer account.
userLoader(requestHeaders)@decocms/apps-shopifyThe signed-in customer (email, givenName, familyName), or null.

Because they take Headers objects, call them from your own server functions rather than over /deco/invoke. On TanStack, read the request from request context and pass the cookies on with forwardResponseCookies:

src/server/cart.ts
import { createServerFn } from "@tanstack/react-start";
import { RequestContext } from "@decocms/blocks/sdk/requestContext";
import { forwardResponseCookies } from "@decocms/tanstack/sdk/cookiePassthrough";
import { getCart } from "@decocms/apps-shopify";
import addItems from "@decocms/apps-shopify/actions/cart/addItems";
 
export const loadCart = createServerFn({ method: "POST" }).handler(async () => {
  const responseHeaders = new Headers();
  const cart = await getCart(RequestContext.request.headers, responseHeaders);
  forwardResponseCookies(responseHeaders.getSetCookie());
  return cart;
});
 
export const addToCart = createServerFn({ method: "POST" })
  .inputValidator((data: { merchandiseId: string; quantity: number }) => data)
  .handler(async ({ data }) => {
    const responseHeaders = new Headers();
    const cart = await addItems({
      lines: { merchandiseId: data.merchandiseId, quantity: data.quantity },
      requestHeaders: RequestContext.request.headers,
      responseHeaders,
    });
    forwardResponseCookies(responseHeaders.getSetCookie());
    return cart;
  });

Call loadCart first, for example when the cart drawer mounts: getCart creates the cart and sets its cookie, and the cart actions throw Missing cart cookie when the visitor has no cart yet.

Environment variables

VariableWhat it does
SHOPIFY_STOREFRONT_TOKENFallback Storefront API token when the block doesn't hold one.
DECO_CRYPTO_KEYDecrypts a token stored encrypted in the block. See Apps.