Content Personalization Setup Based on User Behavior in 1C-Bitrix
Personalization in Bitrix is often misunderstood as "show a banner to Moscow users." That's geotargeting, not personalization. Behavioral personalization means a user who viewed laptops three times sees a laptop block on the homepage instead of a random promotional banner. Implementing this in Bitrix without external platforms is possible but requires understanding where to store the behavioral profile.
Storing User Behavioral Profile
For authorized users, data is stored in b_user_field (UF-fields) or a separate table. UF-fields are convenient but limited — they're not designed for JSON blobs with view history. The best solution is a custom table like b_user_behavior:
CREATE TABLE b_user_behavior (
ID SERIAL PRIMARY KEY,
USER_ID INT NOT NULL,
SESSION_ID VARCHAR(64),
EVENT_TYPE VARCHAR(32) NOT NULL, -- 'view', 'cart', 'search'
ENTITY_TYPE VARCHAR(32), -- 'catalog_element', 'section'
ENTITY_ID INT,
VALUE TEXT,
DATE_CREATE TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_ubehav_user ON b_user_behavior(USER_ID, EVENT_TYPE, DATE_CREATE DESC);
For anonymous users, bind to SESSION_ID. When authorizing, merge the session profile with the user profile via the OnAfterUserAuthorize event handler.
Recording Events via AJAX
Every significant action (viewing a product card, adding to cart, search query) is recorded asynchronously. In the catalog.element template at the end of the page:
fetch('/local/ajax/behavior.php', {
method: 'POST',
headers: {'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest'},
body: JSON.stringify({
event: 'view',
entity_type: 'catalog_element',
entity_id: <?= (int)$arResult['ID'] ?>,
session_id: '<?= session_id() ?>'
})
});
The behavior.php file is a D7 controller (Bitrix\Main\Engine\Controller) that validates input and writes to b_user_behavior. Importantly, writing must be non-blocking — no synchronous database operations in the main thread.
Using Profile to Display Content
The homepage component reads the current user's profile and selects content. Priority logic:
- Categories with the most views in the last 30 days
- Products added to cart but not purchased
- Search queries without purchase result
$userId = $GLOBALS['USER']->GetID();
$topCategories = [];
if ($userId) {
$res = $DB->Query("
SELECT ENTITY_ID, COUNT(*) as cnt
FROM b_user_behavior
WHERE USER_ID = {$userId}
AND EVENT_TYPE = 'view'
AND ENTITY_TYPE = 'section'
AND DATE_CREATE > NOW() - INTERVAL 30 DAY
GROUP BY ENTITY_ID
ORDER BY cnt DESC
LIMIT 3
");
while ($row = $res->Fetch()) {
$topCategories[] = (int)$row['ENTITY_ID'];
}
}
Then the bitrix:catalog.section or bitrix:catalog component is called with a filter for these sections.
Caching Personalized Content
Personalized content cannot be cached with Bitrix's standard cache — the block is unique to each user. Solution: a component with CACHE_TYPE = 'N' only for the personalized block, the rest of the page caches normally. Or use bitrix:main.include with AJAX loading of the personal block after page rendering.
The second option is better for performance: the page loads from cache instantly, the personal block is fetched via a separate request after rendering.

