JAVASCRIPTshopifybeginner

Shopify Recently Viewed Products

Track and display products customers have recently viewed

#shopify#recently-viewed#localstorage#recommendations
Share this snippet:

Code

javascript
1// Recently Viewed Products Class
2class RecentlyViewed {
3 constructor(options = {}) {
4 this.storageKey = options.storageKey || 'recently_viewed_products';
5 this.maxProducts = options.maxProducts || 8;
6 this.currentProduct = options.currentProduct || null;
7 this.container = document.querySelector(options.container || '[data-recently-viewed]');
8
9 this.init();
10 }
11
12 init() {
13 // Track current product
14 if (this.currentProduct) {
15 this.trackProduct(this.currentProduct);
16 }
17
18 // Render recently viewed products
19 if (this.container) {
20 this.render();
21 }
22 }
23
24 trackProduct(product) {
25 let recentlyViewed = this.getRecentlyViewed();
26
27 // Remove product if already exists
28 recentlyViewed = recentlyViewed.filter(p => p.id !== product.id);
29
30 // Add to beginning of array
31 recentlyViewed.unshift(product);
32
33 // Limit to maxProducts
34 if (recentlyViewed.length > this.maxProducts) {
35 recentlyViewed = recentlyViewed.slice(0, this.maxProducts);
36 }
37
38 // Save to localStorage
39 localStorage.setItem(this.storageKey, JSON.stringify(recentlyViewed));
40 }
41
42 getRecentlyViewed() {
43 try {
44 const data = localStorage.getItem(this.storageKey);
45 return data ? JSON.parse(data) : [];
46 } catch (error) {
47 console.error('Error reading recently viewed:', error);
48 return [];
49 }
50 }
51
52 async render() {
53 let products = this.getRecentlyViewed();
54
55 // Filter out current product
56 if (this.currentProduct) {
57 products = products.filter(p => p.id !== this.currentProduct.id);
58 }
59
60 if (products.length === 0) {
61 this.container.style.display = 'none';
62 return;
63 }
64
65 // Fetch fresh product data
66 const freshProducts = await this.fetchProducts(products.map(p => p.handle));
67
68 this.container.innerHTML = this.getTemplate(freshProducts);
69 this.container.style.display = 'block';
70 }
71
72 async fetchProducts(handles) {
73 const products = [];
74
75 for (const handle of handles) {
76 try {
77 const response = await fetch(`/products/${handle}.js`);
78 const product = await response.json();
79 products.push(product);
80 } catch (error) {
81 console.error(`Error fetching product ${handle}:`, error);
82 }
83 }
84
85 return products;
86 }
87
88 getTemplate(products) {
89 return `
90 <div class="recently-viewed">
91 <h2 class="recently-viewed-title">Recently Viewed</h2>
92
93 <div class="recently-viewed-grid">
94 ${products.map(product => this.getProductCard(product)).join('')}
95 </div>
96 </div>
97 `;
98 }
99
100 getProductCard(product) {
101 return `
102 <div class="recently-viewed-item">
103 <a href="/products/${product.handle}" class="recently-viewed-link">
104 <div class="recently-viewed-image">
105 <img src="${product.featured_image}" alt="${product.title}">
106 </div>
107
108 <div class="recently-viewed-info">
109 <h3 class="recently-viewed-product-title">${product.title}</h3>
110
111 <div class="recently-viewed-price">
112 ${this.formatPrice(product.price)}
113 </div>
114 </div>
115 </a>
116 </div>
117 `;
118 }
119
120 formatPrice(cents) {
121 return '$' + (cents / 100).toFixed(2);
122 }
123
124 clear() {
125 localStorage.removeItem(this.storageKey);
126 }
127}
128
129// Initialize on product pages
130document.addEventListener('DOMContentLoaded', () => {
131 // Check if we're on a product page
132 const productData = document.querySelector('[data-product-json]');
133
134 if (productData) {
135 try {
136 const product = JSON.parse(productData.textContent);
137
138 new RecentlyViewed({
139 currentProduct: {
140 id: product.id,
141 handle: product.handle,
142 title: product.title,
143 image: product.featured_image,
144 price: product.price
145 }
146 });
147 } catch (error) {
148 console.error('Error parsing product data:', error);
149 }
150 } else {
151 // Initialize for displaying recently viewed on other pages
152 new RecentlyViewed({
153 container: '[data-recently-viewed]'
154 });
155 }
156});

