E-commerce with Shopify
This starter is a fast, content-driven 11ty site, styled with LESS, edited through Decap CMS, and deployed on Netlify. It doesn't ship a store, but Shopify is happy to own the hard parts, cart, payments, and fulfilment, so adding commerce mostly comes down to pulling product data in and dropping a buy button on a page. There are two ways in: a Buy Button for the quickest path, or the Storefront API to fetch products at build time and render a fully custom storefront.
Two ways to add Shopify
Pick the approach that matches the project. Start with the Buy Button for the quickest path, or go headless with the Storefront API for a fully custom, pre-rendered storefront.
Shopify Buy Button
Drop Shopify's embeddable button into a page. No data fetching, no build step, Shopify owns the cart and checkout.
Jump to setupStorefront API
Fetch products at build time and render them with the kit's own Nunjucks templates for a fast, fully custom storefront.
Jump to setupOption 2: Storefront API
The headless approach: 11ty fetches your catalog once at build time and renders it with its own Nunjucks templates, so product pages ship as static HTML. Shopify still owns the cart and checkout. Everything below covers this path.
How it works
- 1
Connect
Add your Storefront API token to .env
- 2
Fetch
A _data file pulls your catalog at build
- 3
Render
A shop page loops over the products
- 4
Sell
Cart API builds the cart, Shopify hosts checkout
Connecting your store
Generate a Storefront access token and keep it in your environment.
In your Shopify admin, go to Settings → Apps and sales channels → Develop apps → Create an app → Storefront API and grant read access to products. Copy the Storefront access token it generates.
Store it in a .env file, add .env to .gitignore (it isn't ignored by default), and load it with dotenv so the build can read it: npm install dotenv.
# .env , add ".env" to your .gitignore, never commit it SHOPIFY_STORE_DOMAIN=your-store.myshopify.com SHOPIFY_STOREFRONT_TOKEN=your_public_storefront_token SHOPIFY_API_VERSION=2025-01
Deploying on Netlify? Add the same variables under Site settings → Environment variables so production builds can reach Shopify.
The Storefront token is safe in client code
Fetch products at build time
A global data file pulls your catalog once per build.
Add a global data file at src/_data/products.js, following the same pattern as the kit's existing client.js. Because it lives in _data, 11ty runs it once per build and exposes the return value to every template as a products array. Node 18+ (which Eleventy 3 requires) has a global fetch, so no extra HTTP library is needed.
// src/_data/products.js, runs once at build time
require("dotenv").config();
const endpoint = `https://${process.env.SHOPIFY_STORE_DOMAIN}/api/${process.env.SHOPIFY_API_VERSION}/graphql.json`;
const QUERY = `{
products(first: 50) {
edges {
node {
id
title
handle
description
featuredImage { url altText }
priceRange { minVariantPrice { amount currencyCode } }
variants(first: 1) { edges { node { id } } }
}
}
}
}`;
module.exports = async function () {
const res = await fetch(endpoint, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Shopify-Storefront-Access-Token": process.env.SHOPIFY_STOREFRONT_TOKEN,
},
body: JSON.stringify({ query: QUERY }),
});
const { data } = await res.json();
// Flatten the GraphQL edges into a template-friendly shape. Every
// template can now read the result as a global `products` array.
return data.products.edges.map(({ node }) => ({
title: node.title,
handle: node.handle,
description: node.description,
image: node.featuredImage,
price: node.priceRange.minVariantPrice,
variantId: node.variants.edges[0].node.id,
}));
};Extend the query with any fields your store needs from the Storefront API reference.
Render the shop page
Loop over the products with the kit's own page convention.
Create src/content/pages/shop.html: the same convention the starter uses for About, Contact, and the rest. It extends the base layout and loops over the products array to render a card per item, loading a small cart script with <script defer> in its head block.
---
title: "Shop | Eleventy Starter Template"
description: "Browse our products"
permalink: "/shop/"
lang: en
---
{% extends "layouts/base.html" %}
{% block head %}
<link rel="stylesheet" href="/assets/css/shop.css">
<script defer src="/assets/js/shop.js"></script>
{% endblock %}
{% block body %}
<section id="shop">
<ul class="products">
{% for product in products %}
<li class="product">
<img src="{{ product.image.url }}" alt="{{ product.image.altText }}" width="400" height="400">
<h2>{{ product.title }}</h2>
<p>{{ product.price.amount }} {{ product.price.currencyCode }}</p>
<button class="buy" data-variant-id="{{ product.variantId }}">Add to cart</button>
</li>
{% endfor %}
</ul>
</section>
{% endblock %}Use a plain <img> for product images
<img> is right here. The kit's {% image %} shortcode is for optimising local files in src/assets: it can't process a remote URL.Cart & Checkout
Each Add-to-cart button creates a cart and redirects to Shopify.
The button on each product only has to do one thing: create a cart and send the shopper to Shopify's checkout. Add a script at src/assets/js/shop.js (everything under src/assets is passthrough-copied to /assets). It creates a cart with the Storefront Cart API and follows the checkoutUrl it returns.
// src/assets/js/shop.js, passthrough-copied to /assets/js/shop.js
const DOMAIN = "your-store.myshopify.com";
const TOKEN = "your_public_storefront_token"; // Storefront token, safe in client code
const API_VERSION = "2025-01";
async function checkout(variantId) {
const res = await fetch(`https://${DOMAIN}/api/${API_VERSION}/graphql.json`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Shopify-Storefront-Access-Token": TOKEN,
},
body: JSON.stringify({
query: `mutation Create($id: ID!) {
cartCreate(input: { lines: [{ merchandiseId: $id, quantity: 1 }] }) {
cart { checkoutUrl }
}
}`,
variables: { id: variantId },
}),
});
const { data } = await res.json();
// Hand the buyer off to Shopify's hosted, PCI-compliant checkout.
window.location.href = data.cartCreate.cart.checkoutUrl;
}
document.querySelectorAll(".buy").forEach((btn) =>
btn.addEventListener("click", () => checkout(btn.dataset.variantId))
);Pros
- Full control over markup and styling
- Products pre-rendered as static HTML
- Fast and SEO-friendly
Cons
- Requires API integration
- Rebuild needed when products change
