Customizing Standard Components via component_epilog in 1C-Bitrix
component_epilog.php — a file in the component template folder that executes after the component has fully finished, including the template render. Unlike result_modifier.php, at this stage you can no longer affect the HTML output of the current component — but you can execute code with side effects without disrupting caching logic.
Position in the execution sequence
1. component.php (logic, populates $arResult)
2. result_modifier.php (modifies $arResult, before render)
3. template.php (renders HTML)
4. component_epilog.php (after render — actions, JS, analytics)
The file is located in the component template folder:
/local/templates/{site_template}/components/bitrix/catalog.element/default/component_epilog.php
How component_epilog differs from result_modifier
| result_modifier | component_epilog | |
|---|---|---|
| When it executes | Before template render | After template render |
Access to $arResult |
Yes, for modification | Yes, read-only |
| Affects component HTML | Yes (via $arResult) |
No |
| Executes when cached | No (data is taken from cache) | Yes, always |
| For JS and analytics | No | Yes |
| For side effects | No | Yes |
Key difference: component_epilog.php always executes, even when the component serves its result from cache. This makes it the ideal location for code that must run on every request — regardless of cache state.
Practical applications
Outputting JS data for analytics (GA4 / Yandex.Metrica):
Product data for analytics is needed on every page view, even when the component is cached:
// component_epilog.php for bitrix:catalog.element
if (!empty($arResult['ID'])) {
$price = $arResult['CATALOG_PRICE_1'] ?? 0;
$name = $arResult['NAME'] ?? '';
$category = $arResult['SECTION']['NAME'] ?? '';
$productJson = json_encode([
'id' => $arResult['ID'],
'name' => $name,
'price' => $price,
'category' => $category,
], JSON_UNESCAPED_UNICODE);
// Output JS directly from epilog — or inject into <head>
global $APPLICATION;
$APPLICATION->AddHeadScript('');
?>
<script>
window.pageProduct = <?= $productJson ?>;
gtag('event', 'view_item', {
currency: 'RUB',
value: <?= $price ?>,
items: [{ item_id: '<?= $arResult['ID'] ?>', item_name: <?= json_encode($name) ?>, price: <?= $price ?> }]
});
ym(METRIKA_ID, 'reachGoal', 'product_view', { product_id: <?= $arResult['ID'] ?> });
</script>
<?php
}
Registering a product view in an analytics system:
Updating a view counter in a custom table on every page view — without breaking the component cache:
// component_epilog.php for bitrix:catalog.element
if (!empty($arResult['ID'])) {
// Skip bots
$userAgent = $_SERVER['HTTP_USER_AGENT'] ?? '';
if (preg_match('/bot|crawler|spider|crawling/i', $userAgent)) return;
// Increment counter (non-blocking — via queue)
ViewCounterQueue::increment($arResult['ID']);
}
ViewCounterQueue::increment() writes to Redis or a custom table — an agent periodically flushes accumulated views to the main table via a batch UPDATE.
Related products: appending after the main content renders:
If the "similar products" block is heavy and should not be included in the main component's cache:
// component_epilog.php for bitrix:catalog.element
if (!empty($arResult['ID'])) {
// Component with its own cache — independent of the parent's cache
$APPLICATION->IncludeComponent(
'bitrix:catalog.section',
'related_products',
[
'IBLOCK_ID' => $arResult['IBLOCK_ID'],
'FILTER_IDS' => getRelatedProductIds($arResult['ID']),
'CACHE_TYPE' => 'A',
'CACHE_TIME' => 3600,
]
);
}
Logging events without blocking the main request:
// component_epilog.php for bitrix:sale.basket.basket
if (!empty($arResult['ITEMS'])) {
$basketValue = array_sum(array_column($arResult['ITEMS'], 'PRICE'));
// Write to log without waiting
register_shutdown_function(function() use ($basketValue) {
BasketAnalyticsLog::record([
'fuser_id' => \Bitrix\Sale\Fuser::getId(),
'basket_value' => $basketValue,
'items_count' => count($arResult['ITEMS']),
'timestamp' => time(),
]);
});
}
component_epilog and Ajax components
The standard sale.order.ajax component uses AJAX to update checkout steps. In this case component_epilog.php executes on every AJAX request, which can lead to duplicate JS on the page.
Check that the request is not an AJAX request:
// component_epilog.php
if (\Bitrix\Main\Context::getCurrent()->getRequest()->isAjaxRequest()) {
return; // do not output JS on AJAX requests
}
// Main epilog code here
Working with $arResult in epilog
In component_epilog.php, $arResult is the same array that was in result_modifier.php and template.php. It is available for reading, but changes to it do not affect the already-rendered HTML. Changes to $arResult in epilog only affect code that you execute yourself inside epilog.
// component_epilog.php
// This will NOT affect the already-rendered template.php:
$arResult['NAME'] = 'New name'; // pointless
// But you can use $arResult to build JS or API requests:
$analyticsData = [
'product_id' => $arResult['ID'],
'in_stock' => ($arResult['CATALOG_QUANTITY'] ?? 0) > 0,
];
Template folder structure with both files
/local/templates/main/components/bitrix/catalog.element/default/
template.php — HTML template
result_modifier.php — modifies $arResult before render
component_epilog.php — JS, analytics, side effects after render
.description.php — template metadata (optional)
style.css — styles (optional)
script.js — scripts (optional)
Both files are complementary tools: result_modifier.php for data, component_epilog.php for actions. Together they enable complete customization of a standard component's behavior without touching its source code.
Scope of work: writing component_epilog.php for a specific component with analytics and side effects — 4–8 hours. With a view counter queue and agent — another 1–2 days.







