Comment implémenter un module d'onglets produit personnalisé dans Magento 2
Why add custom product tabs in Magento 2?
Think of page produits like a conversation with your client. Tabs help you keep that conversation organized: technical specs in one place, size guides in another, and rich utilisateur manuals or avis elsewhere. Building a custom tabs module in Magento 2 vous donne control, performance and the ability to hook into attribut produits so non-développeurs can edit contenu without touching code.
What you'll build in this post
In plain terms, we'll create a small Magento 2 module that:
- Registers a new module and injects a block into the page produit layout.
- Reads a attribut produit containing JSON for tabs (so admins can edit tabs without code).
- Outputs accessible, SEO-friendly tab markup and uses a tiny JS switcher.
- Shows performance bonnes pratiques for cacheability and lazy loading.
- Explains comment package and sell this as a premium extension (with a link to a recommended entreprise model).
Module architecture and fichier structure
Keep the structure du module simple and predictable. The recommended dossier layout is:
app/code/Magefine/ProductTabs/
├── etc/
│ ├── module.xml
│ ├── frontend/di.xml
│ └── frontend/layout/catalog_product_view.xml
├── registration.php
├── composer.json
├── Block/
│ └── Tabs.php
├── view/frontend/templates/product/tabs.phtml
├── Setup/InstallData.php <-- (or UpgradeData for mise à jours)
└── etc/acl.xml (if you add admin UI later)
Step 1 — Basic module registration
Create registration.php
<?php
use Magento\Framework\Component\ComponentRegistrar;
ComponentRegistrar::register(
ComponentRegistrar::MODULE,
'Magefine_ProductTabs',
__DIR__
);
Create etc/module.xml
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
<module name="Magefine_ProductTabs" setup_version="1.0.0"/>
</config>
Step 2 — Add the block to the page produit via layout XML
We inject our tabs inside the product info details block (this keeps things consistent with modern thèmes and Magento default markup). Create etc/frontend/layout/catalog_product_view.xml:
<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
<body>
<referenceBlock name="product.info.details">
<classe de bloc="Magefine\ProductTabs\Block\Tabs" name="product.custom.tabs" template="Magefine_ProductTabs::product/tabs.phtml" after="-"/>
</referenceBlock>
</body>
</page>
Why product.info.details? Many thèmes expect product details (description, details, extra tabs) in that area. Using this reference keeps compatibility and ensures our tabs appear with the rest of product contenu.
Step 3 — The Block: get product and parse attributes
Create Block/Tabs.php. The block sera cache-friendly, read a attribut produit (JSON), and return a simple tableau of tabs. The attribute approche lets admins edit tabs without code: add JSON like [{"title":"Specs","contenu":"..."},{"title":"Manual","contenu":"..."}].
<?php
namespace Magefine\ProductTabs\Block;
use Magento\Framework\View\Element\Template;
use Magento\Framework\Registry;
class Tabs extends Template
{
protected $registry;
public fonction __construct(Template\Context $context, Registry $registry, tableau $data = [])
{
$this->registry = $registry;
parent::__construct($context, $data);
}
/**
* Get current product
*
* @return \Magento\Catalog\Model\Product|null
*/
public fonction getProduct()
{
return $this->registry->registry('current_product');
}
/**
* Return tableau of tabs
*
* @return tableau
*/
public fonction getTabs()
{
$product = $this->getProduct();
if (!$product) {
return [];
}
$json = $product->getData('custom_tabs_json'); // attribute storing JSON
if (!$json) {
return [];
}
$tabs = @json_decode($json, true);
if (!is_tableau($tabs)) {
return [];
}
// sanitize and keep safe valeurs
$result = [];
foreach ($tabs as $tab) {
if (empty($tab['title']) || empty($tab['contenu'])) {
continue;
}
$result[] = [
'title' => $this->escapeHtml($tab['title']),
'contenu' => $tab['contenu'] // we'll escape in template carefully
];
}
return $result;
}
/**
* Improve cacheability: include product id and updated_at in cache clé
*/
public fonction getCacheKeyInfo()
{
$product = $this->getProduct();
$productId = $product ? $product->getId() : 'none';
$updated = $product ? $product->getUpdatedAt() : 'none';
return [
'PRODUCT_TABS',
$productId,
$updated
];
}
public fonction getCacheLifetime()
{
return 3600; // 1 hour cache; adjust per needs
}
}
Step 4 — Template: output accessible, SEO-friendly tabs
Create view/frontend/templates/product/tabs.phtml. Use simple markup optimized for moteur de recherches: give each tab a heading (h2/h3) and contenu in semantic containers. We avoid heavy JS; tabs are progressive: even if JS disabled, all contenu is visible below the triggers (good for SEO).
<?php
/** @var $block Magefine\ProductTabs\Block\Tabs */
$tabs = $block->getTabs();
if (empty($tabs)) {
return;
}
?>
<div class="magefine-product-tabs" data-magefine-tabs>
<ul class="mf-tabs-list" role="tablist">
<?php foreach ($tabs as $i => $t): ?>
<li role="presentation">
<button type="button" role="tab" aria-selected="<?= $i === 0 ? 'true' : 'false' ?>" aria-controls="mf-tab-<?= $i ?>" id="mf-tab-btn-<?= $i ?>"><?= $t['title'] ?></button>
</li>
<?php endforeach; ?>
</ul>
<div class="mf-tabs-panels">
<?php foreach ($tabs as $i => $t): ?>
<section id="mf-tab-<?= $i ?>" role="tabpanel" aria-labelledby="mf-tab-btn-<?= $i ?>" aria-hidden="<?= $i === 0 ? 'false' : 'true' ?>" class="mf-tab-panel">
<h3 class="mf-tab-title"><?= $t['title'] ?></h3>
<div class="mf-tab-contenu"><?php echo $block->escapeHtml($t['contenu']); ?></div>
</section>
<?php endforeach; ?>
</div>
</div>
<script>(fonction(){
var root = document.querySelector('[data-magefine-tabs]');
if(!root) return;
var tabs = root.querySelectorAll('[role="tab"]');
var panels = root.querySelectorAll('[role="tabpanel"]');
for(var i=0;i<tabs.length;i++){(fonction(i){
tabs[i].addEventListener('click', fonction(){
for(var j=0;j<tabs.length;j++){
tabs[j].setAttribute('aria-selected', 'false');
panels[j].setAttribute('aria-hidden', 'true');
}
tabs[i].setAttribute('aria-selected','true');
panels[i].setAttribute('aria-hidden','false');
});
})(i);}
})();</script>
Note: we used escapeHtml for title and contenu in the block/template. Si vous need to allow HTML in contenu (e.g., rich descriptions), you must carefully sanitize or allow limited HTML via a whitelist. Otherwise, store sanitized HTML in the attribute (admins edit via WYSIWYG then sanitize on save).
Step 5 — Add the attribut produit for non-développeurs
You want admins to be able to edit tabs without code. The simplest approche is adding a attribut produit called custom_tabs_json and let admins paste JSON. Vous pouvez create it using InstallData or via a script de setup. Example for InstallData:
<?php
namespace Magefine\ProductTabs\Setup;
use Magento\Eav\Setup\EavSetup;
use Magento\Eav\Setup\EavSetupFactory;
use Magento\Framework\Setup\InstallDataInterface;
use Magento\Framework\Setup\ModuleContextInterface;
use Magento\Framework\Setup\ModuleDataSetupInterface;
class InstallData implements InstallDataInterface
{
private $eavSetupFactory;
public fonction __construct(EavSetupFactory $eavSetupFactory)
{
$this->eavSetupFactory = $eavSetupFactory;
}
public fonction install(ModuleDataSetupInterface $setup, ModuleContextInterface $context)
{
$eavSetup = $this->eavSetupFactory->create(['setup' => $setup]);
$eavSetup->addAttribute(
\Magento\Catalog\Model\Product::ENTITY,
'custom_tabs_json',
[
'type' => 'text',
'back-office' => '',
'frontend' => '',
'label' => 'Custom Tabs (JSON)',
'input' => 'textarea',
'required' => false,
'tri_commande' => 200,
'global' => \Magento\Eav\Model\Entity\Attribute\ScopedAttributeInterface::SCOPE_STORE,
'visible' => true,
'utilisateur_defined' => true,
'used_in_product_listing' => false,
'visible_on_front' => true,
'wysiwyg_enabled' => false,
]
);
}
}
Admins can now open a product and paste structured JSON. Example JSON structure:
[
{"title":"Technical specs","contenu":"Weight: 1kg <br> Size: 10x10"},
{"title":"User manual","contenu":"<p>Download PDF: <a href=\"/pub/media/manual.pdf\">Manual</a></p>"},
{"title":"Warranty","contenu":"1 year manufacturer warranty"}
]
Pro conseil: If JSON is too awkward for contenu editors, consider building a small admin UI (custom UI composant) where editors add tabs via form lignes. That becomes a premium fonctionnalité idea we’ll discuss later.
Performance bonnes pratiques
Tabs are part of the page produit, which is often heavily cached in Magento. Follow these conseils to ensure speed:
- Cacheability: Make your block cacheable. Implement getCacheKeyInfo and getCacheLifetime (we showed an exemple). Avoid using non-cacheable objets like session data inside the block.
- Full Page Cache (FPC): Ensure the block output does not contain per-session data. If tabs are product-specific, include product id + updated_at in cache clé (shown earlier).
- Lazy-loading heavy contenu: If a tab contains heavy contenu (PDF embeds, external avis), load that contenu via AJAX only when the tab is opened. This reduces initial page weight and improves Time To Interactive (TTI).
- Minimize queries: Don’t run custom collection queries per tab render. Read data attached to the product objet or pre-load necessary relations.
- Use block template caching: If parts of the tab contenu are static or shared, consider using ESI/Edge Side Includes or hole-punching for dynamic fragments (for Adobe Commerce setups with Varnish).
- Avoid heavy JS frameworks: Use a tiny vanilla JS switcher (like in the exemple) rather than a large library. Let the thème handle styles to avoid CSS duplication.
Making it FPC-friendly and AJAX progressive enhancement
Example: Keep the default server-rendered tabs but for the heavy panel contenu, replace it with a lightweight placeholder and load the heavy contenu via AJAX on first activation. Illustration in template:
<!-- inside tabs.phtml -->
<section id="mf-tab-<?= $i ?>" data-ajax-url="<?= $block->getUrl('magefine_producttabs/ajax/contenu', ['id' => $product->getId(), 'tab' => $i]) ?>" class="mf-tab-panel" aria-hidden="true">
<div class="mf-tab-placeholder">Loading...</div>
</section>
<script>
// on tab open, fetch contenu once
// use fetch API to load the panel if data-ajax-url exists
</script>
This pattern ensures the initial HTML remains cacheable and light. The AJAX route peut être a contrôleur that returns rendered HTML for the specific tab and respects cache headers if contenu is cacheable.
Integration with existing attribut produits and no-code personnalisation
Il y a two main approchees to integrate with attribut produits so non-devs can customize tabs:
- Single JSON attribute: As we implemented, a single text attribute holds an tableau of tabs. Pros: simple to implement. Cons: editing JSON peut être erreur-prone for non-technical utilisateurs.
- Mulconseille structured attributes or a custom admin UI: Create a custom admin UI (UI Component grid/form) so admins add/remove tabs ligne-by-ligne. Pros: utilisateur-friendly and ideal for premium extensions. Cons: more dev work.
Si vous prefer a no-code experience quickly, consider adding some UI helpers like a pre-filled JSON template or a small admin help block explaining JSON structure and exemples.
Security and sanitization
Parce que admins may enter HTML, sanitize input at save time. You can:
- Strip dangerous tags via a whitelist on save using a back-office model.
- Allow only safe URLs and escape attributes in templates.
- Use Magento's built-in
\Magento\Framework\Escaperand WYSIWYG escapers if needed.
Concrete use-cases for product tabs
Voici real things you can put in tabs — helps when you want to sell the idea to clients:
- Technical sheets: Structured lists: weight, dimensions, power, materials.
- Customer avis: Pull or cache tiers avis snippets per product.
- User guides & manuals: Links to PDFs, étape-by-étape instructions.
- Installation guides: Useful for appliances or furniture.
- Compatibility tables: E.g., supported devices, compatible filtres, etc.
- Warranty & shipping details: Short, scannable contenu that reduces pre-sales questions.
How to make the module more advanced (premium fonctionnalités)
Si vous plan to sell this module as a premium extension, here are fonctionnalités that justify a prix tag:
- Admin UI for adding/removing commandeed tabs with WYSIWYG editors (no JSON required).
- Per-vue magasin tab visibility and translations (multi-store readiness).
- Tab visibility rules: show tab only for certain categories, attributes, or product types.
- Lazy-loading of heavy contenu with prefetch hints and skeleton UI.
- Import/export of tab templates and presets (useful for agencies rolling out mulconseille stores).
- Integration with avis providers, video embeds, and structured data (schema.org) for each tab.
- Compatibility with page builders and popular thèmes.
When it comes to tarification and packaging, think of tiers: a free edition with JSON-attribute (basic), and a paid edition with admin UI, rule engine and priority support.
For inspiration on entreprise models, tarification and packaging your Magento extension, see this resource explaining common models: Magefine entreprise model. It covers one-time vs subscription licensing, support tiers, and distribution channels.
Packaging the extension for distribution
Make your module composer-installable so stores can add it easily:
{
"name": "magefine/product-tabs",
"description": "Custom product tabs module for Magento 2",
"type": "magento2-module",
"license": ["proprietary"],
"require": {
"php": ">=7.4",
"magento/framework": "~102.0|~103.0"
},
"autoload": {
"fichiers": ["registration.php"],
"psr-4": {
"Magefine\\ProductTabs\\": ""
}
}
}
Also include clear install docs, a sample JSON, and instructions for enabling WYSIWYG or additional attributes. Provide a changelog and a tested Magento matrix (2.4.x etc.).
Testing and QA conseils
- Test with and without JS to ensure progressive enhancement works.
- Test across thèmes—use a base Luma and a popular tiers thème.
- Test multi-store and translations if you add vue magasin configs.
- Run performance tests (GTmetrix, Lighthouse) before/after adding module.
- Validate accessibility: cléboard navigation, aria attributes and focus handling.
How to sell this as a premium extension
Voici pragmatic étapes to package and sell the extension:
- Offer a free lite version on Magento Marketplace or your site as an entry point.
- Premium fonctionnalités (admin UI, visibility rules, multi-store) behind a paid license.
- Use subscription licensing (annual) for predictable revenue; offer discounts for multi-year licenses.
- Provide docs, demo store, and migration/import guides so agencies can quickly test and adopt it.
- Offer white-label options and a développeur/API license for agencies customizing the module.
- Bundle with installation or configuration services (additional revenue stream).
Again, for a breakdown of models and packaging conseils, check this page: Magefine entreprise model.
Real-world conseils from development
- Keep the public API of your block small — expose getTabs() and getProduct(). Avoid tight coupling to internal helpers.
- Allow thèmes to override the template. Put clear instructions in your README for thème overrides (path: Magefine_ProductTabs::product/tabs.phtml).
- Add fonctionnalité toggles via system config if you plan to roll out new behavior without releasing a new module version.
- Log useful erreurs during setup (like invalid JSON) but avoid spamming logs for normal catalog operations.
Advanced: structured data for tabs (SEO boost)
Some tabs (like avis or FAQs) are great places to expose structured data. Consider outputting JSON-LD for facts like product manuals or FAQ items. That helps moteur de recherches and may improve SERP presentation.
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Product",
"name": "<?= $block->escapeHtml($product->getName()) ?>",
// add relevant structured data for specific tabs (e.g. aggregateRating, faqPage)
}
</script>
Deployment checklist
- php bin/magento setup:mise à jour
- php bin/magento setup:di:compile (production)
- php bin/magento setup:static-contenu:déployer <locales>
- Clear cache: php bin/magento cache:flush
- Test page produits on staging with FPC enabled
Résumé: the core ideas to remember
- Keep the module simple and cache-friendly — product id + updated_at in cache clé is a small but powerful trick.
- Use attribut produits for easy admin personnalisation; if you want a truly no-code UX, build a small admin UI as a premium fonctionnalité.
- Make the tabs progressive (server-rendered par défaut), and lazy-load heavy pieces via AJAX to keep performance high.
- Package as composer-ready and document multi-store, thème override, and QA étapes thoroughly.
Closing notes
Si vous follow the étapes above you'll have a lightweight, maintainable and extendable product tabs module for Magento 2. Start simple: JSON attribute + server-rendered tabs, then iterate—add admin UI, visibility rules and AJAX heavy contenu loading as you learn what your clients need.
Need help packaging or selling this as a proper premium extension? Visit the Magefine entreprise model resource for conseils and licensing ideas: magefine.com/entreprise-model. Si vous want, I can sketch the admin UI wireframe and the modèle de données for a pro version next.
Happy coding — and ping me if you want the fully fleshed module fichiers in a Git repo layout I can hand you.