Skip to content

Integration Examples

End-to-end patterns for integrating SaveLayer into different storefronts. For individual method references, see SDK Code Examples.

Liquid template: product card save button

Add a save/unsave button to any product card in your Shopify theme. Requires the SaveLayer theme app embed to be enabled.

Snippet file

Create snippets/savelayer-heart.liquid:

liquid
{% comment %}
  SaveLayer heart button.
  Usage: {% render 'savelayer-heart', product: product %}
{% endcomment %}

<button
  type="button"
  class="sl-heart"
  aria-label="Toggle wishlist"
  aria-pressed="false"
  data-sl-toggle
  data-context="wishlist"
  data-entity-type="product"
  data-entity-gid="gid://shopify/Product/{{ product.id }}"
>
  <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
    <path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78L12 21.23l8.84-8.84a5.5 5.5 0 0 0 0-7.78z"/>
  </svg>
</button>

JavaScript (inline or in a theme JS file)

js
document.addEventListener('DOMContentLoaded', () => {
  // Toggle on click
  document.addEventListener('click', async (e) => {
    const btn = e.target.closest('[data-sl-toggle]');
    if (!btn || !window.SaveLayer) return;

    btn.disabled = true;

    const result = await window.SaveLayer.toggle({
      context: btn.dataset.context,
      entityType: btn.dataset.entityType,
      entityGid: btn.dataset.entityGid,
      source: 'theme-card',
    });

    btn.disabled = false;

    if (result.ok) {
      const saved = result.data.state === 'active';
      btn.classList.toggle('sl-heart--active', saved);
      btn.setAttribute('aria-pressed', String(saved));
    } else if (result.error.code === 'AUTH_REQUIRED') {
      window.location.href = '/account/login';
    }
  });

  // Set initial state for visible hearts
  document.querySelectorAll('[data-sl-toggle]').forEach(async (btn) => {
    if (!window.SaveLayer) return;

    const result = await window.SaveLayer.isSaved({
      context: btn.dataset.context,
      entityType: btn.dataset.entityType,
      entityGid: btn.dataset.entityGid,
    });

    if (result.ok && result.data.saved) {
      btn.classList.add('sl-heart--active');
      btn.setAttribute('aria-pressed', 'true');
    }
  });
});

CSS

css
.sl-heart {
  background: none;
  border: none;
  cursor: pointer;
  color: #999;
  transition: color 0.2s;
}
.sl-heart:hover {
  color: #e74c3c;
}
.sl-heart--active {
  color: #e74c3c;
}
.sl-heart--active svg {
  fill: currentColor;
}

Usage in a product card

liquid
{% for product in collection.products %}
  <div class="product-card">
    <a href="{{ product.url }}">{{ product.title }}</a>
    {% render 'savelayer-heart', product: product %}
  </div>
{% endfor %}

Headless React / Next.js: wishlist component

For headless storefronts, use the SaveLayerSdk class from @savelayer/sdk directly. Headless stores authenticate through the direct API with JWT tokens instead of the app proxy.

SDK setup

tsx
// lib/savelayer.ts
import { SaveLayerSdk } from '@savelayer/sdk';

export const savelayer = new SaveLayerSdk({
  apiBase: process.env.NEXT_PUBLIC_SAVELAYER_API_BASE!,
  channel: 'headless',
});

Wishlist hook

tsx
// hooks/use-wishlist.ts
import { useState, useEffect, useCallback } from 'react';
import { savelayer } from '../lib/savelayer';

interface WishlistItem {
  id: string;
  entityType: string;
  entityGid: string;
  status: string;
  savedAt?: string;
}

export function useWishlist(context = 'wishlist') {
  const [items, setItems] = useState<WishlistItem[]>([]);
  const [loading, setLoading] = useState(true);
  const [hasNextPage, setHasNextPage] = useState(false);
  const [cursor, setCursor] = useState<string | null>(null);

  const fetchItems = useCallback(async (pageCursor?: string) => {
    setLoading(true);
    const result = await savelayer.list({
      context,
      limit: 20,
      ...(pageCursor ? { cursor: pageCursor } : {}),
    });

    if (result.ok) {
      if (pageCursor) {
        setItems((prev) => [...prev, ...result.data.items]);
      } else {
        setItems(result.data.items);
      }
      setHasNextPage(result.data.pageInfo.hasNextPage);
      setCursor(result.data.pageInfo.endCursor);
    }
    setLoading(false);
  }, [context]);

  useEffect(() => {
    fetchItems();
  }, [fetchItems]);

  const loadMore = () => {
    if (cursor) fetchItems(cursor);
  };

  const removeItem = async (entityGid: string) => {
    const result = await savelayer.remove({
      context,
      entityType: 'product',
      entityGid,
    });
    if (result.ok) {
      setItems((prev) => prev.filter((i) => i.entityGid !== entityGid));
    }
  };

  return { items, loading, hasNextPage, loadMore, removeItem, refetch: () => fetchItems() };
}

