# Shopify (/docs/guides/shopify)

Connect a space's Shopify store, and editors pick its collections and products in content: a **Collection** field in a product grid, a **Product** field in a feature block. Content stores which item was picked; the site reads its prices, stock and images live from Shopify.

## Connecting a store [#connecting-a-store]

The connection goes through the **Obelism** app in Shopify, and is made by an admin or developer of the space.

1. On www, open the space’s page and under **Shopify** enter the store’s address (`acme`, or `acme.myshopify.com`) and choose **Connect store**. Or install the Obelism app from the store’s Shopify admin.
2. Shopify asks to allow the app's access to the store. Allow it.
3. Back on Obelism, sign in if needed, and choose the **space** the store belongs to and the **environment** its storefront uses (usually production).

- **One store per space, in every environment.** Develop, staging and production pick from the same store, so a product picked in develop is the same product after [promotion](/docs/guides/environments).
- A store belongs to one space, and a space has one store. Connecting another store to a space means disconnecting the first.
- Content only reads products and collections. The app can also add files, their alt texts and their translations to products, which [Obelism media](https://media.obelism.studio/docs/shopify) does when you ask it to. It never sees orders, customers or payments.
- **Moving or disconnecting**: on the space’s page on www, under **Shopify → Move or disconnect**, or open the Obelism app from the store’s Shopify admin. Content already pointing at the store’s items keeps them.
- **Uninstalling** the app in Shopify disconnects the store, and Obelism forgets its tokens; 48 hours later Shopify asks us to delete the store's data, and we do.

## The product grid block [#the-product-grid-block]

Once the space has a store, **Settings → Shopify** offers **Add a product grid block**: a block type, `productGrid`, with a heading, a collection and a number of columns (2 to 6, 4 by default). It is added to the environment you're in, like any change to the [content model](/docs/guides/content-model); [promote](/docs/guides/environments) the model to bring it to the next environment. After that it's a type like any other: rename it, add fields, or delete it.

Where the model is [managed in code](/docs/guides/model-in-git#locking-the-model-to-code), Settings show the block's definition to add to the model file instead.

## Shopify fields [#shopify-fields]

Any content type can have a field of type **Shopify**, holding a **collection** or a **product** (chosen when the field is created, and fixed after it is saved), one or, with **multiple**, a list with an optional minimum and maximum. See [fields](/docs/guides/content-model#fields). A multiple field's picked items drag into the order you want, by their grip or with Space and the arrow keys, and delivery keeps that order.

Editors choose items with a picker that searches the store by title and shows each item's image: in the entry editor, the [canvas](/docs/guides/canvas) and the form in the [site's prompt bar](/docs/guides/site-editing). A space without a store says so and links to www, and hides the Shopify features until a store is connected.

[Editing by prompt](/docs/guides/prompt-editing) keeps or removes the items an entry has, but never picks new ones: a model can't know a store's products.

## Links to products [#links-to-products]

Tick **Shopify products** on a [link field](/docs/guides/content-model#links) (`products: true` in the model) and it offers **A product** once the space has a store: a button or card that goes to a product, picked with the same picker. Nothing is imported, and no page is needed for it:

```json
{
	"cta": {
		"product": { "id": "gid://shopify/Product/42", "handle": "mug" },
		"hash": "reviews"
	}
}
```

The [delivery API](/docs/api/delivery#links) answers it as `type: "product"`, with the product and its product page (below) in `entry`, or `null` when it has none. Link to the product page's path, or to your own product route by handle (`/products/mug`). Prompts keep or remove a linked product, but never pick a new one.

## Product pages [#product-pages]

For the products you want to say more about than the store does (a story, a video, a comparison), make a page per product. A product page is an ordinary page whose type has a **Product** field marked **unique** (one product, not translated): each published page of that type is the page of its product, and no two can have the same one.

Once the space has a store, **Settings → Shopify** offers **Add a product page type**: a page type, `productPage`, with the product, a blocks field and the SEO fields (no title: the product's comes from the store), and a `products` folder for its pages, as in the store's own `/products/<handle>`. Change it like any other type afterwards, or give one of your own page types a unique product field instead.

A new product page starts from a product: under **Content → New entry**, choose the product page type and pick the product; it names the page, gives it the product's handle as slug and puts it in the `products` folder.

Where your site shows a product is set per environment under **Settings → Preview and site → Preview URL → Product pages at**: a path after the preview URL with `{handle}`, `/products/{handle}` by default (Shopify's own). The canvas opens a product page there (and shows any product of the store there, in its [Products](/docs/guides/canvas#products) column), and the prompt bar on your site opens product pages there from its page menu.

Pages are only made for the products that need one; the rest keep their store page. A link to a product with a product page is delivered with that page. On the site's product route, look the page up by the product's id rather than by path, since a handle can change in Shopify after the page was made:

```sh
GET /api/delivery/v1/entries?contentType=productPage&fields.product=gid://shopify/Product/42
```

No page found means the product has no marketing content: render the store's product alone.

Mark the product on that route with the SDK's `editableProduct(product)`, next to `editable(page)`. When someone [edits on the site](/docs/guides/site-editing), a product without a page then offers **Add a product page** in the prompt bar, which makes the draft for that product and shows it:

```tsx
<main {...editableProduct(product)} {...editable(productPage)}>
	…
</main>
```

## Collection pages [#collection-pages]

Collections get pages the same way, for what goes around a collection's products on your site: an introduction, a banner, a buying guide. A collection page's type has a **Collection** field marked **unique** (one collection, not translated); a type has at most one unique Shopify field, so a page is about a product or a collection, never both.

- **Settings → Shopify** offers **Add a collection page type**: `collectionPage`, with the collection, a blocks field and the SEO fields (no title: the collection's comes from the store), and a `collections` folder for its pages.
- A new collection page starts from a collection under **Content → New entry**, named after it, with its handle as slug, in the `collections` folder.
- **Settings → Preview and site → Preview URL → Collection pages at** sets where your site shows a collection, `/collections/{handle}` by default. The canvas opens collection pages there, and shows any collection of the store there in its [Collections](/docs/guides/canvas#products) column.
- On your collection route, look the page up by the collection's id, and render the store's collection alone without one:

  ```sh
  GET /api/delivery/v1/entries?contentType=collectionPage&fields.collection=gid://shopify/Collection/7
  ```

- Mark the collection with the SDK's `editableCollection(collection)` (`@obelism/content` 0.20 or later), next to `editable(page)`, and the prompt bar on your site offers **Add a collection page** on a collection without one.

## In content [#in-content]

A Shopify field stores the item's id, the same in every environment, and its handle when it was picked:

```json
{
	"collection": {
		"id": "gid://shopify/Collection/451234",
		"handle": "new-arrivals"
	}
}
```

With **multiple**, a list of those. The [delivery API](/docs/api/delivery#shopify-fields) returns it as stored. Content never copies prices, stock or images, so they are never out of date.

If the handle is changed in Shopify later, the stored one stays as it was until the item is picked again. Fetch by `id`, and use the handle only for links.

## On the site [#on-the-site]

**Settings → API tokens → Copy as .env** gives the store's domain and a Storefront API token, made when the store was connected:

```bash
SHOPIFY_STORE_DOMAIN=acme.myshopify.com
SHOPIFY_STOREFRONT_TOKEN=…
```

The Storefront token only reads what the store sells, so it is safe in the browser. With the SDK (`@obelism/content` 0.17.0 and later):

```ts
import { fetchShopifyCollection } from '@obelism/content'

// A productGrid block: { heading, collection, columns }.
const collection = await fetchShopifyCollection(block.collection, {
	shop: process.env.SHOPIFY_STORE_DOMAIN!,
	storefrontToken: process.env.SHOPIFY_STOREFRONT_TOKEN!,
	first: 12,
})

const products = collection?.products ?? []
// Each: id, handle, title, availableForSale, featuredImage,
// priceRange.minVariantPrice ({ amount, currencyCode })
```

`fetchShopifyProducts(items, options)` does the same for a product field, in the field's order. Both leave out what the store no longer has, and take `country` and `language` for a market's prices. See [the JavaScript SDK](/docs/sdk/javascript#shopify).

A Hydrogen or Liquid storefront already queries Shopify: use the `id` in its own queries instead (`collection(id: …)` in the Storefront API).

`npx @obelism/content types` types the fields as `ShopifyItem` (`{ id, handle }`), and `npx @obelism/content init` writes an example item for each.

## On a Hydrogen storefront [#on-a-hydrogen-storefront]

A Hydrogen storefront on [Shopify Oxygen](/docs/getting-started/oxygen) queries the Storefront API itself; the `{ id, handle }` Content stores are all it needs. Use the two values from **Copy as .env**, or Hydrogen's own `PUBLIC_STORE_DOMAIN` and `PRIVATE_STOREFRONT_API_TOKEN`:

```ts
import { gql } from '@shopify/hydrogen'

const PRODUCT = gql(`#graphql
	query Product($id: ID!) {
		product(id: $id) {
			title
			priceRange { minVariantPrice { amount currencyCode } }
		}
	}
`)

const { data } = await storefront.graphql(PRODUCT, {
	variables: { id: item.id }, // gid://shopify/Product/…
})
const product = data?.product
```

`storefront` is Hydrogen's client (`createStorefrontClient`), and `graphql` answers `{ data, errors, headers }`.

`fetchShopifyCollection` and `fetchShopifyProducts` work there too when you'd rather not write the query; they take the same two values.

- **Prices and stock are the store's**, read at render time. Cache the queries with Hydrogen's `Cache.short()` or `Cache.long()`, and let the [webhooks](#keeping-your-site-in-step) tell you to refresh what you keep yourself; on Oxygen the TTL is what bounds staleness.
- **On Oxygen there is no tag cache and no purging of the live deployment's cache**, so the TTL is what bounds how stale a price can be ([caching on Oxygen](/docs/getting-started/oxygen#4-cache-with-a-ttl-not-with-tags)).
- **Images come from Content**, not from the Storefront API: use the asset's `url` as it is ([assets](/docs/guides/assets)).
- **Shoppable images**: the delivery asset's `products` carry each product's point (`position`) when the use asks for them, so a Hydrogen storefront draws the hotspots from that ([products in a file](/docs/guides/assets#products-in-a-file)). The theme's **Shoppable image** app block instead reads the product's `hotspots` metafield, which the Storefront API doesn't expose.

## Keeping your site in step [#keeping-your-site-in-step]

Prices, stock and images come from the Storefront API when your site renders, so a site that caches its pages needs to hear when the store changes. Add a webhook under **Settings → Webhooks** with `product.updated`, `product.deleted`, `collection.updated` and `collection.deleted`, and revalidate the pages and cache tags each event names. See [Webhooks](/docs/guides/webhooks#shopify-changes).

## Media in Shopify [#media-in-shopify]

Files in **Media** tagged with a product can go onto that product in Shopify, without downloading and uploading them again. In the Shopify admin, a product's page has an **Obelism media** block (add it with the page's ⋯ menu) that lists the files tagged with the product, and **More actions → Add from Obelism media** opens the whole library. **Add to product** puts the file on the product with its alt text, and in the store's other languages when the file has alt text in them. A file is uploaded to the store once: adding it to another product uses the same Shopify file. When the file changes in Media (a new version or new alt text), the block shows it and **Update** replaces Shopify's copy in place, keeping its URL and products. Nothing goes to Shopify unless someone there asks for it.

The products tagged on a file with a point become hotspots in the theme: add the **Shoppable image** app block to the product template in the theme editor, and it shows the product's images that have hotspots, each linking to its product. Images (JPEG, PNG, GIF and WebP) and videos (MP4, MOV and WebM, up to 1 GB and 10 minutes) can be added; a video uploads in the background and shows as processing until Shopify has it ready, and only its alt text updates afterwards.
