DEV Community

WeInc
WeInc

Posted on

Turning a Shopify store into a full website with Buy Buttons

A lot of small brands start on Shopify and stay there because it works. Checkout, payments, taxes, inventory and order emails are solved. The part that often feels cramped is everything around the products: the story, the long landing pages, the blog, the booking form, the campaign page that needs a layout no theme section offers.

You can keep Shopify for what it does well and build the rest of the site however you like, then drop products into it with Buy Buttons. This post walks through how that works under the hood, when it is worth doing, and the things that tend to go wrong.

What a Buy Button actually is

The Buy Button is a small JavaScript library that Shopify hosts on its CDN. It renders a product card (image, title, price, variant picker, add to cart) and a slide-out cart on any web page. When the shopper clicks checkout, they are sent to the normal Shopify checkout for that store.

Under the hood it talks to the Storefront API, Shopify's public, read-mostly GraphQL API meant for custom storefronts. That is the key idea: your separate site never touches the Admin API from the browser. It only uses a token that is designed to be public.

A minimal embed looks like this:

<div id="product-1"></div>
<script src="https://sdks.shopifycdn.com/buy-button/latest/buy-button-storefront.min.js"></script>
<script>
  const client = ShopifyBuy.buildClient({
    domain: "your-store.myshopify.com",
    storefrontAccessToken: "YOUR_STOREFRONT_TOKEN",
  });

  ShopifyBuy.UI.onReady(client).then((ui) => {
    ui.createComponent("product", {
      id: "1234567890", // numeric product ID
      node: document.getElementById("product-1"),
    });
  });
</script>
Enter fullscreen mode Exit fullscreen mode

Two values matter: the store's myshopify.com domain and a Storefront access token. The product ID is the numeric ID you can see in the admin URL of the product.

Where the Storefront token comes from

If you are setting this up for one store by hand, the Buy Button sales channel in the Shopify admin will generate the embed code and token for you. That is the simplest route.

If you are building an app that does this for many stores, you create the token yourself through the Admin GraphQL API, using the store's admin access token on your server:

mutation CreateStorefrontToken($input: StorefrontAccessTokenInput!) {
  storefrontAccessTokenCreate(input: $input) {
    storefrontAccessToken { accessToken }
    userErrors { message }
  }
}
Enter fullscreen mode Exit fullscreen mode

Your app needs the right scopes for this to work. The ones that matter for a Buy Button are the unauthenticated_* family, which describe what the Storefront token is allowed to do on behalf of anonymous shoppers:

  • unauthenticated_read_product_listings so the token can read products and prices.
  • unauthenticated_read_checkouts and unauthenticated_write_checkouts so it can create and read carts.

Plus something like read_products if your own app needs to list the catalog in an admin screen.

Notice what is absent: nothing about customers, orders or payments. That is deliberate. The Storefront token sits in page source where anyone can read it, so it should only be able to do what an anonymous shopper on the store could already do.

Two habits that save trouble later:

  1. Reuse, don't multiply. A store can only hold a limited number of Storefront tokens. Before creating one, list the existing tokens (shop { storefrontAccessTokens(first: 50) { ... } }) and reuse yours if it is there. Give it a recognisable title so you can find it.
  2. Validate what you got back. Check that the mutation returned a token and no userErrors before you write it into a page. A silent empty string produces a Buy Button that renders nothing, with no obvious error.

The flow from click to order

The order of events is worth spelling out, because it explains most of the edge cases:

  1. Your page loads the Buy Button script and fetches product data through the Storefront API.
  2. The shopper picks a variant and adds it to the cart. The cart lives on Shopify; the script keeps a reference in the browser.
  3. On checkout, the shopper leaves your domain and lands on the store's Shopify checkout.
  4. Payment, tax, shipping rates, discount codes and the order confirmation all happen on Shopify, exactly as they would from a theme.

Because checkout never runs on your site, you are not handling card data, and your site is not in PCI scope for it. Orders show up in the Shopify admin like any other order, attributed to the Buy Button channel.

When a separate site beats a theme

A theme is still the right answer for most stores. A separate site with Buy Buttons is worth it when:

  • Products are a small part of the site. A studio, a course creator, a restaurant selling a few jars of sauce, a band selling merch. The site is mostly content, and a full store theme is overkill.
  • You need layouts the theme fights. Long-form launch pages, interactive explainers, or a design system you already use elsewhere.
  • The site already exists. Adding commerce to a working marketing site is easier than migrating it into Liquid.
  • You sell in several places. The same product can appear on a blog post, a landing page and a partner microsite, all feeding one inventory.

It is the wrong choice when you rely heavily on theme apps (reviews widgets, bundles, upsell apps). Most of those inject into theme templates and will not appear on your separate site.

Pitfalls

Embedding the wrong token. The Admin API token must never reach the browser. If you see shpat_ style tokens in front-end code, stop. Only the Storefront token belongs in page source.

Hard-coding product IDs without a fallback. If a product is deleted or unpublished from the Buy Button channel, the component renders empty. Keep a plain "View on store" link next to each embed, pointing at https://your-store.myshopify.com/products/handle. If the script fails, people can still buy.

Forgetting the sales channel. Products must be available to the channel your token belongs to. A product that is active in the store but not published to that channel will not load through the Storefront API.

Prices that look stale. The Buy Button fetches live prices, but any price you also typed into your own copy will not update. Let the component show the price and keep it out of your headings.

Two domains, two analytics stories. The shopper browses on your domain and pays on Shopify's. Set up cross-domain tracking, or accept that your site analytics will see "clicked checkout" and Shopify will see "order placed", and join them by date rather than by session.

Expiring admin tokens in apps. If you are building an app, newer Shopify installs hand you expiring offline tokens (around an hour) plus a refresh token. That only matters for your server-side admin calls, like creating the Storefront token. The Storefront token itself does not expire this way, so pages you already published keep working.

Script loading order. If your site is a single-page app, the #product-1 node might not exist yet when the script runs. Create components after the element mounts, and clean them up when it unmounts, or you will get duplicate cards on route changes.

A practical setup

For a small brand, my default is:

  • Shopify stays the source of truth for products, stock and orders.
  • The website is whatever is fastest to edit for the people running it.
  • Each product section has a Buy Button plus a plain text link to the product on the Shopify store.
  • One Storefront token per store, created once, reused, and scoped to the unauthenticated_* permissions only.

We built this flow into the Shopify app for WeInc (we.inc), where a merchant installs it, the app creates the Storefront token, and the generated site gets Buy Buttons for the store's products. The same pattern works fine by hand with the Buy Button channel and a static site, which is a good way to test it before committing to any tooling.

The main thing to remember: Shopify keeps the money part, your site keeps the story. As long as the token you embed can only do what an anonymous shopper can do, the split is safe and simple.

Top comments (0)