2.0 Beta

Smootify 2.0 is in public beta. Your 1.x store keeps working. Move when you want.

What changed

Content from Shopify

Read products, collections, vendors, blogs, articles, pages and metaobjects into Astro content collections, at build time or on every request, with the fields each one has.

v2.0 beta
Open .md
On this page
  1. The loaders
  2. 1. Build or live
  3. 2. Build collections
  4. 3. Live collections
  5. 4. Fields
  6. 5. SEO helpers
  7. Before you publish

Smootify turns your Shopify store into typed Astro content collections. A loader fetches the content, and getCollection, getEntry and getEntries work across it: a product knows its vendor and collections, an article knows its blog. You choose, for each collection, between loading it at build time and loading it on each request.

Note

The site needs its siteId in smootify(): see Install Smootify in Astro. The loaders read the store and the Storefront token from the site.

The loaders

ShopifyBuild loaderLive loader
ProductssmootifyProductssmootifyLiveProducts
CollectionssmootifyCollectionssmootifyLiveCollections
VendorssmootifyVendorssmootifyLiveVendors
BlogssmootifyBlogssmootifyLiveBlogs
ArticlessmootifyArticlessmootifyLiveArticles
PagessmootifyPagessmootifyLivePages
MetaobjectssmootifyMetaobjects({ type })smootifyLiveMetaobjects({ type })

Build loaders come from @smootify/astro, live loaders from @smootify/astro/live. Vendors are not a Shopify object: they are derived from the vendor of the products.

Products, collections and vendors need the Storefront scope unauthenticated_read_product_listings. Blogs, articles and pages need unauthenticated_read_content, and metaobjects unauthenticated_read_metaobjects. The token of a Smootify site does not have the content scope: for blogs, articles and pages, pass your own store and storefrontToken. A metaobject definition needs Storefront access in Shopify.

1. Build or live

BuildLive
Wheresrc/content.config.tssrc/live.config.ts
Pagestatic: getStaticPathson demand: prerender = false
Read withgetCollection, getEntrygetLiveCollection, getLiveEntry
Data isas of the last buildas of the request
Costnone at request timeone Storefront call for each request that is not cached

Choose build for a catalog and content that change rarely, or that you rebuild when the store changes. Choose live when prices, stock or content must be right on the server at the moment of the request. You can mix them: static catalog pages from the build collections, and a live product only on the product page.

Prices and stock in the browser stay fresh either way: Smootify updates them at runtime. Live collections are for the HTML the server sends.

2. Build collections

// src/content.config.ts
import { defineCollection } from "astro:content";
import { smootifyProducts, smootifyCollections, smootifyVendors } from "@smootify/astro";

const siteId = "<24-character Smootify site id>";

export const collections = {
  products: defineCollection({ loader: smootifyProducts({ siteId, metafields: ["material"] }) }),
  collections: defineCollection({ loader: smootifyCollections({ siteId, withProducts: true }) }),
  vendors: defineCollection({ loader: smootifyVendors({ siteId }) }),
};

A page for each entry, with getStaticPaths:

---
// src/pages/products/[slug].astro
import { getCollection, getEntry, getEntries } from "astro:content";

export async function getStaticPaths() {
  const products = await getCollection("products");
  return products.map((product) => ({ params: { slug: product.id }, props: { product } }));
}
const { product } = Astro.props;
const p = product.data;
const vendor = p.vendor ? await getEntry(p.vendor) : undefined;
const collections = (await getEntries(p.collections)).filter(Boolean);
---
<h1>{p.name}</h1>
<p>{p.priceFormatted}</p>

An entry that points to something not in the store, such as a product that is not published to the Storefront API channel, resolves to undefined. Filter it out.

The loaders have these options in common:

OptionDefaultWhat it does
siteIdSMOOTIFY_SITE_IDCredentials of the site.
store, storefrontTokennoneYour own store and Storefront token. They win over siteId.
country, languagethe store’s defaultsPrices and content of that market and language.
metafieldsnoneMetafields to load, as "material", "ns.key" or { namespace, key }.
formatMoneythe runtime’s formatYour own (amount, currencyCode) => string for the *Formatted fields.
withProductsfalseCollections only: add the ordered products of each collection.

A metafield needs Storefront access on its definition. In the entry, it is data.metafields.<key>, parsed by its type: booleans, numbers, JSON and lists become values, a file reference becomes a URL, a product or collection reference becomes a handle.

On the next build, unchanged entries are not rewritten and entries that left Shopify are removed.

