From b3439f75793a4d1001bad86b6859e5fefa12e5f7 Mon Sep 17 00:00:00 2001 From: Bobby Nguyen Date: Thu, 3 Sep 2026 12:41:12 -0400 Subject: [PATCH 1/7] Hand off cart permalinks for every mock.shop host to the demo store mock.shop serves many stores, each on its own host (pets.mock.shop, ...), and none of them renders cart permalinks. The checkout interceptor only special-cased the bare mock.shop host, so a storefront pointed at a per-store host sent buyers to a 404. Treat every *.mock.shop host the same way and redirect to demostore.mock.shop. Assisted-By: devx/c47f963e-a81e-4fb1-b892-875281ad7726 --- .changeset/mock-shop-store-catalog.md | 5 +++++ .../request-routing/interceptors/checkout.test.ts | 13 +++++++++++++ .../core/request-routing/interceptors/checkout.ts | 8 +++++++- 3 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 .changeset/mock-shop-store-catalog.md diff --git a/.changeset/mock-shop-store-catalog.md b/.changeset/mock-shop-store-catalog.md new file mode 100644 index 0000000000..ae2132760c --- /dev/null +++ b/.changeset/mock-shop-store-catalog.md @@ -0,0 +1,5 @@ +--- +"@shopify/hydrogen": patch +--- + +Cart permalinks now hand off to the mock.shop demo store for every mock.shop host, not only `mock.shop` itself, so a storefront built against a per-store host such as `pets.mock.shop` behaves the same in mock mode. The `hydrogen-storefront-client` and `hydrogen-setup` skills now explain that mock.shop is a catalog of stores and how to pick one from https://mock.shop/llms.txt. diff --git a/packages/hydrogen/src/core/request-routing/interceptors/checkout.test.ts b/packages/hydrogen/src/core/request-routing/interceptors/checkout.test.ts index 9e42231046..ea81198012 100644 --- a/packages/hydrogen/src/core/request-routing/interceptors/checkout.test.ts +++ b/packages/hydrogen/src/core/request-routing/interceptors/checkout.test.ts @@ -205,6 +205,19 @@ describe("handleCheckoutRedirect", () => { expect(mockFetch).not.toHaveBeenCalled(); }); + it("redirects cart permalinks for a per-store mock.shop host to the demo store", async () => { + const result = await handleCheckoutRedirect( + new Request("https://my-app.com/cart/123:2?payment=shop_pay&source=hydrogen"), + { storeDomain: "pets.mock.shop" }, + ); + + expect(result?.status).toBe(302); + expect(result?.headers.get("location")).toBe( + "https://demostore.mock.shop/cart/123:2?payment=shop_pay&source=hydrogen", + ); + expect(mockFetch).not.toHaveBeenCalled(); + }); + it("merges checkout URL search params into variant cart permalinks when a cart exists", async () => { mockFetch.mockResolvedValueOnce( mockGqlResponse({ diff --git a/packages/hydrogen/src/core/request-routing/interceptors/checkout.ts b/packages/hydrogen/src/core/request-routing/interceptors/checkout.ts index 7233ff3f3b..e6167d8e60 100644 --- a/packages/hydrogen/src/core/request-routing/interceptors/checkout.ts +++ b/packages/hydrogen/src/core/request-routing/interceptors/checkout.ts @@ -84,7 +84,13 @@ async function getCartRedirectUrl( function getCartPermalinkOrigin(storeUrl: string): string { const url = new URL(storeUrl); - return url.hostname === "mock.shop" ? MOCK_SHOP_CART_PERMALINK_ORIGIN : url.origin; + return isMockShopHost(url.hostname) ? MOCK_SHOP_CART_PERMALINK_ORIGIN : url.origin; +} + +// mock.shop serves many stores, each on its own host (pets.mock.shop, ...), and +// none of them renders cart permalinks, so every mock host hands off to the demo store. +function isMockShopHost(hostname: string): boolean { + return hostname === "mock.shop" || hostname.endsWith(".mock.shop"); } function mergeSearchParams(target: URL, source: URLSearchParams): void { From 66f7158f948e70b85157cb3ab0d348f07d176c85 Mon Sep 17 00:00:00 2001 From: Bobby Nguyen Date: Thu, 3 Sep 2026 12:41:14 -0400 Subject: [PATCH 2/7] React Router template: pick a mock.shop store with PUBLIC_STORE_DOMAIN Mock mode hard-coded the default store at mock.shop. mock.shop is a catalog of stores on their own hosts, so let a mock.shop host in PUBLIC_STORE_DOMAIN select that store's catalog (and imply mock mode). Document the directory at https://mock.shop/llms.txt in .env.example, the README, and AGENTS.md so developers and agents know they can choose. Assisted-By: devx/c47f963e-a81e-4fb1-b892-875281ad7726 --- templates/react-router/.env.example | 5 +++ templates/react-router/AGENTS.md | 9 ++++++ templates/react-router/README.md | 9 +++++- templates/react-router/__test__/shop.test.ts | 26 ++++++++++++++- templates/react-router/app/lib/shop.ts | 34 ++++++++++++++++---- templates/react-router/app/lib/storefront.ts | 3 +- 6 files changed, 77 insertions(+), 9 deletions(-) diff --git a/templates/react-router/.env.example b/templates/react-router/.env.example index e114a54458..01537e83b3 100644 --- a/templates/react-router/.env.example +++ b/templates/react-router/.env.example @@ -8,6 +8,11 @@ # Force the tokenless mock.shop demo (also the default when no token is set). # MOCK_SHOP=1 +# +# mock.shop is many stores, each on its own host. In mock mode, set +# PUBLIC_STORE_DOMAIN to one of the hosts listed at https://mock.shop/llms.txt +# (for example pets.mock.shop) to build against that store's catalog instead of +# the default apparel store at mock.shop. # Real store. Set all three for real-store mode. On Oxygen, a linked storefront # injects these for you. diff --git a/templates/react-router/AGENTS.md b/templates/react-router/AGENTS.md index 8ef9c22e0d..cc6b15cb26 100644 --- a/templates/react-router/AGENTS.md +++ b/templates/react-router/AGENTS.md @@ -3,3 +3,12 @@ This storefront is scaffolded from Shopify's Hydrogen React Router template. See the README for framework-specific details. Use the [Shopify AI Toolkit](https://shopify.dev/docs/apps/build/ai-toolkit) for all Shopify API and platform work. If missing, install it in the agent host per that page (or `npx skills add Shopify/shopify-ai-toolkit --list` for skill-compatible hosts). + +## No store yet? + +Until a store is connected, this project reads [mock.shop](https://mock.shop): a public, auth-free Storefront API backed by fictional stores. It isn't one store but many, each on its own host with its own catalog. + +- The directory at https://mock.shop/llms.txt lists every store with what it sells and its API URL. The default, `mock.shop` itself, is apparel basics. +- To build against a different store, set `PUBLIC_STORE_DOMAIN` in `.env` to that store's host (for example `pets.mock.shop`) and leave `PRIVATE_STOREFRONT_API_TOKEN` empty. Each store describes its own catalog at `https://.mock.shop/llms.txt`. +- Carts work; checkout doesn't, and the Customer Account API isn't available. +- To connect a real store, set `PUBLIC_STORE_DOMAIN`, `PUBLIC_STOREFRONT_ID`, and `PRIVATE_STOREFRONT_API_TOKEN` in `.env` (see `.env.example`). On Oxygen, a linked storefront injects them for you. diff --git a/templates/react-router/README.md b/templates/react-router/README.md index b1a2e4e95f..647694a9da 100644 --- a/templates/react-router/README.md +++ b/templates/react-router/README.md @@ -42,6 +42,12 @@ cp .env.example .env npm run dev ``` +`mock.shop` is a catalog of fictional stores, each on its own host. The default at +`mock.shop` sells apparel basics; the directory at +[mock.shop/llms.txt](https://mock.shop/llms.txt) lists every other store with what it +sells. To build against one of them, set `PUBLIC_STORE_DOMAIN` to its host (for +example `pets.mock.shop`) and leave the token empty. + **Against a real store** — set your store domain, storefront ID, and a **private** Storefront API token, then run normally: @@ -63,7 +69,8 @@ app talks to the real store (`PUBLIC_STORE_DOMAIN`, falling back to the default `app/lib/shop.ts`); with none it falls back to the `mock.shop` demo, so a fresh deploy always renders. **On Oxygen, a linked storefront injects these env vars automatically** — the deployed site connects to your store with no extra config -(and shows the `mock.shop` demo until it's linked). `MOCK_SHOP=1` forces mock. +(and shows the `mock.shop` demo until it's linked). `MOCK_SHOP=1` forces mock, and so +does a `mock.shop` host in `PUBLIC_STORE_DOMAIN`. (`mock.shop` and the Hydrogen Preview store are different data sources.) ## Scripts diff --git a/templates/react-router/__test__/shop.test.ts b/templates/react-router/__test__/shop.test.ts index 3b3d1578c5..82bfeb4e76 100644 --- a/templates/react-router/__test__/shop.test.ts +++ b/templates/react-router/__test__/shop.test.ts @@ -1,7 +1,7 @@ import assert from "node:assert/strict"; import test from "node:test"; -import { shouldUseMockShop } from "../app/lib/shop.ts"; +import { getMockShopDomain, isMockShopDomain, shouldUseMockShop } from "../app/lib/shop.ts"; test("uses a real store only when credentials exist and mock mode is not forced", () => { assert.equal(shouldUseMockShop({}), true); @@ -10,4 +10,28 @@ test("uses a real store only when credentials exist and mock mode is not forced" shouldUseMockShop({ MOCK_SHOP: "1", PRIVATE_STOREFRONT_API_TOKEN: "private-token" }), true, ); + assert.equal( + shouldUseMockShop({ + PRIVATE_STOREFRONT_API_TOKEN: "private-token", + PUBLIC_STORE_DOMAIN: "pets.mock.shop", + }), + true, + ); +}); + +test("recognizes the default mock.shop store and per-store mock.shop hosts", () => { + assert.equal(isMockShopDomain("mock.shop"), true); + assert.equal(isMockShopDomain("pets.mock.shop"), true); + assert.equal(isMockShopDomain("hydrogen-preview.myshopify.com"), false); + assert.equal(isMockShopDomain("notmock.shop"), false); + assert.equal(isMockShopDomain(undefined), false); +}); + +test("mock mode reads the store from PUBLIC_STORE_DOMAIN when it is a mock.shop host", () => { + assert.equal(getMockShopDomain({}), "mock.shop"); + assert.equal(getMockShopDomain({ PUBLIC_STORE_DOMAIN: "pets.mock.shop" }), "pets.mock.shop"); + assert.equal( + getMockShopDomain({ PUBLIC_STORE_DOMAIN: "hydrogen-preview.myshopify.com" }), + "mock.shop", + ); }); diff --git a/templates/react-router/app/lib/shop.ts b/templates/react-router/app/lib/shop.ts index 610d8dfc63..d06e6c7b5d 100644 --- a/templates/react-router/app/lib/shop.ts +++ b/templates/react-router/app/lib/shop.ts @@ -6,7 +6,10 @@ // present. On Oxygen, a linked storefront injects PRIVATE_STOREFRONT_API_TOKEN // and PUBLIC_STORE_DOMAIN; for local real-store dev set them in `.env`. // • mock.shop — the tokenless fallback used when no token is present (so a -// fresh deploy always renders), and forced explicitly by MOCK_SHOP=1. +// fresh deploy always renders), forced explicitly by MOCK_SHOP=1, and implied +// by a mock.shop host in PUBLIC_STORE_DOMAIN. mock.shop is many stores, each +// on its own host (directory: https://mock.shop/llms.txt); PUBLIC_STORE_DOMAIN +// picks one (e.g. pets.mock.shop), otherwise the default store at mock.shop. // // `storeDomain` below is the default used only when PUBLIC_STORE_DOMAIN is unset. // It points at Shopify's public Hydrogen Preview store as an EXAMPLE — replace it @@ -18,13 +21,32 @@ export const storefrontConfig = { i18n: { country: "US", language: "EN" }, } as const; -// Real store iff a private Storefront API token is available; otherwise the -// tokenless mock.shop demo. MOCK_SHOP=1 forces mock (used by the gate + as the -// zero-config default). +// Real store iff a private Storefront API token is available and the store isn't +// a mock.shop host; otherwise the tokenless mock.shop demo. MOCK_SHOP=1 forces +// mock (used by the gate + as the zero-config default). export function shouldUseMockShop( - env: Pick, + env: Pick, ): boolean { - return env.MOCK_SHOP === "1" || !env.PRIVATE_STOREFRONT_API_TOKEN; + return ( + env.MOCK_SHOP === "1" || + !env.PRIVATE_STOREFRONT_API_TOKEN || + isMockShopDomain(env.PUBLIC_STORE_DOMAIN) + ); +} + +// mock.shop is a catalog of stores, not one store. The default store at mock.shop +// sells apparel basics; every other store lives on its own host (pets.mock.shop, +// snowboards.mock.shop, ...) and is listed at https://mock.shop/llms.txt. +export const MOCK_SHOP_DOMAIN = "mock.shop"; + +export function isMockShopDomain(domain: string | undefined): domain is string { + return domain === MOCK_SHOP_DOMAIN || Boolean(domain?.endsWith(`.${MOCK_SHOP_DOMAIN}`)); +} + +// Store domain for mock mode: a mock.shop host in PUBLIC_STORE_DOMAIN selects that +// store's catalog; anything else means the default store. +export function getMockShopDomain(env: Pick): string { + return isMockShopDomain(env.PUBLIC_STORE_DOMAIN) ? env.PUBLIC_STORE_DOMAIN : MOCK_SHOP_DOMAIN; } // Store domain for real-store mode: prefer the worker env (Oxygen injects diff --git a/templates/react-router/app/lib/storefront.ts b/templates/react-router/app/lib/storefront.ts index 3af6dd5489..f1d55d9124 100644 --- a/templates/react-router/app/lib/storefront.ts +++ b/templates/react-router/app/lib/storefront.ts @@ -11,6 +11,7 @@ import type { Env } from "~/lib/env"; import { DEVELOPMENT_BUYER_IP, getBuyerIp, + getMockShopDomain, getPrivateStorefrontToken, getStoreDomain, storefrontConfig, @@ -39,7 +40,7 @@ export function createRequestStorefrontClient( buyerIp, }); - const storeDomain = usingMockShop ? "mock.shop" : getStoreDomain(env); + const storeDomain = usingMockShop ? getMockShopDomain(env) : getStoreDomain(env); const privateStorefrontToken = usingMockShop ? "mock-private-token" : getPrivateStorefrontToken(env); From 41420bf062025e3f2b5851beae86a22c319226ca Mon Sep 17 00:00:00 2001 From: Bobby Nguyen Date: Thu, 3 Sep 2026 12:41:16 -0400 Subject: [PATCH 3/7] Next.js template: pick a mock.shop store with NEXT_PUBLIC_STORE_DOMAIN Same change as the React Router template: a mock.shop host in NEXT_PUBLIC_STORE_DOMAIN selects that store's catalog and keeps the app in mock mode (no Customer Accounts, well-known mock token). The warning now names the store in use and points at the directory. Assisted-By: devx/c47f963e-a81e-4fb1-b892-875281ad7726 --- templates/nextjs/.env.example | 5 +++++ templates/nextjs/AGENTS.md | 9 ++++++++ templates/nextjs/README.md | 2 ++ templates/nextjs/lib/storefront-config.ts | 25 +++++++++++++++++------ 4 files changed, 35 insertions(+), 6 deletions(-) diff --git a/templates/nextjs/.env.example b/templates/nextjs/.env.example index 2c0f78b3ff..0e71bc8845 100644 --- a/templates/nextjs/.env.example +++ b/templates/nextjs/.env.example @@ -6,6 +6,11 @@ SESSION_SECRET= SITE_ORIGIN=http://localhost:3000 # Browser-safe public storefront identity. Next.js inlines NEXT_PUBLIC_* values. +# +# Without a PRIVATE_STOREFRONT_API_TOKEN the app reads mock.shop, which is many +# stores on their own hosts (directory: https://mock.shop/llms.txt). Set +# NEXT_PUBLIC_STORE_DOMAIN to one of those hosts (for example pets.mock.shop) to +# build against that catalog instead of the default apparel store at mock.shop. NEXT_PUBLIC_STORE_DOMAIN= NEXT_PUBLIC_STOREFRONT_API_TOKEN= NEXT_PUBLIC_SHOP_ID= diff --git a/templates/nextjs/AGENTS.md b/templates/nextjs/AGENTS.md index fe753497fc..96b9a3e67e 100644 --- a/templates/nextjs/AGENTS.md +++ b/templates/nextjs/AGENTS.md @@ -13,3 +13,12 @@ This block is written and re-added by `next dev` — verify at `node_modules/nex This storefront is scaffolded from Shopify's Hydrogen Next.js template. See the README for framework-specific details. Use the [Shopify AI Toolkit](https://shopify.dev/docs/apps/build/ai-toolkit) for all Shopify API and platform work. If missing, install it in the agent host per that page (or `npx skills add Shopify/shopify-ai-toolkit --list` for skill-compatible hosts). + +## No store yet? + +Until a store is connected, this project reads [mock.shop](https://mock.shop): a public, auth-free Storefront API backed by fictional stores. It isn't one store but many, each on its own host with its own catalog. + +- The directory at https://mock.shop/llms.txt lists every store with what it sells and its API URL. The default, `mock.shop` itself, is apparel basics. +- To build against a different store, set `NEXT_PUBLIC_STORE_DOMAIN` in `.env` to that store's host (for example `pets.mock.shop`) and leave `PRIVATE_STOREFRONT_API_TOKEN` empty. Each store describes its own catalog at `https://.mock.shop/llms.txt`. +- Carts work; checkout doesn't, and the Customer Account API isn't available. +- To connect a real store, set `PRIVATE_STOREFRONT_API_TOKEN` and `NEXT_PUBLIC_STORE_DOMAIN` in `.env` (see `.env.example`). diff --git a/templates/nextjs/README.md b/templates/nextjs/README.md index 1722294833..8609941c1e 100644 --- a/templates/nextjs/README.md +++ b/templates/nextjs/README.md @@ -58,6 +58,8 @@ Public values: If `PRIVATE_STOREFRONT_API_TOKEN` is unset, the app uses `mock.shop`. If you set a private token, you must also set `NEXT_PUBLIC_STORE_DOMAIN`. +`mock.shop` is a catalog of fictional stores, each on its own host. The default at `mock.shop` sells apparel basics; the directory at [mock.shop/llms.txt](https://mock.shop/llms.txt) lists every other store with what it sells. Set `NEXT_PUBLIC_STORE_DOMAIN` to one of those hosts (for example `pets.mock.shop`) to build against that store's catalog while staying in mock mode. + ## Scripts | Script | Does | diff --git a/templates/nextjs/lib/storefront-config.ts b/templates/nextjs/lib/storefront-config.ts index e37427765f..3ad6082031 100644 --- a/templates/nextjs/lib/storefront-config.ts +++ b/templates/nextjs/lib/storefront-config.ts @@ -14,10 +14,21 @@ import { getOptionalPrivateStorefrontToken } from "./env"; * to the public mock.shop endpoint using its well-known `mock-private-token`. * With a real private token present, `NEXT_PUBLIC_STORE_DOMAIN` must identify * the store. + * + * mock.shop is a catalog of fictional stores, not one store. The default store at + * `mock.shop` sells apparel basics; every other store lives on its own host + * (pets.mock.shop, ...) and is listed at https://mock.shop/llms.txt. Setting + * `NEXT_PUBLIC_STORE_DOMAIN` to one of those hosts selects that catalog and keeps + * the app in mock mode. */ export const MOCK_SHOP_DOMAIN = "mock.shop"; export const MOCK_SHOP_PRIVATE_TOKEN = "mock-private-token"; + +export function isMockShopDomain(domain: string): boolean { + return domain === MOCK_SHOP_DOMAIN || domain.endsWith(`.${MOCK_SHOP_DOMAIN}`); +} + const SESSION_SECRET_MIN_LENGTH = 32; export type ResolvedStorefrontConfig = { @@ -37,7 +48,7 @@ let mockShopFallbackWarned = false; export function isCustomerAccountsAvailable(): boolean { const { storeDomain } = resolveStorefrontConfig(); return ( - storeDomain !== MOCK_SHOP_DOMAIN && + !isMockShopDomain(storeDomain) && Boolean(process.env.NEXT_PUBLIC_SHOP_ID) && Boolean(process.env.NEXT_PUBLIC_CUSTOMER_ACCOUNT_API_CLIENT_ID) && Boolean(process.env.SITE_ORIGIN) && @@ -49,23 +60,25 @@ export function isCustomerAccountsAvailable(): boolean { export function resolveStorefrontConfig(): ResolvedStorefrontConfig { const privateStorefrontToken = getOptionalPrivateStorefrontToken(); + const configuredDomain = storefrontConfig.storeDomain; - if (!privateStorefrontToken) { + if (!privateStorefrontToken || isMockShopDomain(configuredDomain)) { + const storeDomain = isMockShopDomain(configuredDomain) ? configuredDomain : MOCK_SHOP_DOMAIN; if (!mockShopFallbackWarned) { mockShopFallbackWarned = true; console.warn( - `[hydrogen-template-nextjs] No PRIVATE_STOREFRONT_API_TOKEN found — ` + - `running against mock.shop (${MOCK_SHOP_DOMAIN}). Set ` + + `[hydrogen-template-nextjs] Running against mock.shop (${storeDomain}). ` + + `Other mock stores are listed at https://mock.shop/llms.txt. Set ` + `PRIVATE_STOREFRONT_API_TOKEN and NEXT_PUBLIC_STORE_DOMAIN to hit a real store.`, ); } return { - storeDomain: MOCK_SHOP_DOMAIN, + storeDomain, privateStorefrontToken: MOCK_SHOP_PRIVATE_TOKEN, }; } - const storeDomain = storefrontConfig.storeDomain; + const storeDomain = configuredDomain; if (!storeDomain) { throw new Error( "NEXT_PUBLIC_STORE_DOMAIN is required when PRIVATE_STOREFRONT_API_TOKEN is set.", From 5196aac91b4526153e5029491fd38067ddc8ed13 Mon Sep 17 00:00:00 2001 From: Bobby Nguyen Date: Thu, 3 Sep 2026 12:41:17 -0400 Subject: [PATCH 4/7] Skills: explain that mock.shop is a catalog of stores The storefront-client and setup skills described mock.shop as a single tokenless endpoint. Teach agents to read https://mock.shop/llms.txt, pick the store whose categories match what the user is building, and use its host as storeDomain. Assisted-By: devx/c47f963e-a81e-4fb1-b892-875281ad7726 --- packages/hydrogen/skills/hydrogen-setup/steps/2-scaffold.md | 2 ++ packages/hydrogen/skills/hydrogen-storefront-client/SKILL.md | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/packages/hydrogen/skills/hydrogen-setup/steps/2-scaffold.md b/packages/hydrogen/skills/hydrogen-setup/steps/2-scaffold.md index 2fc12d480e..6c9db39520 100644 --- a/packages/hydrogen/skills/hydrogen-setup/steps/2-scaffold.md +++ b/packages/hydrogen/skills/hydrogen-setup/steps/2-scaffold.md @@ -35,6 +35,8 @@ If the framework requires a prefix to expose client-side variables, preserve the List `PUBLIC_STOREFRONT_API_TOKEN` in the app's env example file as a commented-out entry (`# PUBLIC_STOREFRONT_API_TOKEN=`). The Storefront client accepts `undefined` as tokenless access, which is all mock.shop supports, so the scaffold works before the user has tokens and upgrades in place when they add one. Do not write an uncommented empty assignment (`PUBLIC_STOREFRONT_API_TOKEN=`): env loaders parse that as an empty string, and the client rejects empty tokens. Recommend filling it in (or switching to a private client) once the app targets a real store. +When the user has no store yet, point `PUBLIC_STORE_DOMAIN` at a mock.shop store. `mock.shop` is the default apparel catalog; https://mock.shop/llms.txt lists every other fictional store with its host (for example `pets.mock.shop`) and what it sells. Pick the store closest to what the user is building, and see the `hydrogen-storefront-client` skill for the details. + ### Continue when - [ ] Env vars follow the canonical names (plus any required framework prefix) diff --git a/packages/hydrogen/skills/hydrogen-storefront-client/SKILL.md b/packages/hydrogen/skills/hydrogen-storefront-client/SKILL.md index 9e5f1c4080..dc846fa1b5 100644 --- a/packages/hydrogen/skills/hydrogen-storefront-client/SKILL.md +++ b/packages/hydrogen/skills/hydrogen-storefront-client/SKILL.md @@ -43,6 +43,10 @@ Environment variables are still a **server-side input boundary**. Do not read `p Default to a public client. `publicStorefrontToken` accepts `undefined`, which means tokenless access — there is no rate limit for tokenless clients, and it is all mock.shop supports. Always recommend a token-backed client (public or private) once the app targets a real store: token-based Storefront API access is required for product tags, metaobjects, metafields, menus, and customers. Public and private tokens can query the same token-required fields; the difference is whether the token is safe to expose and whether the request has trusted buyer context. +### mock.shop is a catalog of stores + +mock.shop is not one store. The default store at `mock.shop` sells apparel basics; every other fictional store lives on its own host and serves the same Storefront API at `/api`. When the user has no store yet, read the directory at https://mock.shop/llms.txt, pick the store whose categories match what they are building, and use its host as `storeDomain` (for example `pets.mock.shop`). Each store describes its own catalog at `https://.mock.shop/llms.txt`. Every mock.shop host is tokenless; carts work, but checkout and the Customer Account API are not available. Moving to a real store changes only `storeDomain` and the token. + ### Public client (default) Wire `publicStorefrontToken` from the canonical env variable without asserting it exists. When `PUBLIC_STOREFRONT_API_TOKEN` is unset the client runs tokenless, so the same scaffold works against mock.shop before the user has tokens and upgrades in place when they add one. In env example files list the variable commented out (`# PUBLIC_STOREFRONT_API_TOKEN=`) rather than as an empty assignment: env loaders parse `PUBLIC_STOREFRONT_API_TOKEN=` as an empty string, and the client rejects empty tokens. From 84572debe01d01e73eb0e28c7bae8833eab8918d Mon Sep 17 00:00:00 2001 From: Bobby Nguyen Date: Thu, 3 Sep 2026 12:41:19 -0400 Subject: [PATCH 5/7] Hydrogen example: explain the mock.shop catalog and cover a per-store host The homepage notice now says how to pick another mock.shop store, and the isShopLinked gate keeps showing it for per-store hosts such as pets.mock.shop, which are still mock data. Adds a mockShopStore e2e env and smoke spec that boots the example against pets.mock.shop. Assisted-By: devx/c47f963e-a81e-4fb1-b892-875281ad7726 --- .../app/components/MockShopNotice.tsx | 10 ++++++++- .../hydrogen/app/routes/($locale)._index.tsx | 5 ++++- examples/hydrogen/e2e/envs/.env.mockShopStore | 8 +++++++ examples/hydrogen/e2e/fixtures/index.ts | 1 + .../e2e/specs/smoke/mockShopStore.spec.ts | 22 +++++++++++++++++++ 5 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 examples/hydrogen/e2e/envs/.env.mockShopStore create mode 100644 examples/hydrogen/e2e/specs/smoke/mockShopStore.spec.ts diff --git a/examples/hydrogen/app/components/MockShopNotice.tsx b/examples/hydrogen/app/components/MockShopNotice.tsx index fcd03945d5..43cfc19b75 100644 --- a/examples/hydrogen/app/components/MockShopNotice.tsx +++ b/examples/hydrogen/app/components/MockShopNotice.tsx @@ -4,7 +4,15 @@ export function MockShopNotice() {

