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