3. Live collections

// src/live.config.ts
import { defineLiveCollection } from "astro:content";
import { smootifyLiveProducts, smootifySchemas } from "@smootify/astro/live";

const siteId = "<24-character Smootify site id>";

export const collections = {
  products: defineLiveCollection({
    loader: smootifyLiveProducts({ siteId }),
    schema: smootifySchemas.product({ siteId }),
  }),
};
---
// src/pages/products/[slug].astro
import { getLiveEntry } from "astro:content";

export const prerender = false;

const { entry, error, cacheHint } = await getLiveEntry("products", { id: Astro.params.slug! });
if (error) return new Response("Shopify error", { status: 502 });
if (!entry) return new Response(null, { status: 404 });

Astro.cache.set(cacheHint);
Astro.cache.set({ maxAge: 300, swr: 60 });
---
<h1>{entry.data.name}</h1>
<p>{entry.data.priceFormatted}</p>
  • Errors come back as { error }, never thrown. A missing entry is entry: undefined with no error.
  • cacheHint holds the tags and the last-modified date of the entry. maxAge and swr are yours to set. The tags are what lets a webhook refresh just the pages it touches: see Deploy to Cloudflare.
  • On demand needs the cache provider in astro.config.mjs: cache: { provider: cacheCloudflare() }, with cacheCloudflare from @astrojs/cloudflare/cache.
  • The id is the same as in the build collection.

The products of a collection are not refs you can resolve with getEntries. Ask for them with the collection filter, all of them, in the collection’s order:

const { entries: products } = await getLiveCollection("products", { collection: handle });

getLiveCollection takes filters: first on all loaders; query, sortKey, reverse and collection on products; blog on articles. A live vendor lookup pages through the whole catalog on every call, so keep that page behind the cache.

4. Fields

The three main collections have these fields in data.

Products

  • Core. name, slug (the cleaned handle, and the entry id), shopifyId (the full gid), description (HTML), image, imageAlt, isGiftCard, productType, seoTitle, seoDescription.
  • Prices. price, priceFormatted, compareAtPrice, compareAtPriceFormatted (the first variant), priceRange, compareAtPriceRange, currencyCode. Formatted values follow the store’s money format with the market’s symbol, such as €380,00, so the page does not change when Smootify starts.
  • Links. vendor (a reference to the vendor), collections (references).
  • Catalog. options, variants, images, media, tags, availableForSale, totalInventory, defaultVariantId, vendorName, handle, seo, metafields.

Collections

  • name, slug, shopifyId, description, image, imageAlt, seoTitle, seoDescription, metafields, seo.
  • products (references in the collection’s order) only with withProducts: true.

Vendors

  • name, slug, products (references) and productCount.

Blogs, articles, pages and metaobjects follow the same pattern: name, handle, shopifyId, seo and the content of the type (contentHtml, author and blog for an article; body for a page; fields for a metaobject, parsed by Shopify type). Product references in a metaobject become references to the product collection.

A metaobject loader is one collection for one type. The Storefront API does not expose its definition, so pass a schema (a Zod object) for fields, or the fields stay untyped.

5. SEO helpers

Every page puts <Seo> in its <head>. It writes the title, description, canonical, Open Graph and Twitter tags and the JSON-LD of the type:

---
import Seo from "@smootify/astro/components/Seo.astro";
---
<head>
  <Seo type="product" entry={product} siteName="My shop" />
</head>

The types are home, product, collection, vendor, article, blog, page and other (use noindex on pages that must not be indexed). The JSON-LD functions (productJsonLd, collectionJsonLd, breadcrumbJsonLd, articleJsonLd) are also exported. Canonical and alternate addresses follow Astro’s trailingSlash.

Before you publish

Check these first

  • A blog, article or page loader fails with ACCESS_DENIED. The Smootify token has no content scope. Pass your own store and storefrontToken with unauthenticated_read_content.
  • A metaobject comes back empty. The definition has no Storefront access in Shopify.
  • A metafield is missing from data.metafields. It is not in the metafields option, it has no value, or its definition lacks Storefront access. Two namespaces with the same key collide: the last wins.
  • A live page shows old data. It has no maxAge, or it reads a build collection. Use getLiveEntry on pages that must be fresh.
  • A reference crashes the page. It points to something not published to the Storefront API. Filter the result of getEntries with .filter(Boolean) and check a vendor before you read it.
  • A product is missing. Two products with the same cleaned slug: the first wins.

    ↑ ↓ to moveEnter to openEsc to close