Разработка кастомной темы OpenCart
Готовые темы из маркетплейса OpenCart — компромисс между скоростью запуска и соответствием бренду. Кастомная тема даёт полный контроль над HTML-структурой, CSS, производительностью и доступностью. При грамотной реализации кастомная тема быстрее готовых тем за счёт отсутствия неиспользуемого кода.
Архитектура тем в OpenCart 4.x
OpenCart 4.x использует Twig как шаблонизатор вместо PHP-шаблонов в версиях 3.x. Это изменило подход к разработке тем.
Структура кастомной темы:
catalog/view/theme/{theme_name}/
├── template/
│ ├── common/
│ │ ├── header.twig
│ │ ├── footer.twig
│ │ ├── cart.twig
│ │ └── search.twig
│ ├── product/
│ │ ├── category.twig ← каталог
│ │ ├── product.twig ← карточка товара
│ │ ├── search.twig
│ │ └── special.twig
│ ├── checkout/
│ │ ├── cart.twig
│ │ └── checkout.twig
│ └── account/
│ ├── login.twig
│ ├── register.twig
│ └── order.twig
└── stylesheet/
└── (для минимальных переопределений)
CSS и JS подключаются не через папку темы, а через события и конфигурацию контроллера.
Наследование от default-темы
Кастомная тема может быть полностью независимой или расширять тему default. Для второго варианта — в настройках указывается родительская тема:
Admin → System → Settings → Store → Theme → Parent Theme: default
Тогда если в папке кастомной темы нет нужного файла — OpenCart берёт его из default. Это ускоряет разработку: переопределяем только изменённые шаблоны.
Регистрация темы
// Создаём файл extension/myshop/catalog/controller/startup/theme.php
// (или через event system)
// В таблице oc_extension регистрируем тему:
INSERT INTO `oc_extension` (`extension_id`, `extension`, `type`, `code`)
VALUES (NULL, 'opencart', 'theme', 'myshop');
// В oc_setting прописываем путь:
INSERT INTO `oc_setting` (`store_id`, `code`, `key`, `value`)
VALUES (0, 'config', 'config_theme', 'myshop');
Или через Extension Installer, если тема упакована как расширение.
Базовый шаблон header.twig
<!DOCTYPE html>
<html lang="{{ lang }}" dir="{{ direction }}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ title }}</title>
<meta name="description" content="{{ description }}">
{% if canonical %}
<link rel="canonical" href="{{ canonical }}">
{% endif %}
{# Подключаем Bootstrap или собственный CSS #}
<link rel="stylesheet" href="{{ stylesheet }}">
{% for style in styles %}
<link rel="stylesheet" type="text/css" href="{{ style.href }}" media="{{ style.media }}">
{% endfor %}
</head>
<body class="{{ class }}">
<header class="site-header">
<div class="container">
<a class="site-logo" href="{{ home }}">
{% if logo %}
<img src="{{ logo }}" alt="{{ name }}" loading="eager">
{% else %}
<span>{{ name }}</span>
{% endif %}
</a>
<nav class="site-nav">
{% for category in categories %}
<a href="{{ category.href }}" {% if category.children %}class="has-dropdown"{% endif %}>
{{ category.name }}
{% if category.children %}
<ul class="dropdown">
{% for child in category.children %}
<li><a href="{{ child.href }}">{{ child.name }}</a></li>
{% endfor %}
</ul>
{% endif %}
</a>
{% endfor %}
</nav>
<div class="header-actions">
<a href="{{ cart }}" class="cart-icon" data-count="{{ cart_count }}">
Корзина ({{ cart_quantity }})
</a>
{% if logged %}
<a href="{{ account }}">Кабинет</a>
{% else %}
<a href="{{ login }}">Войти</a>
{% endif %}
</div>
</div>
</header>
Карточка товара — product.twig
Ключевые переменные, доступные в шаблоне карточки товара:
{# Основные данные #}
{{ product_id }}, {{ name }}, {{ description }}, {{ model }}
{{ price }}, {{ special }}, {{ tax }}
{{ rating }}, {{ reviews }}
{{ manufacturer }}, {{ manufacturer_href }}
{# Изображения #}
{{ thumb }} {# основное изображение #}
{{ images }} {# массив дополнительных изображений #}
{# Опции #}
{% for option in options %}
{{ option.name }}, {{ option.type }}
{% for value in option.product_option_value %}
{{ value.name }}, {{ value.price }}
{% endfor %}
{% endfor %}
{# SEO #}
{{ meta_title }}, {{ meta_description }}, {{ meta_keyword }}
{{ canonical }}
Шаблон с галереей и выбором опций:
<section class="product-page">
<div class="product-gallery">
<img id="product-image-main"
src="{{ thumb }}"
alt="{{ name }}"
loading="eager"
fetchpriority="high">
<div class="thumbnails">
<img src="{{ thumb }}" data-src="{{ image }}" class="thumb active">
{% for image in images %}
<img src="{{ image.thumb }}" data-src="{{ image.popup }}" class="thumb">
{% endfor %}
</div>
</div>
<div class="product-info">
<h1>{{ name }}</h1>
<div class="product-price">
{% if special %}
<span class="price-old">{{ price }}</span>
<span class="price-new">{{ special }}</span>
{% else %}
<span class="price-current">{{ price }}</span>
{% endif %}
</div>
{% for option in options %}
<div class="product-option">
<label>{{ option.name }}{% if option.required %} *{% endif %}</label>
{% if option.type == 'select' %}
<select name="option[{{ option.product_option_id }}]">
<option value="">— Выберите —</option>
{% for value in option.product_option_value %}
<option value="{{ value.product_option_value_id }}"
{% if value.price %}data-price="{{ value.price }}"{% endif %}>
{{ value.name }}{% if value.price %} (+ {{ value.price }}){% endif %}
</option>
{% endfor %}
</select>
{% elseif option.type == 'radio' %}
<div class="option-radios">
{% for value in option.product_option_value %}
<label class="option-radio">
<input type="radio"
name="option[{{ option.product_option_id }}]"
value="{{ value.product_option_value_id }}">
{{ value.name }}
</label>
{% endfor %}
</div>
{% endif %}
</div>
{% endfor %}
<div class="quantity-row">
<input type="number" name="quantity" value="1" min="1">
<button id="btn-cart" data-id="{{ product_id }}">В корзину</button>
</div>
</div>
</section>
JavaScript в теме
OpenCart 4.x использует собственный AJAX для корзины. Расширение через события:
// catalog/view/javascript/myshop/theme.js
// Добавление в корзину
document.querySelectorAll('[data-id]').forEach(btn => {
btn.addEventListener('click', async function() {
const productId = this.dataset.id
const quantity = document.querySelector('[name="quantity"]')?.value || 1
const options = {}
document.querySelectorAll('[name^="option"]').forEach(el => {
const match = el.name.match(/option\[(\d+)\]/)
if (match && el.value) {
options[match[1]] = el.value
}
})
const response = await fetch('index.php?route=checkout/cart.add', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
product_id: productId,
quantity,
...Object.fromEntries(
Object.entries(options).map(([k, v]) => [`option[${k}]`, v])
)
})
})
const data = await response.json()
if (data.success) {
updateCartWidget(data)
showNotification(data.success)
} else {
showErrors(data.error)
}
})
})
Подключение ресурсов темы
Скрипты и стили подключаются в контроллере через систему событий или напрямую в шаблоне через переменные:
// В event-обработчике или startup-контроллере темы:
$this->document->addStyle(
'catalog/view/javascript/myshop/css/theme.css',
'screen',
100 // sort_order
);
$this->document->addScript(
'catalog/view/javascript/myshop/js/theme.js',
'footer',
100
);
Для production — сборка через Vite или Webpack: минификация, хеш-суффикс для cache busting:
# package.json в корне темы
npm run build
# Генерирует: theme.abc123.css, theme.abc123.js
Адаптивная верстка
OpenCart-тема должна корректно работать на мобильных. Breakpoints:
/* Mobile-first approach */
.product-grid { grid-template-columns: repeat(2, 1fr); gap: 16px; }
@media (min-width: 768px) {
.product-grid { grid-template-columns: repeat(3, 1fr); }
}
@media (min-width: 1200px) {
.product-grid { grid-template-columns: repeat(4, 1fr); }
}
Изображения — с loading="lazy" для всего, кроме первого экрана, srcset для разных плотностей экрана.
Сроки разработки темы
- Верстка header + footer + навигация: 1–2 дня
- Главная страница (баннер, категории, хиты): 1–2 дня
- Каталог с фильтрами: 1–2 дня
- Карточка товара с галереей + опциями: 1–2 дня
- Корзина + оформление заказа: 2–3 дня
- Личный кабинет + страница заказа: 1–2 дня
- Адаптивность + кроссбраузерность: 1–2 дня
Итого: 1,5–2 недели при наличии готового дизайна в Figma. Без дизайна — добавить 1–2 недели на дизайн.