Wishlist page component

tsx
// components/WishlistPage.tsx
import { useWishlist } from '../hooks/use-wishlist';

export function WishlistPage() {
  const { items, loading, hasNextPage, loadMore, removeItem } = useWishlist();

  if (loading && items.length === 0) {
    return <p>Loading your wishlist...</p>;
  }

  if (items.length === 0) {
    return <p>Your wishlist is empty.</p>;
  }

  return (
    <div>
      <h1>My Wishlist</h1>
      <ul>
        {items.map((item) => (
          <li key={item.id}>
            <span>{item.entityGid}</span>
            <button onClick={() => removeItem(item.entityGid)}>Remove</button>
          </li>
        ))}
      </ul>
      {hasNextPage && (
        <button onClick={loadMore} disabled={loading}>
          {loading ? 'Loading...' : 'Load more'}
        </button>
      )}
    </div>
  );
}

Toggle button component

tsx
// components/SaveButton.tsx
import { useState, useEffect } from 'react';
import { savelayer } from '../lib/savelayer';

interface SaveButtonProps {
  entityGid: string;
  entityType?: string;
  context?: string;
}

export function SaveButton({
  entityGid,
  entityType = 'product',
  context = 'wishlist',
}: SaveButtonProps) {
  const [saved, setSaved] = useState(false);
  const [busy, setBusy] = useState(false);

  useEffect(() => {
    savelayer
      .isSaved({ context, entityType, entityGid })
      .then((r) => {
        if (r.ok) setSaved(r.data.saved);
      });
  }, [context, entityType, entityGid]);

  const handleToggle = async () => {
    setBusy(true);
    const result = await savelayer.toggle({
      context,
      entityType,
      entityGid,
      source: 'headless',
    });
    if (result.ok) {
      setSaved(result.data.state === 'active');
    }
    setBusy(false);
  };

  return (
    <button onClick={handleToggle} disabled={busy} aria-pressed={saved}>
      {saved ? 'Saved' : 'Save'}
    </button>
  );
}

Custom event handling for analytics

Use SaveLayer.on() to forward save events to your analytics provider. These listeners fire in the browser after successful API calls, alongside Shopify's customer events pipeline.

Google Analytics 4 (gtag)

js
window.SaveLayer?.on('savelayer:item_saved', (payload) => {
  gtag('event', 'add_to_wishlist', {
    items: [{
      item_id: payload.entityGid,
      item_category: payload.entityType,
    }],
  });
});

window.SaveLayer?.on('savelayer:item_removed', (payload) => {
  gtag('event', 'remove_from_wishlist', {
    items: [{
      item_id: payload.entityGid,
      item_category: payload.entityType,
    }],
  });
});

Generic analytics wrapper

js
function trackSaveLayerEvents() {
  if (!window.SaveLayer) return;

  const events = [
    'savelayer:item_saved',
    'savelayer:item_removed',
    'savelayer:item_toggled',
    'savelayer:list_viewed',
  ];

  events.forEach((event) => {
    window.SaveLayer.on(event, (payload) => {
      // Replace with your analytics call
      console.log('[SaveLayer]', event, payload);

      // Example: send to a custom endpoint
      navigator.sendBeacon('/analytics/collect', JSON.stringify({
        event,
        payload,
        timestamp: Date.now(),
      }));
    });
  });
}

// Call after DOM ready
document.addEventListener('DOMContentLoaded', trackSaveLayerEvents);

Combining with Shopify Web Pixels

SDK on() listeners run in the storefront page context. For sandbox-isolated analytics (ad pixels, third-party scripts), use Shopify Web Pixels instead — see Customer Events for setup. Both channels receive the same event payloads, so choose based on your analytics provider's requirements:

ChannelRuns inBest for
SaveLayer.on()Page contextFirst-party analytics, UI updates, custom dashboards
Web Pixel analytics.subscribe()SandboxAd platforms (Meta, Google Ads), third-party analytics