← Shopify App BuilderCONTENT HISTORY

Update to Shopify App Builder

Snapshot Sep 30, 2026 · 23:13 UTC · version 1.4.1

Collection source: not recorded for this historical snapshot.

WHAT CHANGED · RULE-BASED ANALYSIS

First saved snapshot

No earlier snapshot is available to establish a change.

Compare saved observations

Download comparison JSON
Full technical diff · 0 changed fields
Full snapshot data
{
  "description": "Use this skill for Hydrogen 2026 Storefront Framework. Triggers include: 'hydrogen storefront', 'hydrogen 2026', 'hydrogen remix', 'shopify hydrogen framework', 'hydrogen setup scaffold', 'hydrogen useCart hook', 'hydrogen createCartHandler', 'hydrogen Customer Account API', 'hydrogen caching strategies', 'hydrogen oxygen deployment', 'hydrogen cli commands', 'hydrogen storefront client', 'hydrogen product page', 'hydrogen collection page', 'hydrogen checkout', 'hydrogen admin api', 'hydrogen queueApi', 'hydrogen analytics', 'hydrogen search implementation', 'hydrogen variants and options', 'hydrogen localization i18n', 'hydrogen performance optimization', 'hydrogen seo structured data', 'hydrogen third party scripts', 'hydrogen css styling tailwind', 'hydrogen testing'.",
  "included_files": [],
  "name": "hydrogen-storefront",
  "skill_md_contents": "---\nname: hydrogen-storefront\ndescription: \"Use this skill for Hydrogen 2026 Storefront Framework. Triggers include: 'hydrogen storefront', 'hydrogen 2026', 'hydrogen remix', 'shopify hydrogen framework', 'hydrogen setup scaffold', 'hydrogen useCart hook', 'hydrogen createCartHandler', 'hydrogen Customer Account API', 'hydrogen caching strategies', 'hydrogen oxygen deployment', 'hydrogen cli commands', 'hydrogen storefront client', 'hydrogen product page', 'hydrogen collection page', 'hydrogen checkout', 'hydrogen admin api', 'hydrogen queueApi', 'hydrogen analytics', 'hydrogen search implementation', 'hydrogen variants and options', 'hydrogen localization i18n', 'hydrogen performance optimization', 'hydrogen seo structured data', 'hydrogen third party scripts', 'hydrogen css styling tailwind', 'hydrogen testing'.\"\n---\n\n# Hydrogen 2026 Storefront Framework\n\nHydrogen is Shopify's Remix-based framework for building fast, custom storefronts. This skill covers Hydrogen 2026 project setup, the Storefront API client, cart management with hooks, Customer Account API, caching strategies, Oxygen deployment, CLI commands, and real-world PDP and collection page patterns.\n\n## When Asked\n\n**When asked to scaffold a new Hydrogen storefront:**\nProvide the npm create @shopify/hydrogen command, explain project structure, and show how to configure .env with Storefront API credentials.\n\n**When asked to fetch and display products:**\nUse the Storefront API via the storefront client (GraphQL query), show how to load product details, variants, and media; explain caching strategies.\n\n**When asked to implement a shopping cart:**\nUse createCartHandler for backend cart mutations, useCart hook for frontend state, and explain line item management, discounts, and checkout flow.\n\n**When asked about Customer Account API:**\nExplain how to enable, authenticate with OAuth, and use for customer login, order history, account details, and profile updates.\n\n**When asked to optimize performance and caching:**\nDiscuss Cache-Control headers, Oxygen runtime caching, request coalescing, and streaming SSR; provide cache strategy examples.\n\n**When asked to deploy to Oxygen:**\nShow CLI deployment steps (hydrogen deploy), environment variable setup, monitoring, and rollback procedures.\n\n**When asked to implement search or filtering:**\nUse Hydrogen Search API, faceting, filtering by attribute, and show autocomplete and collection filter patterns.\n\n---\n\n## Hydrogen 2026 Architecture Overview\n\nHydrogen provides:\n- **Remix Framework**: Server-side rendering (SSR), file-based routing, loader/action functions, Remix utilities\n- **Storefront API Client**: GraphQL client for fetching product, collection, cart, order, and customer data\n- **useCart Hook**: Client-side cart state management with add-to-cart, update, remove operations\n- **createCartHandler**: Server-side cart mutations (create, update, discount, checkout)\n- **Customer Account API**: OAuth-based customer login, order history, account endpoints\n- **Caching**: Cache-Control headers, Oxygen response caching, request deduplication\n- **Oxygen Platform**: Serverless execution environment with global edge cache\n- **CLI**: `hydrogen dev`, `hydrogen preview`, `hydrogen deploy` commands\n- **Analytics & Reporting**: Built-in Oxygen analytics, custom event tracking\n\n### Installation & Project Setup\n\n```bash\nnpm create @shopify/hydrogen@latest my-store -- --language TypeScript\ncd my-store\nnpm install\nnpm run dev\n```\n\nThis generates:\n```\nmy-store/\n├── app/\n│   ├── components/        # Reusable React components\n│   ├── routes/            # File-based routing (Remix)\n│   ├── lib/               # Utility functions, API clients\n│   └── root.tsx           # Root layout\n├── public/\n├── .env                   # Storefront API token, store domain\n├── hydrogen.config.ts     # Hydrogen config\n├── remix.config.js        # Remix config (SSR, build)\n├── package.json\n└── tsconfig.json\n```\n\n### Environment Setup\n\n```bash\n# .env\nPRIVATE_STOREFRONT_API_TOKEN=your_token_here\nPUBLIC_STORE_DOMAIN=your-store.myshopify.com\nSESSION_SECRET=random_string_min_32_chars\nPRIVATE_CUSTOMER_ACCOUNT_API_TOKEN=customer_token\nPUBLIC_CUSTOMER_ACCOUNT_API_URL=https://shopifyid.com/oauth/authorize\n```\n\n---\n\n## Storefront API Client\n\n### Initialize & Query\n\n```typescript\n// app/lib/shopify.server.ts\nimport { createStorefrontClient } from '@shopify/hydrogen';\n\nexport const storefront = createStorefrontClient({\n  apiUrl: `https://${process.env.PUBLIC_STORE_DOMAIN}/api/2024-01/graphql.json`,\n  apiVersion: '2024-01',\n  privateStorefrontToken: process.env.PRIVATE_STOREFRONT_API_TOKEN!,\n});\n```\n\n### Fetch Product Details\n\n```typescript\n// app/routes/products/$handle.tsx\nimport { json, type LoaderFunctionArgs } from '@shopify/remix-oxygen';\nimport { useLoaderData } from '@remix-run/react';\nimport { storefront } from '~/lib/shopify.server';\n\nconst PRODUCT_QUERY = `\n  query getProduct($handle: String!) {\n    product(handle: $handle) {\n      id\n      title\n      description\n      handle\n      vendor\n      priceRange {\n        minVariantPrice {\n          amount\n          currencyCode\n        }\n        maxVariantPrice {\n          amount\n          currencyCode\n        }\n      }\n      variants(first: 250) {\n        edges {\n          node {\n            id\n            title\n            availableForSale\n            selectedOptions {\n              name\n              value\n            }\n            priceV2 {\n              amount\n              currencyCode\n            }\n            image {\n              url\n              altText\n            }\n          }\n        }\n      }\n      images(first: 10) {\n        edges {\n          node {\n            url\n            altText\n          }\n        }\n      }\n    }\n  }\n`;\n\nexport async function loader({ params, context }: LoaderFunctionArgs) {\n  const { product } = await storefront.query(PRODUCT_QUERY, {\n    variables: { handle: params.handle },\n    cache: context.storefront.CacheShort(),\n  });\n\n  if (!product) {\n    throw new Response('Product not found', { status: 404 });\n  }\n\n  return json({ product });\n}\n\nexport default function ProductPage() {\n  const { product } = useLoaderData<typeof loader>();\n\n  return (\n    <div>\n      <h1>{product.title}</h1>\n      <p>{product.description}</p>\n      <div className=\"price-range\">\n        ${product.priceRange.minVariantPrice.amount} -\n        ${product.priceRange.maxVariantPrice.amount}\n      </div>\n      {product.images.edges.map(({ node: image }) => (\n        <img key={image.url} src={image.url} alt={image.altText} />\n      ))}\n    </div>\n  );\n}\n```\n\n### Fetch Collections\n\n```typescript\nconst COLLECTIONS_QUERY = `\n  query getCollections($first: Int!) {\n    collections(first: $first) {\n      edges {\n        node {\n          id\n          title\n          handle\n          image {\n            url\n            altText\n          }\n        }\n      }\n    }\n  }\n`;\n\nexport async function loader({ context }: LoaderFunctionArgs) {\n  const { collections } = await storefront.query(COLLECTIONS_QUERY, {\n    variables: { first: 20 },\n    cache: context.storefront.CacheLong(),\n  });\n\n  return json({ collections });\n}\n```\n\n---\n\n## Cart Management\n\n### useCart Hook (Client-Side)\n\n```typescript\n// app/hooks/useCart.ts\nimport { useContext } from 'react';\nimport { CartContext } from '~/context/CartContext';\n\nexport function useCart() {\n  const context = useContext(CartContext);\n  if (!context) {\n    throw new Error('useCart must be used within CartProvider');\n  }\n  return context;\n}\n\n// Usage in component\nimport { useCart } from '~/hooks/useCart';\n\nexport function AddToCartButton({ variantId, quantity = 1 }) {\n  const { addToCart, isLoading } = useCart();\n\n  const handleClick = async () => {\n    await addToCart({\n      variantId,\n      quantity,\n    });\n    // Show success toast\n  };\n\n  return (\n    <button onClick={handleClick} disabled={isLoading}>\n      {isLoading ? 'Adding...' : 'Add to Cart'}\n    </button>\n  );\n}\n```\n\n### createCartHandler (Server-Side)\n\n```typescript\n// app/lib/cart.server.ts\nimport { createCartHandler } from '@shopify/hydrogen';\nimport { storefront } from './shopify.server';\n\nconst CREATE_CART_MUTATION = `\n  mutation createCart($input: CartInput!) {\n    cartCreate(input: $input) {\n      cart {\n        id\n        checkoutUrl\n        lines(first: 10) {\n          edges {\n            node {\n              id\n              quantity\n              merchandise {\n                ... on ProductVariant {\n                  id\n                  title\n                  priceV2 {\n                    amount\n                    currencyCode\n                  }\n                }\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n`;\n\nexport const cartHandler = createCartHandler({\n  storefront,\n  getCartId: async (request) => {\n    // Retrieve cart ID from session or cookie\n    const cartId = request.headers.get('x-cart-id');\n    return cartId;\n  },\n  setCartId: async (request, cartId) => {\n    // Store cart ID in session/cookie\n    // This is called after cart is created\n  },\n  cartQueryFragment: `\n    fragment CartApiFragment on Cart {\n      id\n      checkoutUrl\n      totalQuantity\n      cost {\n        totalAmount {\n          amount\n          currencyCode\n        }\n        subtotalAmount {\n          amount\n        }\n        totalTaxAmount {\n          amount\n        }\n        totalDutyAmount {\n          amount\n        }\n      }\n      lines(first: $lineLimit) {\n        edges {\n          node {\n            id\n            quantity\n            cost {\n              totalAmount {\n                amount\n              }\n            }\n            merchandise {\n              ... on ProductVariant {\n                id\n                title\n                sku\n                priceV2 {\n                  amount\n                }\n                image {\n                  url\n                  altText\n                }\n              }\n            }\n          }\n        }\n      }\n    }\n  `,\n});\n\n// Usage in action handler\nexport async function action({ request, context }: ActionFunctionArgs) {\n  const cart = await cartHandler.queryCart(request, {\n    variables: { lineLimit: 100 },\n  });\n\n  if (request.method === 'POST') {\n    const formData = await request.formData();\n    const variantId = formData.get('variantId');\n    const quantity = parseInt(formData.get('quantity') || '1', 10);\n\n    const updatedCart = await cartHandler.addToCart(request, {\n      lines: [{ merchandiseId: variantId, quantity }],\n    });\n\n    return json({ cart: updatedCart });\n  }\n\n  return json({ cart });\n}\n```\n\n### Cart Context Provider\n\n```typescript\n// app/context/CartContext.tsx\nimport { createContext, ReactNode, useState } from 'react';\n\ninterface CartContextType {\n  cart: any | null;\n  addToCart: (args: any) => Promise<void>;\n  removeFromCart: (lineId: string) => Promise<void>;\n  updateQuantity: (lineId: string, quantity: number) => Promise<void>;\n  isLoading: boolean;\n}\n\nexport const CartContext = createContext<CartContextType | null>(null);\n\nexport function CartProvider({ children }: { children: ReactNode }) {\n  const [cart, setCart] = useState(null);\n  const [isLoading, setIsLoading] = useState(false);\n\n  const addToCart = async ({ variantId, quantity }: any) => {\n    setIsLoading(true);\n    const response = await fetch('/cart', {\n      method: 'POST',\n      body: JSON.stringify({ variantId, quantity }),\n    });\n    const { cart: newCart } = await response.json();\n    setCart(newCart);\n    setIsLoading(false);\n  };\n\n  const removeFromCart = async (lineId: string) => {\n    setIsLoading(true);\n    const response = await fetch('/cart', {\n      method: 'DELETE',\n      body: JSON.stringify({ lineId }),\n    });\n    const { cart: newCart } = await response.json();\n    setCart(newCart);\n    setIsLoading(false);\n  };\n\n  return (\n    <CartContext.Provider\n      value={{ cart, addToCart, removeFromCart, isLoading }}\n    >\n      {children}\n    </CartContext.Provider>\n  );\n}\n```\n\n---\n\n## Customer Account API\n\n### Enable & Setup\n\n```typescript\n// .env\nPUBLIC_CUSTOMER_ACCOUNT_API_URL=https://shopifyid.com/oauth/authorize\nPRIVATE_CUSTOMER_ACCOUNT_API_TOKEN=your_token\n```\n\n### Customer Login & Auth\n\n```typescript\n// app/routes/account/login.tsx\nimport { redirect } from '@shopify/remix-oxygen';\n\nexport async function loader({ context }: LoaderFunctionArgs) {\n  const customerAccessToken = await getCustomerAccessToken(context);\n  if (customerAccessToken) {\n    return redirect('/account/profile');\n  }\n  return null;\n}\n\nexport async function action({ request, context }: ActionFunctionArgs) {\n  if (request.method === 'POST') {\n    const formData = await request.formData();\n    const email = formData.get('email');\n    const password = formData.get('password');\n\n    const { customerAccessToken, customerUserErrors } =\n      await context.storefront.mutate(CUSTOMER_LOGIN_MUTATION, {\n        variables: { email, password },\n      });\n\n    if (customerAccessToken?.accessToken) {\n      // Store token in session\n      const session = await getSession(request.headers.get('cookie'));\n      session.set('customerAccessToken', customerAccessToken.accessToken);\n      return redirect('/account/profile', {\n        headers: { 'Set-Cookie': await commitSession(session) },\n      });\n    }\n\n    return json({ errors: customerUserErrors });\n  }\n}\n\nexport default function LoginPage() {\n  return (\n    <form method=\"post\">\n      <input type=\"email\" name=\"email\" placeholder=\"Email\" required />\n      <input\n        type=\"password\"\n        name=\"password\"\n        placeholder=\"Password\"\n        required\n      />\n      <button type=\"submit\">Sign In</button>\n    </form>\n  );\n}\n\nconst CUSTOMER_LOGIN_MUTATION = `\n  mutation customerAccessTokenCreate(\n    $input: CustomerAccessTokenCreateInput!\n  ) {\n    customerAccessTokenCreate(input: $input) {\n      customerAccessToken {\n        accessToken\n        expiresAt\n      }\n      customerUserErrors {\n        code\n        field\n        message\n      }\n    }\n  }\n`;\n```\n\n### Fetch Customer Profile\n\n```typescript\n// app/routes/account/profile.tsx\nconst CUSTOMER_QUERY = `\n  query getCustomer($customerAccessToken: String!) {\n    customer(customerAccessToken: $customerAccessToken) {\n      id\n      email\n      firstName\n      lastName\n      phone\n      defaultAddress {\n        id\n        formatted\n        address1\n        address2\n        city\n        province\n        country\n        zip\n      }\n      orders(first: 10) {\n        edges {\n          node {\n            id\n            orderNumber\n            processedAt\n            totalPriceSet {\n              shopMoney {\n                amount\n                currencyCode\n              }\n            }\n            lineItems(first: 5) {\n              edges {\n                node {\n                  title\n                  quantity\n                  originalTotalSet {\n                    shopMoney {\n                      amount\n                    }\n                  }\n                }\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n`;\n\nexport async function loader({ request, context }: LoaderFunctionArgs) {\n  const session = await getSession(request.headers.get('cookie'));\n  const customerAccessToken = session.get('customerAccessToken');\n\n  if (!customerAccessToken) {\n    return redirect('/account/login');\n  }\n\n  const { customer } = await context.storefront.query(CUSTOMER_QUERY, {\n    variables: { customerAccessToken },\n    cache: context.storefront.CacheShort(),\n  });\n\n  return json({ customer });\n}\n\nexport default function ProfilePage() {\n  const { customer } = useLoaderData<typeof loader>();\n\n  return (\n    <div>\n      <h1>Welcome, {customer.firstName}</h1>\n      <p>Email: {customer.email}</p>\n      <p>Phone: {customer.phone}</p>\n      <h2>Recent Orders</h2>\n      <ul>\n        {customer.orders.edges.map(({ node: order }) => (\n          <li key={order.id}>\n            Order #{order.orderNumber} - $\n            {order.totalPriceSet.shopMoney.amount}\n          </li>\n        ))}\n      </ul>\n    </div>\n  );\n}\n```\n\n---\n\n## Caching Strategies\n\n### Cache-Control Headers\n\n```typescript\n// app/lib/cache.server.ts\nexport const CACHE_SHORT = () => ({\n  'Cache-Control': 'public, max-age=3600, s-maxage=3600', // 1 hour\n});\n\nexport const CACHE_LONG = () => ({\n  'Cache-Control': 'public, max-age=86400, s-maxage=86400', // 24 hours\n});\n\nexport const CACHE_NONE = () => ({\n  'Cache-Control': 'no-cache, no-store, must-revalidate',\n});\n\n// Usage in loader\nexport async function loader({ context }: LoaderFunctionArgs) {\n  const { product } = await storefront.query(PRODUCT_QUERY, {\n    variables: { handle },\n    cache: context.storefront.CacheShort(),\n  });\n\n  return json(\n    { product },\n    {\n      headers: CACHE_SHORT(),\n    }\n  );\n}\n```\n\n### Request Coalescing\n\nHydrogen automatically deduplicates identical requests made within the same render, preventing unnecessary API calls:\n\n```typescript\n// Both calls return same result without extra API calls\nconst [product1, product2] = await Promise.all([\n  storefront.query(PRODUCT_QUERY, { variables: { handle: 'widget-a' } }),\n  storefront.query(PRODUCT_QUERY, { variables: { handle: 'widget-a' } }),\n]);\n```\n\n### Stale-While-Revalidate\n\n```typescript\nexport const CACHE_SWR = () => ({\n  'Cache-Control': 'public, max-age=3600, s-maxage=3600, stale-while-revalidate=86400',\n});\n```\n\n---\n\n## Oxygen Deployment\n\n### Build & Deploy\n\n```bash\n# Build locally\nnpm run build\n\n# Deploy to Oxygen\nhydrogen deploy\n\n# Deploy with custom environment name\nhydrogen deploy --env=staging\n\n# View deployment logs\nhydrogen deploy --logs\n```\n\n### Configuration\n\n```typescript\n// hydrogen.config.ts\nimport { defineConfig } from '@shopify/hydrogen/config';\n\nexport default defineConfig({\n  storefront: {\n    id: 'your-storefront-id',\n    title: 'My Store',\n    apiUrl: 'https://your-store.myshopify.com/api/2024-01/graphql.json',\n  },\n  oxygen: {\n    preloadRequestCookie: [],\n  },\n});\n```\n\n### Environment Variables in Oxygen\n\nSet via dashboard or CLI:\n```bash\nhydrogen deploy --set PRIVATE_STOREFRONT_API_TOKEN=token_value\n```\n\n---\n\n## Hydrogen CLI Commands\n\n```bash\n# Local development\nhydrogen dev                    # Start dev server (localhost:3000)\nhydrogen dev --port 8080       # Custom port\n\n# Preview production build\nhydrogen preview                # Simulate production locally\n\n# Build\nnpm run build                   # Build for production\n\n# Deploy\nhydrogen deploy                 # Deploy to Oxygen\nhydrogen deploy --env=staging   # Deploy to named environment\nhydrogen deploy --logs          # Show deployment logs\n\n# Analytics\nhydrogen analyze                # Analyze bundle size and performance\n\n# Pull remote config\nhydrogen config pull            # Fetch Storefront API config from dashboard\n```\n\n---\n\n## Worked Example: Product Details Page (PDP)\n\n```typescript\n// app/routes/products/$handle.tsx\nimport { json, type LoaderFunctionArgs } from '@shopify/remix-oxygen';\nimport { useLoaderData } from '@remix-run/react';\nimport { useState } from 'react';\nimport { storefront } from '~/lib/shopify.server';\nimport { AddToCartButton } from '~/components/AddToCartButton';\n\nconst PRODUCT_QUERY = `\n  query getProduct($handle: String!) {\n    product(handle: $handle) {\n      id\n      title\n      description\n      handle\n      vendor\n      priceRange {\n        minVariantPrice { amount currencyCode }\n        maxVariantPrice { amount currencyCode }\n      }\n      options {\n        name\n        values\n      }\n      variants(first: 250) {\n        edges {\n          node {\n            id\n            title\n            availableForSale\n            selectedOptions {\n              name\n              value\n            }\n            priceV2 {\n              amount\n              currencyCode\n            }\n            image {\n              url\n              altText\n              width\n              height\n            }\n          }\n        }\n      }\n      images(first: 20) {\n        edges {\n          node {\n            url\n            altText\n            width\n            height\n          }\n        }\n      }\n      seo {\n        title\n        description\n      }\n    }\n  }\n`;\n\nexport async function loader({ params, context }: LoaderFunctionArgs) {\n  const { product } = await storefront.query(PRODUCT_QUERY, {\n    variables: { handle: params.handle },\n    cache: context.storefront.CacheShort(),\n  });\n\n  if (!product) {\n    throw new Response('Not Found', { status: 404 });\n  }\n\n  return json(\n    { product },\n    {\n      headers: {\n        'Cache-Control': 'public, max-age=3600, s-maxage=3600',\n      },\n    }\n  );\n}\n\nexport const meta: MetaFunction<typeof loader> = ({ data }) => {\n  return [\n    { title: data?.product?.seo?.title || data?.product?.title },\n    { name: 'description', content: data?.product?.seo?.description },\n  ];\n};\n\nexport default function ProductPage() {\n  const { product } = useLoaderData<typeof loader>();\n  const [selectedVariant, setSelectedVariant] = useState(\n    product.variants.edges[0].node\n  );\n  const [selectedOptions, setSelectedOptions] = useState<\n    Record<string, string>\n  >({});\n\n  const handleOptionChange = (optionName: string, value: string) => {\n    const updated = { ...selectedOptions, [optionName]: value };\n    setSelectedOptions(updated);\n\n    // Find matching variant\n    const matchingVariant = product.variants.edges.find(({ node }) =>\n      node.selectedOptions.every(\n        (opt) => updated[opt.name] === opt.value\n      )\n    )?.node;\n\n    if (matchingVariant) {\n      setSelectedVariant(matchingVariant);\n    }\n  };\n\n  return (\n    <main className=\"product-page\">\n      <section className=\"gallery\">\n        <img\n          src={selectedVariant.image?.url}\n          alt={selectedVariant.image?.altText}\n          width={selectedVariant.image?.width}\n          height={selectedVariant.image?.height}\n        />\n      </section>\n\n      <section className=\"details\">\n        <h1>{product.title}</h1>\n        <p className=\"vendor\">{product.vendor}</p>\n\n        <div className=\"price\">\n          <span className=\"amount\">${selectedVariant.priceV2.amount}</span>\n        </div>\n\n        <div className=\"description\">\n          {product.description}\n        </div>\n\n        {product.options.map((option) => (\n          <fieldset key={option.name}>\n            <legend>{option.name}</legend>\n            <div className=\"options\">\n              {option.values.map((value) => (\n                <label key={value}>\n                  <input\n                    type=\"radio\"\n                    name={option.name}\n                    value={value}\n                    checked={selectedOptions[option.name] === value}\n                    onChange={() => handleOptionChange(option.name, value)}\n                  />\n                  {value}\n                </label>\n              ))}\n            </div>\n          </fieldset>\n        ))}\n\n        <AddToCartButton\n          variantId={selectedVariant.id}\n          disabled={!selectedVariant.availableForSale}\n        />\n      </section>\n    </main>\n  );\n}\n```\n\n---\n\n## Performance Optimization Tips\n\n- Use `CacheShort()` for product data (1 hour), `CacheLong()` for collections (24 hours)\n- Enable streaming SSR for faster Time to First Byte (TTFB)\n- Use `defer()` for non-critical data (recommendations, related products)\n- Optimize images with responsive srcset and lazy loading\n- Use Code Splitting for route components\n- Monitor Core Web Vitals in Oxygen Analytics dashboard\n- Use `<Image>` component from Hydrogen for automatic optimization\n\n---\n\n## Testing\n\n```typescript\n// app/__tests__/routes/products/$handle.test.tsx\nimport { loader } from '~/routes/products/$handle';\n\ndescribe('Product Page Loader', () => {\n  it('fetches product data', async () => {\n    const mockContext = {\n      storefront: {\n        query: vi.fn().mockResolvedValue({\n          product: { id: '123', title: 'Test Product' },\n        }),\n      },\n    };\n\n    const result = await loader({\n      params: { handle: 'test-product' },\n      context: mockContext,\n    });\n\n    expect(result).toBeDefined();\n    expect(mockContext.storefront.query).toHaveBeenCalled();\n  });\n});\n```\n\n---\n\n## Common Patterns Checklist\n\n- [ ] Initialize Storefront API client with token and domain\n- [ ] Use `CacheShort()` / `CacheLong()` for loader queries\n- [ ] Fetch product variants and options for variant selection UI\n- [ ] Implement cart add/update/remove via createCartHandler\n- [ ] Wrap cart functionality with useCart hook\n- [ ] Enable Customer Account API for customer login\n- [ ] Set Cache-Control headers on response.json()\n- [ ] Use file-based routing (Remix conventions)\n- [ ] Test queries locally before deploying\n- [ ] Monitor Oxygen Analytics for performance\n- [ ] Use hydrogen preview to test production build locally\n- [ ] Set environment variables via hydrogen deploy --set\n"
}

SHA-256 of public snapshot: 67a61520ae8367d3122525f88c3e8fd99c1ee61f4b5b6e2066b3fcc7c17fb91c