Recently Viewed Block in 1C-Bitrix: Choosing the Right Approach
A user finds a product, opens the detail page, checks the specs—and leaves. An hour later, they return but can't remember which product they were looking at. Without a "recently viewed" block, you lose up to 30% of repeat visits. This is especially critical on mobile: browser history is inconvenient, and tabs get mixed up.
Implementing a recently viewed block in 1C-Bitrix is a task we’ve solved for 30+ online stores. In this article, we’ll cover three approaches to storing history: localStorage, server-side storage, and hybrid. You’ll learn how to implement each option and avoid common pitfalls. The development cost is calculated individually, and proper approach selection can save up to 30% on rework. Contact us for a consultation on your project.
How to Implement a "Recently Viewed" Block in Bitrix?
The "recently viewed" block is a mechanism that remembers product IDs viewed by the user and displays them on the page. There are three approaches: localStorage, server database, and hybrid. The choice depends on whether cross-device synchronization is needed and whether it should work without authorization.
localStorage: Fast but No Synchronization
localStorage is the simplest solution. JavaScript saves viewed product IDs in the browser. When the page loads, the list is sent to the server via AJAX, and the server returns up-to-date data (price, availability, discount). It works without authorization and doesn't load the database. The downside: history is tied to one device and is cleared when the browser data is reset.
Server Storage: Synchronization but Only for Authorized Users
For authorized users, we store history in MySQL in the custom_user_recently_viewed table. Each view is a record with user_id, product_id, and time. This provides cross-device synchronization but requires authorization. Anonymous users are left without history unless you use b_sale_fuser (guestID).
Hybrid Approach: Best of Both Worlds
Hybrid: anonymous users get localStorage, authorized users get server storage. On login, history migrates from localStorage to the database. This is a full-featured solution: for both unauthorized and returning customers. According to our data, the hybrid approach outperforms localStorage by 2–3 times in user retention across devices. Order a turnkey block development—we’ll choose the optimal scheme.
Why Choose the Hybrid Approach?
If your online store has a high percentage of authorized users, hybrid is a must. It preserves history even when switching devices. Implementation is more complex but pays off in loyalty. According to 1C-Bitrix documentation, a separate table with optimized indexes is recommended for storing history.
Step-by-Step Guide: Hybrid Approach
- Install a JavaScript tracker on the product detail page to save IDs in localStorage.
- Create an AJAX handler that returns product data for given IDs.
- Implement server storage for authorized users with the
custom_user_recently_viewedtable. - Write a migration script to transfer history from localStorage to the database upon login.
- Display the block on the page and in the personal account.
Implementation: localStorage and AJAX Handler
- Write a JavaScript tracker to save the product ID in localStorage.
- Create an AJAX handler in PHP that returns product data for given IDs.
- Display the block on the page, hiding it if no history exists.
JavaScript Tracker
const STORAGE_KEY = 'viewed_products';
const MAX_ITEMS = 20;
// Called on product detail page load
function trackProductView(productId) {
let viewed = getViewedProducts();
// Remove if already exists (move to front)
viewed = viewed.filter(id => id !== productId);
// Add to front
viewed.unshift(productId);
// Limit size
if (viewed.length > MAX_ITEMS) {
viewed = viewed.slice(0, MAX_ITEMS);
}
localStorage.setItem(STORAGE_KEY, JSON.stringify(viewed));
}
function getViewedProducts() {
try {
return JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]');
} catch {
return [];
}
}
// Load data for the block
function loadRecentlyViewed(currentProductId, containerId, limit = 8) {
const viewed = getViewedProducts()
.filter(id => id !== currentProductId)
.slice(0, limit);
if (viewed.length === 0) {
document.getElementById(containerId).style.display = 'none';
return;
}
fetch('/ajax/recently-viewed/', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ ids: viewed }),
})
.then(r => r.json())
.then(data => renderRecentlyViewed(data.products, containerId));
}
AJAX Handler in PHP
// local/ajax/recently-viewed/index.php
\Bitrix\Main\Loader::includeModule('iblock');
\Bitrix\Main\Loader::includeModule('catalog');
$ids = array_filter(array_map('intval', json_decode(file_get_contents('php://input'), true)['ids'] ?? []));
if (empty($ids) || count($ids) > 20) {
echo json_encode(['products' => []]);
exit;
}
// Preserve order from localStorage
$result = [];
$products = [];
$res = \CIBlockElement::GetList(
[],
['ID' => $ids, 'IBLOCK_ID' => CATALOG_IBLOCK_ID, 'ACTIVE' => 'Y'],
false,
false,
['ID', 'NAME', 'DETAIL_PAGE_URL', 'PREVIEW_PICTURE']
);
while ($product = $res->GetNext()) {
$productId = $product['ID'];
$product['PRICE'] = \CPrice::GetBasePrice($productId);
$products[$productId] = $product;
}
// Restore view order
foreach ($ids as $id) {
if (isset($products[$id])) {
$result[] = $products[$id];
}
}
header('Content-Type: application/json');
echo json_encode(['products' => $result]);
Server Storage and History Migration
For authorized users, we store history in the database. On login, we transfer data from localStorage.
Table Schema
CREATE TABLE custom_user_recently_viewed (
id SERIAL PRIMARY KEY,
user_id INT NOT NULL,
product_id INT NOT NULL,
viewed_at DATETIME DEFAULT NOW(),
UNIQUE KEY uk_user_product (user_id, product_id),
INDEX idx_user (user_id, viewed_at DESC)
);
// On authorized view
if ($USER->IsAuthorized()) {
$userId = $USER->GetID();
// INSERT or UPDATE viewed_at
Application::getConnection()->query("
INSERT INTO custom_user_recently_viewed (user_id, product_id, viewed_at)
VALUES ({$userId}, {$productId}, NOW())
ON DUPLICATE KEY UPDATE viewed_at = NOW()
");
// Limit history to 50 records
Application::getConnection()->query("
DELETE FROM custom_user_recently_viewed
WHERE user_id = {$userId}
AND id NOT IN (
SELECT id FROM (
SELECT id FROM custom_user_recently_viewed
WHERE user_id = {$userId}
ORDER BY viewed_at DESC
LIMIT 50
) t
)
");
}
Migration of History on Login
After successful login of an anonymous user, transfer history from localStorage to the database:
// On successful login event
document.addEventListener('userLoggedIn', function(e) {
const viewed = getViewedProducts();
if (viewed.length === 0) return;
fetch('/ajax/sync-viewed-history/', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ ids: viewed, user_id: e.detail.userId }),
});
});
Display in Personal Account
A "View History" section in the personal account—full list with a clear button. Show up to 100 recent products with pagination. A "Clear History" button performs a DELETE from custom_user_recently_viewed by user_id plus clears localStorage.
Comparison of Approaches
| Approach | Synchronization | Without Authorization | Complexity | Recommendation |
|---|---|---|---|---|
| localStorage | No | Yes | Low | For small projects |
| Server Storage | Yes | No | Medium | For authorized users |
| Hybrid | Yes | Yes | High | For full-featured experience |
What’s Included in the Work
- Analysis of current catalog architecture and user sessions
- Storage scheme design (localStorage, database, or hybrid)
- Development of JavaScript tracker and AJAX handler
- Implementation of server storage with optimized queries
- Migration of history on login
- History page in personal account
- Testing: cross-browser, mobile, load testing
- Documentation for modifications and instructions for content managers
- Code review and handover of access
- Support for 2 weeks after delivery
Estimated Timelines
| Stage | Duration |
|---|---|
| localStorage implementation + AJAX handler | 2–3 days |
| Server storage for authorized users | 2–3 days |
| History migration on login | 1 day |
| History page in personal account | 1–2 days |
| Testing (cross-browser, mobile) | 1–2 days |
Total: 1–1.5 weeks for the full version. localStorage-only: 3–4 days.
Common Mistakes During Development
- Not accounting for localStorage limits (5–10 MB) — history gets trimmed without warning in large catalogs.
- Displaying the current product in the block — users see what they are already viewing.
- Not caching AJAX requests — a request is made on every page load even if history hasn’t changed.
- Not removing duplicates in localStorage — the array grows uncontrollably with frequent navigation.
- Missing handling when the user clears browser cache — the code must gracefully handle missing data.
Order a turnkey development of the "recently viewed" block. We’ll evaluate your project in 1 day and propose the optimal solution. Get a consultation — our specialists with 10+ years of experience and 40+ projects guarantee quality.







