nmaier95 / shopify-product-fetcher
Shopify for Craft CMS
Installs: 4 688
Dependents: 0
Suggesters: 0
Security: 0
Stars: 48
Watchers: 7
Forks: 26
Open Issues: 17
Type:craft-plugin
Requires
- php: ^8.2
- carnage/php-graphql-client: ^1.14
- craftcms/cms: ^5.0.0-beta.10||^4.15.0
- shopify/shopify-api: ^5.2.0
Requires (Dev)
- craftcms/ecs: dev-main
- craftcms/feed-me: ^6.6.1||^5.9.0
- craftcms/phpstan: dev-main
- craftcms/rector: dev-main
- craftcms/redactor: *
- vlucas/phpdotenv: ^3.4
- 6.x-dev
- 6.1.x-dev
- 6.0.0
- 6.0.0-beta.6
- 6.0.0-beta.5
- 6.0.0-beta.4
- 6.0.0-beta.3
- 6.0.0-beta.2
- 6.0.0-beta.1
- 5.x-dev
- 5.4.1
- 5.4.0
- 5.3.x-dev
- 5.3.1
- 5.3.0
- 5.2.0
- 5.1.2
- 5.1.1
- 5.1.0
- 5.0.0
- 4.x-dev
- 4.1.2
- 4.1.1
- 4.1.0
- 4.0.0
- v3.x-dev
- 3.2.0
- 3.1.1
- 3.1.0
- 3.0.1
- 3.0.0.1
- 3.0.0
- v2.x-dev
- 2.2.0
- 2.1.1
- 2.0.1
- 2.0.0
- 1.2.1
- 1.1.1
- 1.1.0
- 1.0.5
- 1.0.4
- 1.0.3
- 1.0.2
- 1.0.1
- dev-dependabot/npm_and_yarn/webpack-5.99.5
- dev-dependabot/npm_and_yarn/ws-8.18.1
- dev-dependabot/npm_and_yarn/express-4.21.2
- dev-dependabot/npm_and_yarn/multi-d74e9e8b49
- dev-dependabot/npm_and_yarn/multi-db17b8dbde
- dev-dependabot/npm_and_yarn/multi-63fbbd1def
- dev-dependabot/npm_and_yarn/multi-b4d14387f7
- dev-dependabot/npm_and_yarn/http-proxy-middleware-2.0.9
- dev-feature/pt-2562-graphql-support
- dev-dependabot/npm_and_yarn/serialize-javascript-6.0.2
- dev-main
- dev-feature/multi-store
- dev-docs/prices
- dev-feature/example-templates
This package is auto-updated.
Last update: 2025-04-17 08:46:21 UTC
README
Shopify for Craft CMS
Build a content-driven storefront by synchronizing Shopify products into Craft CMS.
Important
Version 6.x of the Shopify plugin uses the new GraphQL Admin API to set up webhooks and synchronize data. Review the Upgrading section for more info about the impacts of this change.
Topics
- 📦 Installation: Set up the plugin and get connected to Shopify.
- 🗃️ Working with Products: Learn what kind of data is available and how to access it.
- 📑 Templating: Tips and tricks for using products in Twig.
- 🍃 Upgrading: Take advantage of new features and performance improvements.
- 🔭 Advanced Features: Go further with your integration.
Installation
Shopify requires Craft CMS 4.15.0+ or 5.0.0+.
To install the plugin, visit the Plugin Store from your Craft project, or follow these instructions.
-
Navigate to your Craft project in a new terminal:
cd /path/to/project
-
Require the package with Composer:
composer require craftcms/shopify -w
-
In the Control Panel, go to Settings → Plugins and click the “Install” button for Shopify, or run:
php craft plugin/install shopify
Create a Shopify App
The plugin works with Shopify’s Custom Apps system.
Note
If you are not the owner of the Shopify store, have the owner add you as a collaborator or staff member with the Develop Apps permission.
Follow Shopify’s directions for creating a private app (through the Get the API credentials for a custom app section), and take these actions when prompted:
-
App Name: Choose something that identifies the integration, like “Craft CMS.”
-
Admin API access scopes: The following scopes are required for the plugin to function correctly:
read_products
read_product_listings
read_inventory
Additionally (at the bottom of this screen), the Webhook subscriptions → Event version should be
2024-10
. -
Admin API access token: Reveal and copy this value into your
.env
file, asSHOPIFY_ADMIN_ACCESS_TOKEN
. -
API key and secret key: Reveal and/or copy the API key and API secret key into your
.env
underSHOPIFY_API_KEY
andSHOPIFY_API_SECRET_KEY
, respectively.
Store Hostname
The last piece of info you’ll need on hand is your store’s hostname. This is usually what appears in the browser when using the Shopify admin—it’s also shown in the Settings screen of your store:
Save this value (without the leading http://
or https://
) in your .env
as SHOPIFY_HOSTNAME
. At this point, you should have the following Shopify-specific values:
# ... SHOPIFY_ADMIN_ACCESS_TOKEN="..." SHOPIFY_API_VERSION="2024-10" SHOPIFY_API_KEY="..." SHOPIFY_API_SECRET_KEY="..." SHOPIFY_HOSTNAME="my-storefront.myshopify.com"
Connect Plugin
Now that you have credentials for your custom app, it’s time to add them to Craft.
- Visit the Shopify → Settings screen in your project’s control panel.
- Assign the four environment variables to the corresponding settings, using the special config syntax:
- API Version:
$SHOPIFY_API_VERSION
- API Key:
$SHOPIFY_API_KEY
- API Secret Key:
$SHOPIFY_API_SECRET_KEY
- Access Token:
$SHOPIFY_ACCESS_TOKEN
- Host Name:
$SHOPIFY_HOSTNAME
- API Version:
- Click Save.
Note
These settings are stored in Project Config, and will be automatically applied in other environments. Webhooks will still need to be configured for each environment!
Set up Webhooks
Once your credentials have been added to Craft, a new Webhooks tab will appear in the Shopify section of the control panel.
Click Create on the Webhooks screen to add the required webhooks to Shopify. The plugin will use the credentials you just configured to perform this operation—so this also serves as an initial communication test.
Warning
You will need to add webhooks for each environment you deploy the plugin to, because each webhook is tied to a specific URL.
Note
If you need to test live synchronization in development, we recommend using ngrok to create a tunnel to your local environment. DDEV makes this simple, with the ddev share
command. Keep in mind that your site’s primary/base URL is used when registering webhooks, so you may need to update it to match the ngrok tunnel, then recreate your webhooks.
Upgrading
To guarantee that the plugin can access all the Shopify resources it needs, review Admin API access scopes in the requirements section before performing an upgrade.
After upgrading, check that the required webhooks are in place by visiting Shopify → Webhooks in the Craft control panel. The plugin will retrieve all the webhooks for your storefront, and display a Create button if any are missing for the current environment.
Note
You must create webhooks for each environment. Repeat this process in your live environment, after deploying.
The remainder of this section applies specifically to the 5.x → 6.x upgrade. Review the changelog for a complete list of added, removed, and deprecated APIs.
Deprecated Settings
The syncProductMetafields
and syncVariantMetafields
are no longer used, and should be removed from your configuration file. Meta fields are now automatically loaded alongside product and variant data.
Property Names
Accessors on our product element remain stable, but with the shift to the GraphQL Admin API, many canonical property names on products and variants have changed. If you directly output properties of variants in your templates, they are apt to need updates. The ProductVariant
model documentation shows how to translate old property names (teal) to the new GraphQL schema (magenta).
Contextual Pricing
Shopify’s “presentment prices” are now referred to as “contextual pricing.” Variant arrays still have the default price
and compareAtPrice
fields (previously price
and compare_at_price
, respectively), but to fetch context-dependent prices, you must provide a list of two-letter country codes via the Contextual Pricing Countries setting. Product data must be sychronized after changing this setting.
Contextual prices are stored among other variant properties, with keys corresponding to each country code. For example: US
pricing would be available as usContextualPricing
; DE
pricing would be available as deContextualPricing
. Each contextual price has this structure:
[ 'price' => [ 'amount' => '50.0', 'currencyCode' => 'USD', ], 'compareAtPrice' => null, ]
You can display these prices using Craft’s built-in currency formatter:
{% set usPrice = variant.usContextualPricing.price %} {{ usPrice.amount|currency(usPrice.currency) }}
Resource IDs
The GraphQL API no longer uses numeric IDs to look up objects; instead, it expects a new gid://
-prefixed value. Product elements expose this as shopifyId
(so as to avoid conflicts with the internal, Craft-specific element id
property), but it appears at the top level of other resources, like options, variants, and media.
Product Element
Products from your Shopify store are represented in Craft as product elements, and can be found by going to Shopify → Products in the control panel.
Synchronization
Once the plugin has been configured, you can perform an initial synchronization of all products via the control panel (via Utilities → Shopify Sync) or the command line:
php craft shopify/sync/products
This adds a bulk operation to the plugin’s internal queue. Once Shopify has gathered the data, it will issue a webhook to your project, and the plugin will download and process the payload.
Going forward, your products are automatically kept in sync via webhooks. You can view a history of synchronization operations by visiting the Shopify Sync utility.
Native Attributes
In addition to the standard element attributes like id
, title
, and status
, each Shopify product element contains direct accessors for these canonical Shopify Product attributes:
Attribute | Description | Type |
---|---|---|
shopifyId |
The unique product identifier in your Shopify store. | String |
shopifyStatus |
The status of the product in your Shopify store. Values can be active , draft , or archived . |
String |
handle |
The product’s “URL handle” in Shopify, equivalent to a “slug” in Craft. For existing products, this is visible under the Search engine listing section of the edit screen. | String |
productType |
The product type of the product in your Shopify store. | String |
descriptionHtml |
Product description. Use the |raw filter to output it in Twig—but only if the content is trusted. This was previously called bodyHtml . |
String |
tags |
Tags associated with the product in Shopify. | Array |
templateSuffix |
Liquid template suffix used for the product page in Shopify. | String |
vendor |
Vendor of the product. | String |
metaFields |
Metafields associated with the product. | Array |
images |
Images attached to the product in Shopify. The complete ProductImage resources are stored in Craft. | Array |
options |
ProductOption objects, as configured in Shopify. Each option has a name , position , and an array of in-use values . |
Array |
createdAt |
When the product was created in your Shopify store. (This will almost always be different from the element’s native dateCreated property.) |
DateTime |
publishedAt |
When the product was published in your Shopify store. | DateTime |
updatedAt |
When the product was last updated in your Shopify store. (This will almost always be different from the element’s native dateUpdated property.) |
DateTime |
All of these properties are available when working with a product element in your templates.
Important
See the Shopify documentation on the product resource for more information about what kinds of values to expect from these properties.
A complete copy of the Shopify API data used to populate a product element is available under its data
property. Wherever possible, we have used Shopify’s native property names—but by virtue of fetching products via GraphQL, there may be differences between the structure of this object and the API documentation, especially as it relates to nested objects. Use the following methods to access related or nested data!
Methods
The product element has a few methods you might find useful in your templates.
Product::getVariants()
Returns an array of variants belonging to the product. Each variant is an associative array—not an element—but you can use the same dot notation to access their properties:
{% set variants = product.getVariants() %} <select name="variantId"> {% for variant in variants %} <option value="{{ variant.id }}">{{ variant.title }}</option> {% endfor %} </select>
You can eager-load variants alongside products using the product query’s .withVariants()
method.
Product::getDefaultVariant()
Shortcut for getting the first/default variant belonging to the product.
{% set products = craft.shopifyProducts .withVariants() .all() %} <ul> {% for product in products %} {% set defaultVariant = product.getDefaultVariant() %} <li> <a href="{{ product.url }}">{{ product.title }}</a> <span>{{ defaultVariant.price|currency }}</span> </li> {% endfor %} </ul>
Product::getCheapestVariant()
Shortcut for getting the lowest-priced variant belonging to the product.
{% set cheapestVariant = product.getCheapestVariant() %} Starting at {{ cheapestVariant.price|currency }}!
Note that this does not factor in contextual pricing.
Product::getShopifyUrl()
{# Get a link to the product’s page on Shopify: #} <a href="{{ product.getShopifyUrl() }}">View on our store</a> {# Link to a product with a specific variant pre-selected: #} <a href="{{ product.getShopifyUrl({ variant: variant.id }) }}">Buy now</a>
This has limited utility if you are displaying products on-site (rather than linking back to a Shopify storefront). To get the URL of a product within your Craft project, use product.url
.
Product::getShopifyEditUrl()
For administrators, you can even link directly to the Shopify admin:
{# Assuming you’ve created a custom group for Shopify admin: #} {% if currentUser and currentUser.isInGroup('clerks') %} <a href="{{ product.getShopifyEditUrl() }}">Edit product on Shopify</a> {% endif %}
Custom Fields
Products synchronized from Shopify have a dedicated field layout, which means they support Craft’s full array of content tools.
The product field layout can be edited by going to Shopify → Settings → Products, and scrolling down to Field Layout.
Fields are accessible from any product element, by their handle:
{# Native properties: #} <h2>{{ product.title }}</h2> <span class="price">{{ product.price|currency }}</span> {# Custom relational field: #} <ul class="support"> {% for article in product.relatedHelpArticles.all() %} <li>{{ article.getLink() }}</li> {% endfor %} </ul>
Variants and other nested records do not support custom fields.
Routing
You can give synchronized products their own on-site URLs. To set up the URI format (and the template that will be loaded when a product URL is requested), go to Shopify → Settings → Products.
If you would prefer your customers to view individual products on Shopify, clear out the Product URI Format field on the settings page, and use product.shopifyUrl
instead of product.url
in your templates.
Product Status
A product’s status
in Craft is a combination of its shopifyStatus
attribute ('active', 'draft', or 'archived') and its enabled state. The former can only be changed from Shopify; the latter is set in the Craft control panel.
Note
Statuses in Craft are often a synthesis of multiple properties. For example, an entry with the Pending status just means it isenabled
and has apostDate
in the future.
In most cases, you’ll only want to display “Live” products, or those which are Active in Shopify and Enabled in Craft:
Status | Shopify | Craft |
---|---|---|
live |
Active | Enabled |
shopifyDraft |
Draft | Enabled |
shopifyArchived |
Archived | Enabled |
disabled |
Any | Disabled |
This is the default behavior when querying for products, but you can pass one of the custom Status options above to the .status()
param to override it.
Querying Products
Products can be queried like any other element type in Craft.
A new query begins with the craft.shopifyProducts
factory function:
{% set products = craft.shopifyProducts.all() %}
The plugin automatically loads the relevant product when its route is requested, and makes a product
variable available in the template. You only need to query for products when when they are displayed outside of this context. Product fields also return product queries.
Query Parameters
The following element query parameters are supported, in addition to Craft’s standard set.
Note
Fields stored as JSON (like tags
, options
and metafields
) are only queryable as plain text. If you need to do advanced organization or filtering, we recommend using custom Category or Tag fields in your Product field layout.
shopifyId
Filter by legacy numeric Shopify product IDs.
{# Watch out—these aren't the same as element IDs! #} {% set singleProduct = craft.shopifyProducts .shopifyId(123456789) .one() %}
shopifyGid
Filter by Shopify GIDs.
{# Watch out—these aren't the same as element IDs! #} {% set singleProduct = craft.shopifyProducts .shopifyId('gid://shopify/Product/123456789') .one() %}
This is equivalent to .shopifyId(123456789)
, but may be simpler if you are combining data from client-side queries.
shopifyStatus
Directly query against the product’s status in Shopify.
{% set archivedProducts = craft.shopifyProducts .shopifyStatus('archived') .all() %}
Use the regular .status()
param if you'd prefer to query against the synthesized product status values.
Warning
Note that .shopifyStatus()
does not override conditions applied by the .status()
param (including the defaults). You may need to call .status(null)
to unset them, or use .status('shopifyDraft')
, directly.
handle
Query by the product’s handle, in Shopify.
{% set product = craft.shopifyProducts .handle('worlds-tallest-socks') .all() %}
Warning
This is not a reliable means to fetch a specific product, as the value may change during a synchronization. If you want to store a permanent reference to a product, consider using the Shopify product field to relate it by element ID.
productType
Find products by their “type” in Shopify.
{% set upSells = craft.shopifyProducts .productType(['apparel', 'accessories']) .all() %}
tags
Tags are stored as a JSON array, which may complicate direct comparisons. You may see better results using the .search()
param.
{# Find products whose tags include the term in any position, with variations on casing: #} {% set clogs = craft.shopifyProducts .tags(['*clog*', '*Clog*']) .all() %}
options
Options are stored as a JSON array, which may complicate direct comparisons. You may see better results using the .search()
param.
{# Find products whose options include a `size` key: #} {% set clogs = craft.shopifyProducts .tags('*"size"*') .all() %}
vendor
Filter by the vendor information from Shopify.
{# Find products with a vendor matching either option: #} {% set fancyBags = craft.shopifyProducts .vendor(['Louis Vuitton', 'Jansport']) .all() %}
Eager-loading
Variants (ProductVariant
s), images (MediaImage
s), and meta fields (Metafield
s) attached to product elements are not elements themselves, and must be explicitly eager-loaded to avoid performance issues when displaying data in a loop:
{% set products = craft.shopifyProducts() .withVariants() .withImages() .withMetafields() .all() %} <ul> {% for product in products %} <li> <h2>{{ product.title }}</h2> Available in {{ product.variants|column('title')|join(', ') }}. {# Similar loops for each type of nested record... #} </li> {% endfor %} </ul>
Tip
The shorthand .withAll()
is a future-proof means of eager-loading each additional type of nested record.
You can still access product.variants
, product.images
, and product.metafields
without eager-loading—but it may result in an additional query for each kind of content. Once you’ve retrieved variants, for example, they are memoized on the product element instance for the duration of the request.
Templating
Product Data
Products behave just like any other element, in Twig. Once you’ve loaded a product via a query (or have a reference to one on its template), you can output its native Shopify attributes and custom field data.
Note
Some attributes are stored as JSON, which limits nested properties’s types. As a result, dates may be slightly more difficult to work with.
{# Standard element title: #} {{ product.title }} {# -> Root Beer #} {# Shopify HTML content: #} {{ product.descriptionHtml|raw }} {# -> <p>...</p> #} {# Tags, as list: #} {{ product.tags|join(', ') }} {# -> sweet, spicy, herbal #} {# Tags, as filter links: #} {% for tag in tags %} <a href="{{ siteUrl('products', { tag: tag }) }}">{{ tag|title }}</a> {# -> <a href="https://mydomain.com/products?tag=herbal">Herbal</a> #} {% endfor %} {# Images: #} {% for image in product.images %} <img src="{{ image.src }}" alt="{{ image.alt }}"> {# -> <img src="https://cdn.shopify.com/..." alt="Bubbly Soda"> #} {% endfor %} {# Variants: #} <select name="variantId"> {% for variant in product.variants %} <option value="{{ variant.id }}">{{ variant.title }} ({{ variant.price|currency }})</option> {% endfor %} </select>
Variants and Pricing
Products don’t have a price, despite what the Shopify UI might imply—instead, every product has at least one Variant.
You can get an array of variant objects for a product by accessing product.variants
or calling product.getVariants()
. The product element also provides convenience methods for getting the default and cheapest variants, but you can filter them however you like with Craft’s collect()
Twig function.
Unlike products, variants in Craft…
- …are represented (mostly) as the API returns them;
- …the
metafields
property is accessible in addition to the API’s returned properties; - …use Shopify’s convention of underscores in property names instead of exposing camel-cased equivalents;
- …are plain associative arrays;
- …have no methods of their own;
Once you have a reference to a variant, you can output its properties:
{% set defaultVariant = product.getDefaultVariant() %} {{ defaultVariant.price|currency }}
Note
The built-in currency
Twig filter is a great way to format money values.
Using Options
Options are Shopify’s way of distinguishing variants on multiple axes.
If you want to let customers pick from options instead of directly select variants, you will need to resolve which variant a given combination points to.
Form
<form id="add-to-cart" method="post" action="{{ craft.shopify.store.getUrl('cart/add') }}"> {# Create a hidden input to send the resolved variant ID to Shopify: #} {{ hiddenInput('id', null, { id: 'variant', data: { variants: product.variants, }, }) }} {# Create a dropdown for each set of options: #} {% for option in product.options %} <label> {{ option.name }} {# The dropdown includes the option’s `position`, which helps match it with the variant, later: #} <select data-option="{{ option.position }}"> {% for val in option.values %} <option value="{{ val }}">{{ val }}</option> {% endfor %} </select> </label> {% endfor %} <button>Add to Cart</button> </form>
Script
The code below can be added to a {% js %}
tag, alongside the form code.
// Store references to <form> elements: const $form = document.getElementById("add-to-cart"); const $variantInput = document.getElementById("variant"); const $optionInputs = document.querySelectorAll("[data-option]"); // Create a helper function to test a map of options against known variants: const findVariant = (options) => { const variants = JSON.parse($variantInput.dataset.variants); // Use labels for the inner and outer loop so we can break out early: variant: for (const v in variants) { option: for (const o in options) { // Option values are stored as `option1`, `option2`, or `option3` on each variant: if (variants[v][`option${o}`] !== options[o]) { // Didn't match one of the options? Bail: continue variant; } } // Nice, all options matched this variant! Return it: return variants[v]; } }; // Listen for change events on the form, rather than the individual option menus: $form.addEventListener("change", (e) => { const selectedOptions = {}; // Loop over option menus and build an object of selected values: $optionInputs.forEach(($input) => { // Add the value under the "position" key selectedOptions[$input.dataset.option] = $input.value; }); // Use our helper function to resolve a variant: const variant = findVariant(selectedOptions); if (!variant) { console.warn("No variant exists for options:", selectedOptions); return; } // Assign the resolved variant’s ID to the hidden input: $variantInput.value = variant.id; }); // Trigger an initial `change` event to simulate a selection: $form.dispatchEvent(new Event("change"));
Cart
Your customers can add products to their cart directly from your Craft site:
{% set product = craft.shopifyProducts.one() %} <form action="{{ craft.shopify.store.getUrl('cart/add') }}" method="post"> <select name="id"> {% for variant in product.getVariants() %} <option value="{{ variant.id }}">{{ variant.title }}</option> {% endfor %} </select> {{ hiddenInput('qty', 1) }} <button>Add to Cart</button> </form>
JS Buy SDK
On-site cart management and checkout are not currently supported in a native way.
However, Shopify provides the (deprecated) Javascript Buy SDK as a means of interacting with their Storefront API to create completely custom shopping experiences.
Note
Use of the Storefront API (directly, or via the Buy SDK or JS Buy Button) requires a different access key, and assumes that you have published your products into the Storefront app’s sales channel.
Your public Storefront API token can be stored with your other credentials in .env
and output in your front-end with the {{ getenv('...') }}
Twig helper—or just baked into a Javascript bundle. Keep your other secrets safe! This is the only one that can be disclosed.
The plugin makes no assumptions about how you use your product data in the front-end, but provides the tools necessary to connect it with the SDK. As an example, let’s look at how you might render a list of products in Twig, and hook up a custom client-side cart…
Shop Template: templates/shop.twig
{# Include the Buy SDK on this page: #} {% do view.registerJsFile('https://sdks.shopifycdn.com/js-buy-sdk/v2/latest/index.umd.min.js', {POS_HEAD) %} {# Register your own script file (see “Custom Script,” below): #} {% do view.registerJsFile('/assets/js/shop.js') %} {# Load some products: #} {% set products = craft.shopifyProducts().all() %} <ul> {% for product in products %} {# For now, we’re only handling a single variant: #} {% set defaultVariant = product.getVariants()|first %} <li> {{ product.title }} <button class="buy-button" data-default-variant-id="{{ defaultVariant.id }}">Add to Cart</button> </li> {% endfor %} </ul>
Custom Script: assets/js/shop.js
This script must be registered after the Buy SDK.
// Initialize a client: const client = ShopifyBuy.buildClient({ domain: "my-storefront.myshopify.com", storefrontAccessToken: "...", }); // Create a simple logger for the cart’s state: const logCart = (c) => { console.log(c.lineItems); console.log(`Checkout URL: ${c.webUrl}`); }; // Create a cart or “checkout” (or perhaps load one from `localStorage`): client.checkout.create().then((checkout) => { const $buyButtons = document.querySelectorAll(".buy-button"); // Add a listener to each button: $buyButtons.forEach(($b) => { $b.addEventListener("click", (e) => { // Read the variant ID off the product: client.checkout .addLineItems(checkout.id, [ { // Build the Storefront-style resource identifier: variantId: `gid://shopify/ProductVariant/${$b.dataset.defaultVariantId}`, quantity: 1, }, ]) .then(logCart); // <- Log the changes! }); }); });
Buy Button JS
The above example can be simplified with the Buy Button JS, which provides some ready-made UI components, like a fully-featured cart. The principles are the same:
- Make products available via the appropriate sales channels in Shopify;
- Output synchronized product data in your front-end;
- Initialize, attach, or trigger SDK functionality in response to events, using Shopify-specific identifiers from step #2;
Storefront API Client
For fully custom front-end solutions, consider the Storefront API Javascript client, which is built and maintained with the new GraphQL API in mind.
{% do view.registerJsFile('https://unpkg.com/@shopify/storefront-api-client@1.0.5/dist/umd/storefront-api-client.min.js') %} <script> // Note that these values are interpolated into the script tag with Twig! const client = ShopifyStorefrontAPIClient.createStorefrontApiClient({ storeDomain: '{{ craft.shopify.settings.hostName }}', apiVersion: '{{ craft.shopify.settings.apiVersion }}', publicAccessToken: '{{ getenv('SHOPIFY_PUBLIC_ACCESS_TOKEN') }}', }); </script>
See the usage examples for ideas. Many queries will require Shopify identifiers, which you can output as hidden attributes:
{% for variant in product.variants %} <button class="buy-button" data-variant-id="{{ variant.id }}">Buy {{ variant.title }}</button> {% endfor %}
You would then consume these GIDs in Javascript, passing them to queries via the Shopify client. Here are the two GraphQL query fragments for creating and updating a cart:
const createCartMutation = ` mutation cartCreate($input: CartInput) { cartCreate(input: $input) { cart { id } } } `; const updateCartMutation = ` mutation cartLinesAdd($cartId: ID!, $lines: [CartLineInput!]!) { cartLinesAdd(cartId: $cartId, lines: $lines) { cart { # Cart fields } userErrors { field message } warnings { # CartWarning fields } } } `;
…and the corresponding plumbing to connect those queries to the DOM elements and localStorage
:
async function getCartId() { // Have we already done this? Use an existing cart ID, if available: if (localStorage.getItem('shopifyCartGid')) { return localStorage.getItem('shopifyCartGid'); } const { data, errors, extensions } = await client.request(createCartMutation, { variables: { input: { // Accepted parameters are available in the documentation: // https://shopify.dev/docs/api/storefront/latest/mutations/cartCreate }, }, }); // Ok, save it for later! localStorage.setItem('shopifyCartGid', data.cartCreate.cart.id); return localStorage.getItem('shopifyCartGid'); } function addItem(cartId, $el) { const line = { quantity: 1, // The Shopify GID was set on the button as `data-variant-id`: merchandiseId: $el.dataset.variantId, }; return client.request(updateCartMutation, { variables: { cartId, lines: [line], }, }); } // Find "buy buttons" and listen for clicks: const $buyButtons = document.getElementsByClassName('buy-button'); Array.from($buyButtons).forEach(function($bb) { $bb.addEventListener('click', function(e) { // Ensure we have a cart ID, then add the clicked item: getCartId() .then(function(cartId) { return addItem(cartId, $bb); }) .then(console.log); }); });
Warning
This is just a slice of the required functionality for an on-site cart—the actual implementation depends largely on what features you want to offer customers, your front-end stack, and your appetite for dealing directly with the GraphQL client!
Checkout
While solutions exist for creating a customized shopping experience, checkout will always happen on Shopify’s platform. This is a policy matter, not a technical limitation of the plugin (or any other integration, for that matter)—Shopify’s checkout flow is fast, reliable, secure, and familiar to many shoppers.
If you want your customers’ entire journey to be kept on-site, we encourage you to try out our powerful ecommerce plugin, Commerce.
Helpers
In addition to product element methods, the plugin exposes its API to Twig via craft.shopify
.
API Service
Warning
Use of API calls in Twig blocks rendering and—depending on traffic—may cause timeouts and/or failures due to rate limits. Consider using the {% cache %}
tag with a key and specific expiry time to avoid making a request every time a template is rendered:
{% cache using key "shopify:collections" for 10 minutes %} {# API calls + output... #} {% endcache %}
Legacy REST API
[!DANGER] The Admin REST API has been deprecated. This information is provided only for posterity; the methods still exist in the plugin, but may stop returning data some time in 2025.
Issue requests to the Shopify Admin API via craft.shopify.api
:
{% set req = craft.shopify.api.get('custom_collections') %} {% set collections = req.response.custom_collections %}
The schema for each API resource will differ. Consult the Shopify API documentation for more information.
You can make arbitrary GraphQL queries against the Shopify API with craft.shopify.api.query()
:
{% set gql %} { collections(first: 10) { nodes { id title } } } {% endset %} {% set response = craft.shopify.api.query(gql) %} {% set collections = response.nodes ?? [] %} {% if collections is not empty %} <ul> {% for collection in collections %} <li>{{ collection.title }}</li> {% endfor %} </ul> {% endif %}
The Shopify GraphQL client is also available if you need to safely pass variables (like pagination offsets or search strings), or make mutations:
{% set response = craft.shopify.api.gqlClient.query({ query: gql, variables: { num: 10, }, }) %} {# The plugin does not intercept response data, so you must unpack it based on what was requested: #} {% set data = response.data.nodes %}
Store Service
A simple URL generator is available via craft.shopify.store
. You may have noticed it in the cart example, above—but it is a little more versatile than that!
{# Create a link to add a product/variant to the cart: #} {{ tag('a', { href: craft.shopify.store.getUrl('cart/add', { id: variant.id, quantity: 1, }), text: 'Add to Cart', target: '_blank', }) }}
The same params argument can be passed to a product element’s getShopifyUrl()
method:
{% for variant in product.getVariants() %} <a href="{{ product.getShopifyUrl({ id: variant.id }) }}">{{ variant.title }}</a> {% endfor %}
Product Field
The plugin provides a Shopify Products field, which uses the familiar relational field UI to allow authors to select synchronized Product elements.
Relationships defined with the Shopify Products field use stable element IDs under the hood. When Shopify products are archived or deleted, the corresponding elements will also be updated in Craft, and naturally filtered out of your query results—including those explicitly attached via a Shopify Products field.
These fields return a product query, which you can customize using any supported query param—or immediately execute:
{% set featuredProducts = category.myProductsField.all() %} <ul> {% for product in featuredProducts %} <li>{{ product.link }}</li> {% endfor %} </ul>
Going Further
Settings
The following settings can also be set via a shopify.php
file in your config/
directory.
Setting | Type | Default | Description |
---|---|---|---|
apiKey |
string |
— | Shopify API key. |
apiSecretKey |
string |
— | Shopify API secret key. |
apiVersion |
string |
— | Shopify API version description. |
accessToken |
string |
— | Shopify API access token. |
contextualPricingCountries |
string |
— | Comma-separated list of two-letter country codes that determine which contextual prices are loaded via the API. |
hostName |
string |
— | Shopify host name. |
uriFormat |
string |
— | Product element URI format. |
template |
string |
— | Product element template path. |
Note
Setting apiKey
, apiSecretKey
, apiVersion
, accessToken
, or hostName
via shopify.php
will override Project Config values set via the control panel during app setup. You can still reference environment values from the config file with craft\helpers\App::env()
.
Events
Learn about responding to events in the Craft extension documentation.
craft\shopify\services\Products::EVENT_BEFORE_SYNCHRONIZE_PRODUCT
Emitted just prior to a product element is saved with new Shopify data. The craft\shopify\events\ShopifyProductSyncEvent
extends craft\events\CancelableEvent
, so setting $event->isValid
allows you to prevent the new data from being saved.
The event object has three properties:
element
: The product element being updated.source
: The Shopify product object that was applied.
use craft\shopify\events\ShopifyProductSyncEvent; use craft\shopify\services\Products; use yii\base\Event; Event::on( Products::class, Products::EVENT_BEFORE_SYNCHRONIZE_PRODUCT, function(ShopifyProductSyncEvent $event) { // Example 1: Cancel the sync if a flag is set via a Shopify metafield: $metafields = $event->element->getMetafields(); if ($metafields['do_not_sync'] ?? false) { $event->isValid = false; } // Example 2: Set a custom field value from metafield data: $event->element->setFieldValue('myNumberFieldHandle', $metafields['cool_factor']); } );
Warning
Do not manually save changes made in this event handler. The plugin will take care of this for you!
Element API
Your synchronized products can be published into an Element API endpoint, just like any other element type. This allows you to set up a local JSON feed of products, decorated with any content you’ve added in Craft:
use craft\shopify\elements\Product; return [ 'endpoints' => [ 'products.json' => function() { return [ 'elementType' => Product::class, 'criteria' => [ 'publishedScope' => 'web', 'with' => [ ['myImageField'] ], ], 'transformer' => function(Product $product) { $image = $product->myImageField->one(); return [ 'title' => $product->title, 'variants' => $product->getVariants(), 'image' => $image ? $image->getUrl() : null, ]; }, ]; }, ], ];