Розробка кастомної теми 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 тижні на дизайн.