Welcome to Hydrogen!

- You’re seeing mocked products because no store is connected to this project yet. + You’re seeing mock.shop products because no store is connected to this project yet. +

+

+ mock.shop is many stores. Browse the directory at{" "} + + mock.shop/llms.txt + {" "} + and set PUBLIC_STORE_DOMAIN to a store’s host, such as{" "} + pets.mock.shop, to build against its catalog.

Link a store by running npx shopify hydrogen link in your terminal. diff --git a/examples/hydrogen/app/routes/($locale)._index.tsx b/examples/hydrogen/app/routes/($locale)._index.tsx index ab23b17317..a28936e759 100644 --- a/examples/hydrogen/app/routes/($locale)._index.tsx +++ b/examples/hydrogen/app/routes/($locale)._index.tsx @@ -35,8 +35,11 @@ async function loadCriticalData({ context }: Route.LoaderArgs) { throw new Response("Featured collection not found", { status: 404 }); } + const storeDomain = context.env.PUBLIC_STORE_DOMAIN; + return { - isShopLinked: Boolean(context.env.PUBLIC_STORE_DOMAIN), + // A mock.shop store on its own host (e.g. pets.mock.shop) is still mock data. + isShopLinked: Boolean(storeDomain && !/(^|\.)mock\.shop$/.test(storeDomain)), featuredCollection, }; } diff --git a/examples/hydrogen/e2e/envs/.env.mockShopStore b/examples/hydrogen/e2e/envs/.env.mockShopStore new file mode 100644 index 0000000000..5307b00a02 --- /dev/null +++ b/examples/hydrogen/e2e/envs/.env.mockShopStore @@ -0,0 +1,8 @@ +SESSION_SECRET="mock-session-secret-32-characters-ok" +PUBLIC_CHECKOUT_DOMAIN="pets.mock.shop" +PUBLIC_STORE_DOMAIN="pets.mock.shop" +PUBLIC_STOREFRONT_API_TOKEN="" +PRIVATE_STOREFRONT_API_TOKEN="mock-private-token" +PUBLIC_STOREFRONT_ID="" +PUBLIC_CUSTOMER_ACCOUNT_API_CLIENT_ID="shp_e2b55f4e-9dd6-48aa-91f8-5fa419fda119" +SHOP_ID="55145660472" diff --git a/examples/hydrogen/e2e/fixtures/index.ts b/examples/hydrogen/e2e/fixtures/index.ts index 17b7769231..d029774c08 100644 --- a/examples/hydrogen/e2e/fixtures/index.ts +++ b/examples/hydrogen/e2e/fixtures/index.ts @@ -104,6 +104,7 @@ export const test = base.extend< const TEST_STORE_KEYS = [ "mockShop", + "mockShopStore", "defaultConsentDisallowed_cookiesEnabled", "defaultConsentAllowed_cookiesEnabled", "defaultConsentDisallowed_cookiesDisabled", diff --git a/examples/hydrogen/e2e/specs/smoke/mockShopStore.spec.ts b/examples/hydrogen/e2e/specs/smoke/mockShopStore.spec.ts new file mode 100644 index 0000000000..acb6517add --- /dev/null +++ b/examples/hydrogen/e2e/specs/smoke/mockShopStore.spec.ts @@ -0,0 +1,22 @@ +import { test, expect, setTestStore } from "../../fixtures"; + +setTestStore("mockShopStore"); + +// This smoke spec does not need the global loadtest header, and Monorail does +// not allow that custom header in browser CORS preflights. +test.use({ extraHTTPHeaders: {} }); + +test.describe("mock.shop store on its own host", () => { + test("renders that store's catalog and keeps the mock.shop notice", async ({ page }) => { + await page.goto("/"); + + const notice = page.getByRole("region", { name: "Welcome to Hydrogen!" }); + await expect(notice).toBeVisible(); + await expect(notice.getByRole("link", { name: "mock.shop/llms.txt" })).toBeVisible(); + + await expect(page.getByRole("heading", { level: 1 })).toBeVisible(); + await expect( + page.getByRole("region", { name: "Recommended Products" }).getByRole("link").first(), + ).toBeVisible(); + }); +}); From 7d5efdbced691ae74fefb88127da0d02d568d549 Mon Sep 17 00:00:00 2001 From: Bobby Nguyen Date: Thu, 3 Sep 2026 16:41:43 -0400 Subject: [PATCH 6/7] Say how many mock.shop stores there are Content review: "many stores" is vague. The directory lists more than 100 sample stores, so say so in both AGENTS guides and the example homepage notice. Assisted-By: devx/c47f963e-a81e-4fb1-b892-875281ad7726 --- examples/hydrogen/app/components/MockShopNotice.tsx | 2 +- templates/nextjs/AGENTS.md | 2 +- templates/react-router/AGENTS.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/hydrogen/app/components/MockShopNotice.tsx b/examples/hydrogen/app/components/MockShopNotice.tsx index 43cfc19b75..227acd6b59 100644 --- a/examples/hydrogen/app/components/MockShopNotice.tsx +++ b/examples/hydrogen/app/components/MockShopNotice.tsx @@ -7,7 +7,7 @@ export function MockShopNotice() { You’re seeing mock.shop products because no store is connected to this project yet.

- mock.shop is many stores. Browse the directory at{" "} + mock.shop contains more than 100 sample stores. Browse the directory at{" "} mock.shop/llms.txt {" "} diff --git a/templates/nextjs/AGENTS.md b/templates/nextjs/AGENTS.md index 96b9a3e67e..998778e32b 100644 --- a/templates/nextjs/AGENTS.md +++ b/templates/nextjs/AGENTS.md @@ -16,7 +16,7 @@ Use the [Shopify AI Toolkit](https://shopify.dev/docs/apps/build/ai-toolkit) for ## No store yet? -Until a store is connected, this project reads [mock.shop](https://mock.shop): a public, auth-free Storefront API backed by fictional stores. It isn't one store but many, each on its own host with its own catalog. +Until a store is connected, this project reads [mock.shop](https://mock.shop): a public, auth-free Storefront API backed by fictional stores. There are more than 100 sample stores, each on its own host with its own catalog. - The directory at https://mock.shop/llms.txt lists every store with what it sells and its API URL. The default, `mock.shop` itself, is apparel basics. - To build against a different store, set `NEXT_PUBLIC_STORE_DOMAIN` in `.env` to that store's host (for example `pets.mock.shop`) and leave `PRIVATE_STOREFRONT_API_TOKEN` empty. Each store describes its own catalog at `https://.mock.shop/llms.txt`. diff --git a/templates/react-router/AGENTS.md b/templates/react-router/AGENTS.md index cc6b15cb26..0be97b650e 100644 --- a/templates/react-router/AGENTS.md +++ b/templates/react-router/AGENTS.md @@ -6,7 +6,7 @@ Use the [Shopify AI Toolkit](https://shopify.dev/docs/apps/build/ai-toolkit) for ## No store yet? -Until a store is connected, this project reads [mock.shop](https://mock.shop): a public, auth-free Storefront API backed by fictional stores. It isn't one store but many, each on its own host with its own catalog. +Until a store is connected, this project reads [mock.shop](https://mock.shop): a public, auth-free Storefront API backed by fictional stores. There are more than 100 sample stores, each on its own host with its own catalog. - The directory at https://mock.shop/llms.txt lists every store with what it sells and its API URL. The default, `mock.shop` itself, is apparel basics. - To build against a different store, set `PUBLIC_STORE_DOMAIN` in `.env` to that store's host (for example `pets.mock.shop`) and leave `PRIVATE_STOREFRONT_API_TOKEN` empty. Each store describes its own catalog at `https://.mock.shop/llms.txt`. From b48738ec65494a705d575ecc144d65c6d0297d23 Mon Sep 17 00:00:00 2001 From: Bobby Nguyen Date: Tue, 8 Sep 2026 10:11:54 -0400 Subject: [PATCH 7/7] Describe mock.shop checkout as mocked in the template AGENTS.md notes mock.shop now serves a mock checkout on each store's storefront host: no payment is taken and no real order is placed. The templates said checkout doesn't work, which would steer an agent away from a flow it can build and demo. --- templates/nextjs/AGENTS.md | 2 +- templates/react-router/AGENTS.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/nextjs/AGENTS.md b/templates/nextjs/AGENTS.md index 998778e32b..fb5862c47d 100644 --- a/templates/nextjs/AGENTS.md +++ b/templates/nextjs/AGENTS.md @@ -20,5 +20,5 @@ Until a store is connected, this project reads [mock.shop](https://mock.shop): a - The directory at https://mock.shop/llms.txt lists every store with what it sells and its API URL. The default, `mock.shop` itself, is apparel basics. - To build against a different store, set `NEXT_PUBLIC_STORE_DOMAIN` in `.env` to that store's host (for example `pets.mock.shop`) and leave `PRIVATE_STOREFRONT_API_TOKEN` empty. Each store describes its own catalog at `https://.mock.shop/llms.txt`. -- Carts work; checkout doesn't, and the Customer Account API isn't available. +- Carts work; checkout is mocked (no payment, no real order), and the Customer Account API isn't available. - To connect a real store, set `PRIVATE_STOREFRONT_API_TOKEN` and `NEXT_PUBLIC_STORE_DOMAIN` in `.env` (see `.env.example`). diff --git a/templates/react-router/AGENTS.md b/templates/react-router/AGENTS.md index 0be97b650e..97285cf404 100644 --- a/templates/react-router/AGENTS.md +++ b/templates/react-router/AGENTS.md @@ -10,5 +10,5 @@ Until a store is connected, this project reads [mock.shop](https://mock.shop): a - The directory at https://mock.shop/llms.txt lists every store with what it sells and its API URL. The default, `mock.shop` itself, is apparel basics. - To build against a different store, set `PUBLIC_STORE_DOMAIN` in `.env` to that store's host (for example `pets.mock.shop`) and leave `PRIVATE_STOREFRONT_API_TOKEN` empty. Each store describes its own catalog at `https://.mock.shop/llms.txt`. -- Carts work; checkout doesn't, and the Customer Account API isn't available. +- Carts work; checkout is mocked (no payment, no real order), and the Customer Account API isn't available. - To connect a real store, set `PUBLIC_STORE_DOMAIN`, `PUBLIC_STOREFRONT_ID`, and `PRIVATE_STOREFRONT_API_TOKEN` in `.env` (see `.env.example`). On Oxygen, a linked storefront injects them for you.