Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.uselayers.com/llms.txt

Use this file to discover all available pages before exploring further.

In a server-rendered Shopify theme, every product or collection page is a full page load — DOMContentLoaded is a reliable trigger for product_view and collection_view events. In a single-page app (Hydrogen, Next.js Commerce, Remix, custom React/Vue storefronts), navigation is client-side and DOMContentLoaded only fires once. You need to hook into the router instead. This guide assumes you’ve already set up the enqueue() helper and session/attribution helpers from Sending events with navigator.sendBeacon.

Prerequisites

  • A working enqueue() function and a flush-on-pagehide listener.
  • getSessionId() and getAttributionToken() helpers.
  • The Layers storefront access token available to your client bundle (typically via a build-time env var like NEXT_PUBLIC_LAYERS_TOKEN).

React Router / Next.js App Router

Use a top-level effect that re-runs when the pathname changes.
'use client';

import { usePathname, useSearchParams } from 'next/navigation';
import { useEffect } from 'react';

export function LayersTracker({ product, collection }) {
  const pathname = usePathname();
  const search = useSearchParams();

  useEffect(() => {
    if (product) {
      enqueue({
        event_id: crypto.randomUUID(),
        event_type: 'product_view',
        timestamp: new Date().toISOString(),
        session_id: getSessionId(),
        product_id: product.id,
        attribution_token: getAttributionToken(),
      });
    } else if (collection) {
      enqueue({
        event_id: crypto.randomUUID(),
        event_type: 'collection_view',
        timestamp: new Date().toISOString(),
        session_id: getSessionId(),
        collection_handle: collection.handle,
      });
    }
  }, [pathname, search, product?.id, collection?.handle]);

  return null;
}
Drop <LayersTracker product={...} /> into your product page layout and <LayersTracker collection={...} /> into your collection page layout. The effect dependency array re-fires the event when the route changes between two products or two collections.

Remix / React Router v6

Hook into useLocation() the same way.
import { useLocation } from '@remix-run/react';
import { useEffect } from 'react';

export function ProductViewTracker({ product }) {
  const location = useLocation();

  useEffect(() => {
    if (!product) return;
    enqueue({
      event_id: crypto.randomUUID(),
      event_type: 'product_view',
      timestamp: new Date().toISOString(),
      session_id: getSessionId(),
      product_id: product.id,
      attribution_token: getAttributionToken(),
    });
  }, [location.pathname, product?.id]);

  return null;
}

Vue Router

Subscribe to the global afterEach guard.
import { useRouter } from 'vue-router';

const router = useRouter();

router.afterEach((to) => {
  const productId = to.meta?.layers?.productId;
  const collectionHandle = to.meta?.layers?.collectionHandle;

  if (productId) {
    enqueue({
      event_id: crypto.randomUUID(),
      event_type: 'product_view',
      timestamp: new Date().toISOString(),
      session_id: getSessionId(),
      product_id: productId,
      attribution_token: getAttributionToken(),
    });
  } else if (collectionHandle) {
    enqueue({
      event_id: crypto.randomUUID(),
      event_type: 'collection_view',
      timestamp: new Date().toISOString(),
      session_id: getSessionId(),
      collection_handle: collectionHandle,
    });
  }
});
Set meta.layers on each route when you define it:
{
  path: '/products/:handle',
  component: ProductPage,
  meta: {
    layers: () => ({ productId: currentProduct.value?.id }),
  },
}

Capturing the attribution token after every Search / Browse / Blocks call

The attribution token rotates per request. Update it from your data fetcher so subsequent clicks and conversions carry the right one:
async function searchProducts(query) {
  const res = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
  const data = await res.json();
  setAttributionToken(data.attributionToken); // store for later events
  return data;
}
If your storefront fetches Search, Browse, and Blocks data on the server (e.g., Next.js server components, Remix loaders), forward the attributionToken to the client via the page’s props or a small <script> tag so the tracker can pick it up.

Why pagehide still matters in a SPA

Even though your app doesn’t do full page reloads on internal navigation, the browser still fires pagehide when the user:
  • Navigates to an external site.
  • Closes the tab or window.
  • Goes to the back/forward cache on mobile Safari.
The unload-flush listener you set up in the navigator.sendBeacon guide handles all of these. Keep it.

Next steps