"Recently Viewed" Block Development for 1C-Bitrix
"Recently viewed" is a block that shows products the user opened in the current or a previous session. A classic retention tool: the user leaves a page, then comes back — and doesn't have to search for the product again. Especially valuable on mobile devices, where browser history is inconvenient.
Where to Store View History
Option 1: localStorage (browser). The simplest implementation. JavaScript saves viewed product IDs in localStorage. On page load — reads the list, sends an AJAX request with IDs, receives up-to-date product data.
Pros: works without authentication, no server load. Cons: history doesn't sync across devices, lost when the browser is cleared.
Option 2: Server (database). For authenticated users, history is stored on the server. Anonymous users — in localStorage or via b_sale_fuser (guestID).
Option 3: Hybrid. Anonymous user — localStorage + server-side cache by fuser_id. After login — history migrates to the account. The most complete option.
Implementation via localStorage (Fast Option)
const STORAGE_KEY = 'viewed_products';
const MAX_ITEMS = 20;
// Called when a product page loads
function trackProductView(productId) {
let viewed = getViewedProducts();
// Remove if already present (will move to top)
viewed = viewed.filter(id => id !== productId);
// Add to beginning
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));
}
Server-Side AJAX Handler
// 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 the 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-Side Storage for Authenticated Users
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)
);
// When an authenticated user views a product
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
)
");
}
History Migration on Login
When an anonymous user logs in, migrate 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 User Account
The "View history" section in the user account — a full list with the ability to clear it. Show up to 100 recent products with pagination. "Clear history" button — DELETE from custom_user_recently_viewed by user_id + clear localStorage.
Timeline
| Stage | Timeline |
|---|---|
| localStorage implementation + AJAX handler | 2–3 days |
| Server-side storage for authenticated users | 2–3 days |
| History migration on login | 1 day |
| History page in user 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.

