Cómo implementar un sistema de insignias de producto personalizado (Nuevo, Oferta, etc.) en 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 de terceros extension, this post walks you through a pragmatic, step-by-step approach. I’ll show you cómo build a small custom module (PHP/XML), cómo integrate it with Magento marketing rules for automation, cómo avoid performance pitfalls, and cómo 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 le da:
- Precise control over markup, styles and placement.
- No extra license or unpredictable updates.
- The ability to integrate tightly with your business rules and indexadors.
That said, a custom approach needs proper planning to avoid hurting performance or maintainability. Let’s do it right.
Descripción general — 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/indexador job to compute badges in bulk (avoids heavy per-request logic).
- Includes hooks to update badges when stock or sales data changes.
- Shows cómo plug into marketing rules for automation (example using Magento Rule model).
- Shows UX mejores prácticas for design, placement and color choices.
- Explains cómo 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 product attribute (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 basado en that attribute.
Step-by-step: Create the module skeleton
Create a module named Magefine_Badges (replace Magefine with your vendor if needed). Minimal files:
- 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 product attribute
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 parche. Example data parche:
<?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 function __construct(
ModuleDataSetupInterface $moduleDataSetup,
CategorySetupFactory $categorySetupFactory
) {
$this->moduleDataSetup = $moduleDataSetup;
$this->categorySetupFactory = $categorySetupFactory;
}
public function 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,
'user_defined' => true,
'global' => \Magento\Catalog\Model\ResourceModel\Eav\Attribute::SCOPE_GLOBAL,
'visible_on_front' => false,
]
);
$this->moduleDataSetup->getConnection()->endSetup();
}
public static function getDependencies() { return []; }
public function getAliases() { return []; }
}
Después de the parche, run bin/magento setup:actualización. The attribute will exist and you can populate it programmatically from your indexador/cron.
Compute badges: indexador vs cron
Both indexador 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 indexación mechanism. Use a tarea cron if your logic is heavy (calls external APIs, analyzing sales history) and you prefer scheduled batches.
Simple cron processor (example)
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" method="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 function __construct(
LoggerInterface $logger,
CollectionFactory $productCollectionFactory,
\Magento\Catalog\Api\ProductRepositoryInterface $productRepository
) {
$this->logger = $logger;
$this->productCollectionFactory = $productCollectionFactory;
$this->productRepository = $productRepository;
}
public function execute()
{
try {
$collection = $this->productCollectionFactory->create();
$collection->addAttributeToSelect(['created_at', 'price', 'special_price', '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->error($e->getMessage());
}
}
private function determineBadges($product)
{
$badges = [];
// Example: New badge: products created in last N days
$isNew = $this->isNewProduct($product);
if ($isNew) $badges[] = 'new';
// Sale badge: check special price
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 sería added here
return $badges;
}
private function isNewProduct($product)
{
// Simple example: created in last 14 days
$created = strtotime($product->getCreatedAt());
return (time() - $created) <= 14 * 24 * 3600;
}
private function isOnSale($product)
{
return (float)$product->getFinalPrice() < (float)$product->getPrice();
}
private function isLowStock($product)
{
// Si usted use MSI, prefer the inventory API. Esto es a naive example.
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 inventory 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
Vamos a 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 página de producto.
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">
<block class="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">
<block class="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 function getBadgesForProduct($product)
{
$value = $product->getData('product_badges');
if (!$value) return [];
return array_filter(array_map('trim', explode(',', $value)));
}
}
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 mejores prácticas
Badges are visual signposts — they debería ser clear, consistent and unobtrusive. Aquí están some mejores prácticas:
- 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. Por ejemplo, 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/benefit, 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 tooltip 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; border-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 integrating badges with Magento marketing rules so badges puede ser automated basado en your marketing logic. Aquí están two practical ways:
- Reuse Magento Rule condition models (Magento\Rule) to evaluate custom badge rules.
- Create a mapping layer so admin users 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 indexador/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) {
// Asegúrese the product has all attributes needed by the condition
if ($rule->getConditions()->validate($product)) {
// add the rule's badge code to product badges
}
}
This le permite 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-developer way to control badges.
Performance considerations — cómo avoid slowing down página de productos
Key idea: move heavy work out of page render and into background processes or indexadors.
Do this:
- Precompute badge values 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 indexador if the badge depende de product data changes — indexadors puede ser incremental.
- Use caching: the badge HTML puede ser part of product list block and benefit 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 (price, image, product_badges) to reduce DB overhead.
- Si usted need to show dynamic badges per customer (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 inventory API per product in the template.
Example: using an indexador
Indexer advantages:
- Supports partial reindex on product save.
- Better integrated with Magento admin and CLI reindex commands.
Indexer implementation outline:
- Create an indexador class implementing Magento\Framework\Indexer\ActionInterface and TagScopeInterface.
- When products change, the indexador receives product ids to reindex 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 cómo implement them while staying performant.
Low stock
Don’t query stock per product on rendering. Options:
- Batch stock query in cron/indexador using Inventory Reservation/Source API (MSI) and write a badge if salable quantity < threshold.
- Use events: observe stock change events (cataloginventory_stock_item_save_after or MSI events) and enqueue product ids for reindexación.
Trending
Trending usually requires analyzing recent sales. Implement as a scheduled batch job:
- Query sales_order_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 approach; marketing can set conditions like "category is Shoes AND price > 100" and a date range during which the rule is active.
Back-in-stock / Personalized badges
These are customer-specific (e.g. "Back in stock: you saved this item"). Render via a small AJAX call to a controller that checks the customer context and returns a tiny fragment. Keep AJAX minimal and cache responses per customer session if possible.
Triggering updates efficiently
Don’t reindex 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 indexador to handle product save/delete events — it provides incremental updates.
- For structural marketing changes (a badge rule change), you can schedule a full reindex; for small changes, do targeted reindex.
Admin UI ideas for marketing teams
To let non-developers manage badge logic, create an admin screen similar a 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 reindex for affected products.
Testing and QA checklist
- Check that badges appear in página de categorías 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 reindex/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 vista de tienda, make the attribute scope to website or store and compute per-scope in your batch. Be careful with indexador complexity.
Use Redis / Message queue for heavy jobs
If computing trending involves analyzing millions of rows, 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 es importante. 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_price < price & 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 value.
- Frontend fetches product_badges as part of product collection and renders the sale badge.
Common pitfalls & cómo avoid them
- Updating product via repository in large loops puede ser 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 indexación: if you store product_badges as a non-indexable attribute and you query on it, you may see poor performance.
Conclusión — cheat-sheet
Quick checklist to implement a robust, performant badge system:
- Create a product attribute to hold badge codes.
- Compute badges in background: use indexador 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 indexadors and tareas cron.
- Magento Rule framework docs and examples in CatalogPriceRule.
- Magefine blog and docs for Magento 2 performance tips — build integrations that keep FPC intact.
Si usted want, I can generate a ready-to-install minimal module archive with the exact files we discussed (module skeleton, data parche, 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 example Badge Rule admin UI to manage rules from the backend. Which Magento version are you on?