recently-viewed-products
Recently Viewed Products
Overview
Track which products a shopper views and display them in a "Recently Viewed" widget on product pages, the cart, and the homepage. Uses browser storage for within-session and cross-session history. Product data is always re-fetched from the server to avoid showing stale prices or out-of-stock items.
When to Use This Skill
- When implementing a "Continue where you left off" experience for returning visitors
- When adding a "Recently Viewed" widget to product detail pages or the cart drawer
- When integrating with a personalization engine that requires client-side behavioral data
- When building a headless storefront and need lightweight browsing history without a backend dependency
Core Instructions
Step 1: Determine the merchant's platform and choose the right approach
| Platform | Recommended Approach | Why |
|---|---|---|
| Shopify | Enable the built-in Recently Viewed section in your OS2.0 theme, or install Also Bought • Related products / LimeSpot Personalizer | Dawn and most OS2.0 themes include a native Recently Viewed section; LimeSpot ($15/mo) adds AI-powered personalization including recently viewed across sessions |
| WooCommerce | Use WooCommerce's built-in Recently Viewed Products widget or shortcode, or install YITH WooCommerce Frequently Bought Together | WooCommerce includes a recently viewed widget out of the box; place it via Appearance → Widgets or with the [recent_products] shortcode |
| BigCommerce | Use the Recently Viewed widget in the BigCommerce Page Builder or enable it in the Cornerstone theme settings | Cornerstone and BigCommerce Page Builder both include a native Recently Viewed widget that uses browser cookies automatically |
| Custom / Headless | Implement localStorage-based view tracking with a server-side batch endpoint to fetch fresh product data |
Client-side storage means no server state needed; re-fetching data on display ensures prices and stock are current |
Step 2: Enable Recently Viewed on your platform
Shopify
Using a built-in section (OS2.0 themes — Dawn, Sense, Craft):
- Go to Online Store → Themes → Customize
- Navigate to a product page template
- Click Add section and search for "Recently Viewed" — it appears as Recently viewed products in the section list
- Configure it:
- Set Maximum products to show (4–8 recommended)
- Set the section heading text ("Recently Viewed", "Your Browsing History", etc.)
- Set product card style: show price, show rating, etc.
- The section uses JavaScript and browser storage (localStorage) automatically — no additional configuration needed
For the homepage:
- Go to your homepage template
- Add a Recently viewed products section in the same way
- This section will only render content for returning visitors who have previously browsed products
LimeSpot Personalizer (cross-device, AI-powered):
- Install from the Shopify App Store
- LimeSpot tracks views server-side (not just in the browser) so recently viewed persists across devices and browsers
- Configure the Recently Viewed widget placement in the LimeSpot dashboard — it can appear on any page via app blocks
WooCommerce
Built-in Recently Viewed widget:
- Go to Appearance → Widgets
- Find the WooCommerce Recently Viewed Products widget
- Drag it to your Shop Sidebar or Footer widget area
- Configure:
- Number of products to show: 4–6 recommended
- Show title: Yes/No
- Alternatively, place the shortcode
[woocommerce_recently_viewed_products per_page="4"]directly in a product page template, sidebar, or Gutenberg block
For Classic Editor / Gutenberg:
- In Gutenberg, add a Shortcode block and paste
[woocommerce_recently_viewed_products per_page="4"] - For Elementor: use the Shortcode widget to embed the same code
WooCommerce stores recently viewed product IDs in the visitor's session (PHP session / cookie) — no plugin required. The widget reads from this session automatically.
BigCommerce
Built-in Recently Viewed widget (Cornerstone theme):
- Go to Storefront → My Themes → Customize
- Navigate to a product page template
- In the sidebar, find the Recently Viewed section and enable it (or adjust its position in the page layout)
- Set the number of products to display
- BigCommerce uses browser cookies to track recently viewed products — no app needed
Page Builder approach:
- Go to Storefront → Page Builder
- Open any page where you want the widget
- Drag the Recently Viewed Products widget from the widget panel onto the page
- Configure display settings in the widget panel on the right
Custom / Headless
localStorage-based view tracking:
// lib/recentlyViewed.js
const STORAGE_KEY = 'rv_products';
const MAX_ITEMS = 12;
const TTL_MS = 30 * 24 * 60 * 60 * 1000; // 30 days
export function recordView(productId) {
const items = getStoredItems();
const filtered = items.filter(i => i.id !== productId); // remove existing entry
const updated = [{ id: productId, viewedAt: Date.now() }, ...filtered].slice(0, MAX_ITEMS);
try { localStorage.setItem(STORAGE_KEY, JSON.stringify(updated)); } catch {}
}
export function getRecentlyViewedIds(excludeId = null) {
const now = Date.now();
return getStoredItems()
.filter(i => now - i.viewedAt < TTL_MS && i.id !== excludeId)
.map(i => i.id);
}
function getStoredItems() {
try { return JSON.parse(localStorage.getItem(STORAGE_KEY) ?? '[]'); }
catch { return []; }
}
Record view on the product detail page:
// In ProductDetailPage.jsx — read localStorage only in useEffect to avoid SSR mismatch
import { useEffect } from 'react';
import { recordView } from '../lib/recentlyViewed';
export function ProductDetailPage({ product }) {
useEffect(() => { recordView(product.id); }, [product.id]);
return (
<div>
{/* product content */}
<RecentlyViewedWidget excludeId={product.id} maxItems={4} />
</div>
);
}
Widget component — re-fetches fresh product data from the server:
// RecentlyViewedWidget.jsx
export function RecentlyViewedWidget({ excludeId, maxItems = 4 }) {
const [products, setProducts] = useState([]);
useEffect(() => {
const ids = getRecentlyViewedIds(excludeId).slice(0, maxItems);
if (ids.length === 0) return;
fetch('/api/products/by-ids', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ids }),
})
.then(r => r.json())
.then(data => {
// Preserve the viewed order (API may return in any order)
const map = new Map(data.products.map(p => [p.id, p]));
setProducts(ids.map(id => map.get(id)).filter(Boolean));
})
.catch(() => {}); // Non-critical — fail silently
}, [excludeId, maxItems]);
if (products.length === 0) return null;
return (
<section aria-label="Recently viewed">
<h2>Recently Viewed</h2>
<div className="recently-viewed-grid">
{products.map(product => (
<article key={product.id}>
<a href={product.url}>
<img src={product.image} alt={product.name} loading="lazy" width="150" height="150" />
<p>{product.name}</p>
<p>${product.price}</p>
</a>
</article>
))}
</div>
</section>
);
}
Batch product endpoint (Node.js):
// api/products/by-ids.js
export async function getProductsByIds(req, res) {
const { ids } = req.body;
if (!Array.isArray(ids) || ids.length === 0 || ids.length > 20)
return res.status(400).json({ error: 'Invalid ids' });
const products = await db.products.findMany({
where: { id: { in: ids }, published: true },
select: { id: true, name: true, price: true, image: true, url: true, inStock: true },
});
res.json({ products });
}
Best Practices
- Store only product IDs, not full product objects — prices and availability change; always re-fetch from the server when rendering the widget
- Exclude the current product — pass
excludeIdto prevent showing the product the shopper is already viewing - Fail silently — the recently viewed widget is non-critical; catch all errors and render nothing rather than breaking the page
- Hydrate after mount — in SSR frameworks, read from
localStorageinsideuseEffectto avoid server/client mismatch - Respect privacy consent — only write to storage after the user has accepted analytics cookies if your cookie policy requires it
- Consider TTL expiry — purge entries older than 30 days on read to avoid showing discontinued products
Common Pitfalls
| Problem | Solution |
|---|---|
| Hydration mismatch in Next.js / SSR | Read localStorage only inside useEffect, never during render; initialize state as empty array |
localStorage throws in private browsing |
Wrap all reads/writes in try/catch; fall back to in-memory array for the current session |
| Widget shows out-of-stock or deleted products | Filter server response to only include published: true products; never trust stored IDs to reflect current catalog state |
| Duplicate product appears at multiple positions | Before prepending, filter out the existing entry for that product ID |
| Widget causes layout shift on load | Reserve the widget's height with a min-height skeleton while data loads, or position it below the fold |
Related Skills
- @wishlist-save-for-later
- @product-page-design
- @product-comparison
- @storefront-theming