Shopify Recently Viewed Products

Keep track of products customers have viewed and display them to encourage return visits and increase conversions.

// Recently Viewed Products Class
class RecentlyViewed {
    constructor(options = {}) {
        this.storageKey = options.storageKey || 'recently_viewed_products';
        this.maxProducts = options.maxProducts || 8;
        this.currentProduct = options.currentProduct || null;
        this.container = document.querySelector(options.container || '[data-recently-viewed]');

        this.init();
    }

    init() {
        // Track current product
        if (this.currentProduct) {
            this.trackProduct(this.currentProduct);
        }

        // Render recently viewed products
        if (this.container) {
            this.render();
        }
    }

    trackProduct(product) {
        let recentlyViewed = this.getRecentlyViewed();

        // Remove product if already exists
        recentlyViewed = recentlyViewed.filter(p => p.id !== product.id);

        // Add to beginning of array
        recentlyViewed.unshift(product);

        // Limit to maxProducts
        if (recentlyViewed.length > this.maxProducts) {
            recentlyViewed = recentlyViewed.slice(0, this.maxProducts);
        }

        // Save to localStorage
        localStorage.setItem(this.storageKey, JSON.stringify(recentlyViewed));
    }

    getRecentlyViewed() {
        try {
            const data = localStorage.getItem(this.storageKey);
            return data ? JSON.parse(data) : [];
        } catch (error) {
            console.error('Error reading recently viewed:', error);
            return [];
        }
    }

    async render() {
        let products = this.getRecentlyViewed();

        // Filter out current product
        if (this.currentProduct) {
            products = products.filter(p => p.id !== this.currentProduct.id);
        }

        if (products.length === 0) {
            this.container.style.display = 'none';
            return;
        }

        // Fetch fresh product data
        const freshProducts = await this.fetchProducts(products.map(p => p.handle));

        this.container.innerHTML = this.getTemplate(freshProducts);
        this.container.style.display = 'block';
    }

    async fetchProducts(handles) {
        const products = [];

        for (const handle of handles) {
            try {
                const response = await fetch(`/products/${handle}.js`);
                const product = await response.json();
                products.push(product);
            } catch (error) {
                console.error(`Error fetching product ${handle}:`, error);
            }
        }

        return products;
    }

    getTemplate(products) {
        return `
            <div class="recently-viewed">
                <h2 class="recently-viewed-title">Recently Viewed</h2>

                <div class="recently-viewed-grid">
                    ${products.map(product => this.getProductCard(product)).join('')}
                </div>
            </div>
        `;
    }

    getProductCard(product) {
        return `
            <div class="recently-viewed-item">
                <a href="/products/${product.handle}" class="recently-viewed-link">
                    <div class="recently-viewed-image">
                        <img src="${product.featured_image}" alt="${product.title}">
                    </div>

                    <div class="recently-viewed-info">
                        <h3 class="recently-viewed-product-title">${product.title}</h3>

                        <div class="recently-viewed-price">
                            ${this.formatPrice(product.price)}
                        </div>
                    </div>
                </a>
            </div>
        `;
    }

    formatPrice(cents) {
        return '$' + (cents / 100).toFixed(2);
    }

    clear() {
        localStorage.removeItem(this.storageKey);
    }
}

// Initialize on product pages
document.addEventListener('DOMContentLoaded', () => {
    // Check if we're on a product page
    const productData = document.querySelector('[data-product-json]');

    if (productData) {
        try {
            const product = JSON.parse(productData.textContent);

            new RecentlyViewed({
                currentProduct: {
                    id: product.id,
                    handle: product.handle,
                    title: product.title,
                    image: product.featured_image,
                    price: product.price
                }
            });
        } catch (error) {
            console.error('Error parsing product data:', error);
        }
    } else {
        // Initialize for displaying recently viewed on other pages
        new RecentlyViewed({
            container: '[data-recently-viewed]'
        });
    }
});

Liquid Template - Product Page

