Comment implémenter un système de badges produit personnalisé (Nouveau, Promo, etc.) dans Magento 2
Hey — if you want to add clear, consistent product badges like "New", "Sale", "Low stock" or "Trending" in Magento 2 without installing a tiers extension, this post walks you through a pragmatic, étape-by-étape approche. I’ll show you comment build a small custom module (PHP/XML), comment integrate it with Magento marketing rules for automation, comment avoid performance pitfalls, and comment extend it for dynamic badges. Think of this as chatting with a colleague while pairing on the code.
Why build custom badges (and why not just install an extension)
Extensions are great, but a custom solution vous donne:
- Precise control over markup, styles and placement.
- No extra license or unpredictable updates.
- The ability to integrate tightly with your entreprise rules and indexeurs.
That said, a custom approche needs proper planning to avoid hurting performance or maintainability. Let’s do it right.
Aperçu — what we’ll build
We are going to build a compact Magento 2 module that:
- Defines a product EAV attribute to store computed badge(s).
- Provides a frontend block + template to render badges on product list and product view.
- Includes a cron/indexeur job to compute badges in bulk (avoids heavy per-request logic).
- Includes hooks to update badges when stock or sales data changes.
- Shows comment plug into marketing rules for automation (exemple using Magento Rule model).
- Shows UX bonnes pratiques for design, placement and color choices.
- Explains comment add dynamic badges (low stock, trending) and keeps performance in mind.
High-level architecture
Keep the runtime path light. Don’t calculate badges per product during page rendering. Instead:
- Create a attribut produit (varchar) called
product_badgesthat stores comma-separated badge codes. - Implement an Indexer/Cron that computes the badge list for products and writes it to
product_badges. - On the frontend, fetch the attribute as part of the product collection and render HTML/CSS basé sur that attribute.
Step-by-étape: Create the module skeleton
Create a module named Magefine_Badges (replace Magefine with your vendor if needed). Minimal fichiers:
- app/code/Magefine/Badges/registration.php
- app/code/Magefine/Badges/etc/module.xml
- app/code/Magefine/Badges/etc/di.xml
- app/code/Magefine/Badges/etc/cron.xml (optional)
- app/code/Magefine/Badges/Setup/InstallData.php (or UpgradeData/Declarative schema to add attribute)
- app/code/Magefine/Badges/Model/Indexer or Model/Cron/BadgesProcessor.php
- app/code/Magefine/Badges/view/frontend/templates/badges.phtml
- app/code/Magefine/Badges/view/frontend/layout/catalog_product_view.xml and catalog_category_view.xml
registration.php
<?php
use Magento\Framework\Component\ComponentRegistrar;
ComponentRegistrar::register(
ComponentRegistrar::MODULE,
'Magefine_Badges',
__DIR__
);
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_Badges" setup_version="1.0.0"/>
</config>
Add the attribut produit
We’ll add a simple varchar attribute that stores a comma-separated list of badge codes (e.g. new,sale,low_stock). This will allow simple queries via product collection. Using an attribute also ensures it’s indexable and available in product collections.
Declarative way (recommended on newer Magento versions)
Use InstallData or declarative schema with data correctif. Example data correctif:
<?php
namespace Magefine\Badges\Setup\Patch\Data;
use Magento\Framework\Setup\ModuleDataSetupInterface;
use Magento\Framework\Setup\Patch\DataPatchInterface;
use Magento\Catalog\Setup\CategorySetupFactory;
class AddProductBadgesAttribute implements DataPatchInterface
{
private $moduleDataSetup;
private $categorySetupFactory;
public fonction __construct(
ModuleDataSetupInterface $moduleDataSetup,
CategorySetupFactory $categorySetupFactory
) {
$this->moduleDataSetup = $moduleDataSetup;
$this->categorySetupFactory = $categorySetupFactory;
}
public fonction apply()
{
$this->moduleDataSetup->getConnection()->startSetup();
$categorySetup = $this->categorySetupFactory->create(['setup' => $this->moduleDataSetup]);
$categorySetup->addAttribute(
\Magento\Catalog\Model\Product::ENTITY,
'product_badges',
[
'type' => 'varchar',
'label' => 'Product Badges',
'input' => 'text',
'required' => false,
'visible' => false,
'utilisateur_defined' => true,
'global' => \Magento\Catalog\Model\ResourceModel\Eav\Attribute::SCOPE_GLOBAL,
'visible_on_front' => false,
]
);
$this->moduleDataSetup->getConnection()->endSetup();
}
public static fonction getDependencies() { return []; }
public fonction getAliases() { return []; }
}
Après the correctif, run bin/magento setup:mise à jour. The attribute will exist and you can populate it programmatically from your indexeur/cron.
Compute badges: indexeur vs cron
Both indexeur and cron are valid. Use an Indexer if you want the badge computation to run automatically in response to product changes and integrate with Magento's indexation mechanism. Use a tâche cron if your logic is heavy (calls external APIs, analyzing sales history) and you prefer scheduled batches.
Simple cron processor (exemple)
Create a cron that runs every 5–15 minutes and recalculates badges for changed products. Put this in etc/cron.xml and wire a class to compute badges.
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Cron:etc/cron.xsd">
<group id="default">
<job name="magefine_badges_cron" instance="Magefine\Badges\Model\Cron\BadgesProcessor" méthode="execute">
<schedule>*/15 * * * *</schedule>
</job>
</group>
</config>
BadgesProcessor (skeleton)
<?php
namespace Magefine\Badges\Model\Cron;
use Psr\Log\LoggerInterface;
use Magento\Catalog\Model\ProductFactory;
use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory;
class BadgesProcessor
{
private $logger;
private $productCollectionFactory;
private $productRepository;
public fonction __construct(
LoggerInterface $logger,
CollectionFactory $productCollectionFactory,
\Magento\Catalog\Api\ProductRepositoryInterface $productRepository
) {
$this->logger = $logger;
$this->productCollectionFactory = $productCollectionFactory;
$this->productRepository = $productRepository;
}
public fonction execute()
{
try {
$collection = $this->productCollectionFactory->create();
$collection->addAttributeToSelect(['created_at', 'prix', 'special_prix', 'special_from_date', 'special_to_date', 'product_badges']);
$collection->setPageSize(200);
$pages = $collection->getLastPageNumber();
for ($page = 1; $page <= $pages; $page++) {
$collection->setCurPage($page);
$collection->load();
foreach ($collection as $product) {
$badges = $this->determineBadges($product);
$product->setData('product_badges', implode(',', $badges));
$this->productRepository->save($product);
}
$collection->clear();
}
} catch (\Exception $e) {
$this->logger->erreur($e->getMessage());
}
}
private fonction determineBadges($product)
{
$badges = [];
// Example: New badge: products created in last N days
$isNew = $this->isNewProduct($product);
if ($isNew) $badges[] = 'new';
// Sale badge: check special prix
if ($this->isOnSale($product)) $badges[] = 'sale';
// Low stock and others are added via dedicated checks
if ($this->isLowStock($product)) $badges[] = 'low_stock';
// Additional checks like trending serait added here
return $badges;
}
private fonction isNewProduct($product)
{
// Simple exemple: created in last 14 days
$created = strtotime($product->getCreatedAt());
return (time() - $created) <= 14 * 24 * 3600;
}
private fonction isOnSale($product)
{
return (float)$product->getFinalPrice() < (float)$product->getPrice();
}
private fonction isLowStock($product)
{
// Si vous use MSI, prefer the inventaire API. C'est a naive exemple.
try {
$stock = $product->getExtensionAttributes()->getStockItem();
if ($stock) {
return ($stock->getQty() <= 5 && !$stock->getIsInStock());
}
} catch (\Exception $e) {
return false;
}
return false;
}
}
Note: For stock, on Magento 2.3+ with MSI, use Magento inventaire APIs to get sources and salable quantities. Don’t query stock per product inside the loop if you can batch it.
Render badges on the frontend
Nous allons fetch the product_badges attribute on product collections and render a small template that outputs badges markup. Add layout updates to include the badge template in product list and page produit.
Layout updates
<!-- view/frontend/layout/catalog_category_view.xml -->
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
<body>
<referenceBlock name="category.products.list">
<classe de bloc="Magefine\Badges\Block\Badges" name="magefine.badges.list" template="Magefine_Badges::badges.phtml"/ >
</referenceBlock>
</body>
</page>
<!-- view/frontend/layout/catalog_product_view.xml -->
<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.media">
<classe de bloc="Magefine\Badges\Block\Badges" name="magefine.badges.view" template="Magefine_Badges::badges.phtml"/ >
</referenceBlock>
</body>
</page>
Block class
<?php
namespace Magefine\Badges\Block;
use Magento\Framework\View\Element\Template;
use Magento\Catalog\Api\ProductRepositoryInterface;
class Badges extends Template
{
public fonction getBadgesForProduct($product)
{
$valeur = $product->getData('product_badges');
if (!$valeur) return [];
return tableau_filtre(tableau_map('trim', explode(',', $valeur)));
}
}
Template (badges.phtml)
<?php
/** @var $block \Magefine\Badges\Block\Badges */
$product = $block->getProduct() ?: (isset($product) ? $product : null);
if (!$product) return;
$badges = $block->getBadgesForProduct($product);
?>
<div class="magefine-product-badges">
<?php foreach ($badges as $badge): ?>
<span class="badge badge--<?php echo $badge; ?>"><?php echo ucfirst(str_replace('_',' ', $badge)); ?></span>
<?php endforeach; ?>
</div>
Styling and UX bonnes pratiques
Badges are visual signposts — they devrait être clear, consistent and unobtrusive. Voici some bonnes pratiques:
- Placement: top-left or top-right corner of the product image is conventional. Keep consistent placement across list and product views.
- Size: ensure badges are readable on small screens. Use rem/em units and media queries.
- Contrast: use accessible contrast. Par exemple, white text on a colored background; keep compliance with WCAG AA for large text.
- Colors: limit the palette. Typical choices: red for sale/urgent actions, green for new/avantage, orange for trending/attention. Avoid more than 3-4 badge colors to maintain focus.
- Count: avoid stacking many badges. If more than 2 badges exist, choose a primary badge and offer a small “+n” indicator or toolconseil for the rest.
- Animations: subtle fades or scale are fine; avoid heavy animations that hurt performance.
Example CSS (minimal)
.magefine-product-badges { position: absolute; top: 8px; left: 8px; display:flex; gap:6px; z-index: 10; }
.badge { padding: .25rem .5rem; bcommande-radius: 4px; color: #fff; font-size: .75rem; font-weight: 600; display: inline-block; }
.badge--sale { background: #e53935; }
.badge--new { background: #4caf50; }
.badge--low_stock { background: #ff9800; }
.badge--trending { background: #2196f3; }
@media (max-width: 480px) {
.badge { font-size: .6rem; padding: .2rem .4rem; }
}
Integration with Magento marketing rules (automation)
You asked specifically about integnote badges with Magento marketing rules so badges peut être automated basé sur your marketing logic. Voici two practical ways:
- Reuse Magento Rule condition models (Magento\Rule) to evaluate custom badge rules.
- Create a mapping layer so admin utilisateurs can create badge rules in a simple UI and the processor evaluates them and marks products.
Approach: Create a Badge Rule entity that uses Magento's Rule model
The Rule framework in Magento is used by Catalog Price Rules and Cart Price Rules. Reusing the Rule condition parser is powerful because it already supports attribute conditions and complex boolean logic.
Steps:
- Create a new table
magefine_badge_ruleand model that extendsMagento\Rule\Model\Rule. - In the admin you provide a form that saves
conditions_serializedand a badge code (likesaleorspecial_offer). - During badge indexeur/cron run, load active badge rules, and evaluate them against product models using rule->getActions()->validate($product) or similar.
Example pseudo-code: evaluating a rule
// $rule is instance of Magefine\Badge\Model\Rule (extends \Magento\Rule\Model\Rule)
$rule->load($id);
foreach ($productCollection as $product) {
// Assurez-vous the product has all attributes needed by the condition
if ($rule->getConditions()->validate($product)) {
// add the rule's badge code to product badges
}
}
This vous permet de author expressive rules in the admin using the same condition UI as Catalog Price Rules. It’s a good integration path that provides marketing teams a non-développeur way to control badges.
Performance considerations — comment avoid slowing down page produits
Key idea: move heavy work out of page render and into background processes or indexeurs.
Do this:
- Precompute badge valeurs and store them on the product entity (attribute). Then include the attribute on product collections using addAttributeToSelect. This makes display a simple attribute read during rendering.
- If a badge relies on stock or sales data, compute it in batch and store the result (don’t compute with many SELECTs for each product on render).
- Use a proper indexeur if the badge dépend de product data changes — indexeurs peut être incremental.
- Use caching: the badge HTML peut être part of product list block and avantage from FPC (Full Page Cache). Avoid adding Vary headers that break caching unless necessary.
- Limit attributes selected in product collections to the ones you need (prix, image, product_badges) to reduce DB overhead.
- Si vous need to show dynamic badges per client (e.g., "Back in stock for you"), use ESI or AJAX fragments to keep FPC intact for the rest of the page.
Avoid this:
- Don’t call expensive services, heavy queries, or PHP loops per-product during page render.
- Don’t load stock data for every product by calling the inventaire API per product in the template.
Example: using an indexeur
Indexer avantages:
- Supports partial réindexer on product save.
- Better integrated with Magento admin and CLI réindexer commands.
Indexer implémentation outline:
- Create an indexeur class implementing Magento\Framework\Indexer\ActionInterface and TagScopeInterface.
- When products change, the indexeur receives product ids to réindexer and you run badge computation only for those ids.
Extending for dynamic badges: low stock, trending, limited time offers
Let’s look at common dynamic badges and comment implement them while staying performant.
Low stock
Don’t query stock per product on rendering. Options:
- Batch stock query in cron/indexeur using Inventory Reservation/Source API (MSI) and write a badge if salable quantity < threshold.
- Use events: observe stock change events (cataloginventaire_stock_item_save_after or MSI events) and enqueue product ids for réindexeration.
Trending
Trending usually requires analyzing recent sales. Implement as a scheduled batch job:
- Query sales_commande_item grouped by product_id for last N days.
- Define a threshold or ranking: top 10% or threshold of units sold.
- Write badge to top products only. Keep historical data to avoid jumping badges too often (smooth using moving average).
Limited-time or campaign badges (marketing-driven)
Use the Badge Rule entity approche; marketing can set conditions like "category is Shoes AND prix > 100" and a date range during which the rule is active.
Back-in-stock / Personalized badges
These are client-specific (e.g. "Back in stock: you saved this item"). Render via a small AJAX call to a contrôleur that checks the client context and returns a tiny fragment. Keep AJAX minimal and cache responses per client session if possible.
Triggering updates efficiently
Don’t réindexer every product every time. Instead:
- Observe domain events and add product ids to a queue (a simple DB table or Redis list) to be processed by the scheduled job.
- Use the indexeur to handle product save/delete events — it provides incremental updates.
- For structural marketing changes (a badge rule change), you can schedule a full réindexer; for small changes, do targeted réindexer.
Admin UI ideas for marketing teams
To let non-développeurs manage badge logic, create an admin screen similaire à Catalog Price Rules:
- Fields: rule name, badge_code, conditions (using Magento Rule condition UI), active from/to, priority, status.
- When rule saves, mark it active and optionally kick off a réindexer for affected products.
Testing and QA checklist
- Check that badges appear in page de catégories and product view consistently.
- Test mobile and responsive behavior.
- Test performance: load category with many products and measure DB queries before and after adding the badge attribute to collection.
- Test rule authoring: create a rule in admin and verify matching products get the badge after réindexer/cron runs.
- Edge cases: product with many badges — ensure UI handles it (stacking, +n indicator).
Advanced ideas & optimizations
Store view / multi-website badges
If badges must differ by vue magasin, make the attribute scope to website or store and compute per-scope in your batch. Be careful with indexeur complexity.
Use Redis / Message queue for heavy jobs
If computing trending involves analyzing millions of lignes, push product ids to a queue and process in workers rather than in a single cron run.
SVGs & sprites
Use SVG icons or CSS sprites for badge visuals to avoid many small image requests. Inline critical CSS for FCP improvements.
Accessibility
Ensure badges have appropriate aria-labels or hidden text for screen readers if the visual meaning est important. Don’t rely only on color.
Example: Full flow for "Sale" badge with marketing rule
- Create rule in admin: name "Sale badge - summer" badge_code="sale" conditions: special_prix < prix & category is "Sale"; active dates set.
- Cron reads all active badge rules, loads product collections including attributes required by conditions, and evaluates each rule for the products.
- When the rule matches a product, its badge code is appended to the product_badges attribute valeur.
- Frontend fetches product_badges as part of product collection and renders the sale badge.
Common pitfalls & comment avoid them
- Updating product via repository in large loops peut être slow — prefer using resource model save or bulk queries when safe.
- Don’t mix per-product heavy logic in templates — keep template rendering simple.
- Be careful with attribute indexation: if you store product_badges as a non-indexable attribute and you query on it, you may see poor performance.
Conclusion — cheat-sheet
Quick checklist to implement a robust, performant badge system:
- Create a attribut produit to hold badge codes.
- Compute badges in background: use indexeur or cron with queueing for incremental updates.
- Allow marketing to define rules via the Magento Rule framework for automation.
- On frontend, add attribute to product collection and render minimal HTML/CSS.
- Optimize: batch stock/sales queries, use cache, avoid per-product DB calls during page render.
- Design: keep placement consistent, use limited color set and ensure accessibility.
Further reading and references
- Magento DevDocs: creating custom indexeurs and tâches cron.
- Magento Rule framework docs and exemples in CatalogPriceRule.
- Magefine blog and docs for Magento 2 performance conseils — build integrations that keep FPC intact.
Si vous want, I can generate a ready-to-install minimal module archive with the exact fichiers we discussed (module skeleton, data correctif, cron processor and templates) customized for your Magento version (2.3 / 2.4 / 2.5). I can also add MSI-aware stock checks or an exemple Badge Rule admin UI to manage rules from the back-office. Which Magento version are you on?