# Next.js & Remix

This guide covers integrating Elevate A/B Testing into Next.js and Remix storefronts using the `@elevateab/sdk` package. The setup is nearly identical for both frameworks, with minor differences noted where applicable.

> **Using a standard Shopify theme?** This guide is for headless storefronts only. For standard Shopify theme integration, see [Setup & Installation](https://docs.elevateab.com/elevate-helpcenter/getting-started/setup-and-installation).

### Prerequisites

Before starting, make sure you've completed the steps in [Headless Getting Started](https://docs.elevateab.com/elevate-helpcenter/headless-integration/getting-started-with-headless):

* Installed the `@elevateab/sdk` package
* Located your Storefront Access Token
* Familiarized yourself with creating experiments on Elevate

### Provider Setup

#### Next.js (App Router)

Wrap your app with `ElevateNextProvider` in your root layout. This provider automatically tracks page views on route changes and initializes analytics globally.

```tsx
// app/layout.tsx
import { ElevateNextProvider } from "@elevateab/sdk/next";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <body>
        <ElevateNextProvider
          storeId="your-store.myshopify.com"
          storefrontAccessToken={process.env.NEXT_PUBLIC_STOREFRONT_TOKEN}
          preventFlickering={true}
        >
          {children}
        </ElevateNextProvider>
      </body>
    </html>
  );
}
```

Note that Next.js uses `ElevateNextProvider` (imported from `@elevateab/sdk/next`) rather than the base `ElevateProvider`. This variant includes automatic page view tracking on route changes.

#### Remix

Wrap your app with `ElevateProvider` in your root file:

```tsx
// app/root.tsx
import { ElevateProvider } from "@elevateab/sdk";
import { Outlet } from "@remix-run/react";

export default function App() {
  return (
    <html>
      <body>
        <ElevateProvider
          storeId="your-store.myshopify.com"
          storefrontAccessToken="your-public-token"
          preventFlickering={true}
        >
          <Outlet />
        </ElevateProvider>
      </body>
    </html>
  );
}
```

### Running an Experiment

Once the provider is set up, you can create experiments directly on the Elevate dashboard — split URL, custom code, or content editor experiments all work out of the box without any code changes in your storefront.

If you want to conditionally render content in your codebase, you can optionally use the `useExperiment` hook. Pass in the experiment ID from your Elevate dashboard URL (`elevateab.app/<test-id>`):

```tsx
"use client"; // Required in Next.js App Router

import { useExperiment } from "@elevateab/sdk";

function HeroBanner() {
  const { isControl, isA, isB, isLoading } = useExperiment("abc123");

  if (isLoading) return <HeroBannerSkeleton />;

  if (isControl) return <h1>Welcome to Our Store</h1>;
  if (isA) return <h1>Shop Our Spring Collection</h1>;
  if (isB) return <h1>Free Shipping on Orders Over $50</h1>;

  return <h1>Welcome to Our Store</h1>;
}
```

In Next.js, components using `useExperiment` must be client components (add `"use client"` at the top of the file). This isn't needed in Remix.

For more details on creating experiments and using the hook, see [Headless Getting Started](https://docs.elevateab.com/elevate-helpcenter/headless-integration/getting-started-with-headless).

### Event Tracking

Unlike Hydrogen, Next.js and Remix don't have Shopify's `Analytics.Provider`, so there's no built-in event system for the SDK to hook into. This means you need to send events to Elevate manually so we can track conversions and attribute them to the correct A/B test variants.

Page views are handled automatically by the provider, but product views, cart events, and checkout events require manual calls.

#### Product Views

For Next.js, the SDK provides a `ProductViewTracker` component that you drop into your product pages:

```tsx
// app/product/[handle]/page.tsx (Next.js)
import { ProductViewTracker } from "@elevateab/sdk/next";

export default function ProductPage({ product }) {
  return (
    <>
      <ProductViewTracker
        productId={product.id}
        productVendor={product.vendor}
        productPrice={parseFloat(product.priceRange.minVariantPrice.amount)}
        currency={product.priceRange.minVariantPrice.currencyCode}
      />
      {/* Rest of your product page */}
    </>
  );
}
```

For Remix, use the `trackProductView` function instead:

```tsx
// app/routes/products.$handle.tsx (Remix)
import { useEffect } from "react";
import { trackProductView } from "@elevateab/sdk";

export default function ProductPage() {
  const { product } = useLoaderData<typeof loader>();

  useEffect(() => {
    trackProductView({
      productId: product.id,
      productPrice: parseFloat(product.priceRange.minVariantPrice.amount),
    });
  }, [product.id]);

  return <>{/* Rest of your product page */}</>;
}
```

#### Add to Cart

Call `trackAddToCart` wherever your add-to-cart logic lives. Include `cartId` so the SDK can tag the cart with A/B test data for order attribution.

```tsx
import { trackAddToCart } from "@elevateab/sdk";

async function handleAddToCart() {
  // Your existing add-to-cart logic...

  await trackAddToCart({
    productId: product.id,
    variantId: variant.id,
    productPrice: 99.99,
    productQuantity: 1,
    currency: "USD",
    cartId: cart.id,
  });
}
```

Shopify GIDs (e.g., `gid://shopify/Product/123456`) are automatically converted to numeric IDs — no need to parse them yourself.

#### Remove from Cart

```tsx
import { trackRemoveFromCart } from "@elevateab/sdk";

await trackRemoveFromCart({
  productId: product.id,
  variantId: variant.id,
  productPrice: 99.99,
  productQuantity: 1,
});
```

#### Cart View

```tsx
import { trackCartView } from "@elevateab/sdk";

await trackCartView({
  cartTotalPrice: 199.99,
  cartTotalQuantity: 2,
  currency: "USD",
  cartItems: [
    { productId: "123", variantId: "456", productPrice: 99.99, productQuantity: 1 },
    { productId: "789", variantId: "012", productPrice: 100.00, productQuantity: 1 },
  ],
});
```

#### Search

```tsx
import { trackSearchSubmitted } from "@elevateab/sdk";

await trackSearchSubmitted({ searchQuery: "blue shirt" });
```

#### Checkout Started

```tsx
import { trackCheckoutStarted } from "@elevateab/sdk";

await trackCheckoutStarted();
```

#### Checkout Completed

```tsx
import { trackCheckoutCompleted } from "@elevateab/sdk";

await trackCheckoutCompleted();
```

#### Event Tracking Summary

| Event              | Next.js                    | Remix                      |
| ------------------ | -------------------------- | -------------------------- |
| Page view          | Automatic                  | Automatic                  |
| Product view       | `ProductViewTracker`       | `trackProductView()`       |
| Add to cart        | `trackAddToCart()`         | `trackAddToCart()`         |
| Remove from cart   | `trackRemoveFromCart()`    | `trackRemoveFromCart()`    |
| Cart view          | `trackCartView()`          | `trackCartView()`          |
| Search             | `trackSearchSubmitted()`   | `trackSearchSubmitted()`   |
| Checkout started   | `trackCheckoutStarted()`   | `trackCheckoutStarted()`   |
| Checkout completed | `trackCheckoutCompleted()` | `trackCheckoutCompleted()` |

### Content Security Policy (CSP)

This step is only needed if your site enforces a strict Content Security Policy and you see console errors about blocked scripts.

Add the following domains to your CSP headers:

```
Content-Security-Policy:
  script-src 'self' https://ds0wlyksfn0sb.cloudfront.net;
  script-src-elem 'self' https://ds0wlyksfn0sb.cloudfront.net;
  connect-src 'self' https://ds0wlyksfn0sb.cloudfront.net https://d339co84ntxcme.cloudfront.net https://configs.elevateab.com
```

How you configure this depends on your setup — it might be in `next.config.js`, a middleware file, or your hosting platform's headers configuration.

### Preview Mode

You can preview a specific variant without affecting live traffic or analytics by adding URL parameters:

```
https://your-store.com/products/example?eabUserPreview=true&abtid=<test-id>&eab_tests=<short-id>_<variant-id>
```

You'll find the test ID, short ID, and variant IDs in your Elevate dashboard under the experiment settings.

To check whether preview mode is active in your code:

```tsx
import { isPreviewMode } from "@elevateab/sdk";

if (isPreviewMode()) {
  // Optionally show a preview indicator banner
}
```

Preview visits are excluded from analytics and won't affect your experiment results.

### Troubleshooting

**Tests aren't showing / visitors all see the control** Make sure the experiment is set to "Running" in your Elevate dashboard, and that the `storeId` in your provider matches your Shopify domain exactly (e.g., `your-store.myshopify.com`, not your custom domain).

**Orders aren't attributed to variants** Check that you're passing `storefrontAccessToken` to the provider and `cartId` to `trackAddToCart`. Both are needed for order attribution in Next.js and Remix.

**Console errors about blocked scripts** This is a CSP issue. See the [CSP Configuration](#content-security-policy-csp) section above and make sure all three Elevate domains are allowlisted.

**Content flickers before the test loads** Make sure `preventFlickering={true}` is set on your provider. This hides content briefly until the test configuration loads, preventing a flash of the wrong variant.

**`useExperiment` throws a "must be used within a provider" error** Make sure your component is rendered inside the `ElevateNextProvider` (Next.js) or `ElevateProvider` (Remix). In Next.js, also make sure the component using the hook has `"use client"` at the top of the file.