<!-- Add this hidden div with product data on product pages -->
<script type="application/json" data-product-json>
{
    "id": {{ product.id }},
    "handle": "{{ product.handle }}",
    "title": {{ product.title | json }},
    "featured_image": "{{ product.featured_image | img_url: 'large' }}",
    "price": {{ product.price }}
}
</script>

Liquid Template - Display Section

<!-- Add this where you want to display recently viewed products -->
<section class="recently-viewed-section">
    <div class="container">
        <div data-recently-viewed></div>
    </div>
</section>

CSS Styling

.recently-viewed-section {
    padding: 60px 0;
    background: #f9f9f9;
}

.recently-viewed {
    display: none; /* Hidden until populated */
}

.recently-viewed-title {
    font-size: 28px;
    margin-bottom: 30px;
    text-align: center;
}

.recently-viewed-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
    gap: 30px;
}

.recently-viewed-item {
    background: #fff;
    border-radius: 8px;
    overflow: hidden;
    transition: transform 0.3s, box-shadow 0.3s;
}

.recently-viewed-item:hover {
    transform: translateY(-5px);
    box-shadow: 0 5px 20px rgba(0,0,0,0.1);
}

.recently-viewed-link {
    text-decoration: none;
    color: inherit;
    display: block;
}

.recently-viewed-image {
    position: relative;
    padding-bottom: 100%;
    overflow: hidden;
    background: #f5f5f5;
}

.recently-viewed-image img {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
}

.recently-viewed-info {
    padding: 15px;
}

.recently-viewed-product-title {
    font-size: 16px;
    margin: 0 0 10px;
    font-weight: 500;
    overflow: hidden;
    text-overflow: ellipsis;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
}

.recently-viewed-price {
    font-size: 18px;
    font-weight: bold;
    color: #000;
}

/* Responsive */
@media (max-width: 768px) {
    .recently-viewed-grid {
        grid-template-columns: repeat(2, 1fr);
        gap: 15px;
    }

    .recently-viewed-title {
        font-size: 22px;
        margin-bottom: 20px;
    }
}

Advanced: With Slider

// Recently Viewed with Carousel
class RecentlyViewedSlider extends RecentlyViewed {
    getTemplate(products) {
        return `
            <div class="recently-viewed">
                <div class="recently-viewed-header">
                    <h2 class="recently-viewed-title">Recently Viewed</h2>

                    <div class="recently-viewed-controls">
                        <button class="slider-arrow slider-prev" data-prev>←</button>
                        <button class="slider-arrow slider-next" data-next>→</button>
                    </div>
                </div>

                <div class="recently-viewed-slider" data-slider>
                    <div class="recently-viewed-track" data-track>
                        ${products.map(product => this.getProductCard(product)).join('')}
                    </div>
                </div>
            </div>
        `;
    }

    async render() {
        await super.render();

        if (this.container.querySelector('[data-slider]')) {
            this.initSlider();
        }
    }

    initSlider() {
        const track = this.container.querySelector('[data-track]');
        const prevBtn = this.container.querySelector('[data-prev]');
        const nextBtn = this.container.querySelector('[data-next]');

        let position = 0;
        const itemWidth = 280; // Width + gap

        prevBtn.addEventListener('click', () => {
            position = Math.min(position + itemWidth, 0);
            track.style.transform = `translateX(${position}px)`;
        });

        nextBtn.addEventListener('click', () => {
            const maxScroll = -(track.scrollWidth - track.parentElement.offsetWidth);
            position = Math.max(position - itemWidth, maxScroll);
            track.style.transform = `translateX(${position}px)`;
        });
    }
}

Clear Recently Viewed (Optional)

<!-- Add a clear button if needed -->
<button onclick="clearRecentlyViewed()">Clear History</button>

<script>
function clearRecentlyViewed() {
    localStorage.removeItem('recently_viewed_products');
    location.reload();
}
</script>

Features

  • Automatic Tracking: Tracks products as customers view them
  • Persistent: Uses localStorage to persist across sessions
  • Smart Filtering: Excludes current product from display
  • Fresh Data: Fetches live product info on render
  • Configurable Limit: Set maximum number to track
  • Responsive Grid: Mobile-friendly layout
  • Slider Option: Advanced carousel version available
  • Privacy-Friendly: Stored locally, not on server

Related Snippets