Skip to content

Bestsellers

Bestseller lists for Magento 2 stores on Hyvä, built from what customers actually paid for: free gifts, cancellations, refunds and test orders are left out, and a list never fills up with random products. Carousels, grids and numbered lists anywhere in the store, a “#2 in Bags” box on the product page, and lists that refresh from hourly to daily — or right away, with a button.

  • Only real sales. A line the customer paid nothing for — a free gift, a 100% discount, a zero-price item — does not count, whichever module or rule made it free. Cancelled units never count, refunded units are subtracted, and orders in the states you choose (pending payment, payment review…), placed before a date or by test accounts are left out.
  • The period you choose. Last 7, 30, 90, 180 or 365 days, all time, or any number of days — per store view.
  • Never random. When the period has fewer sold products than a list shows, the list continues with the products sold over longer periods (for example 90 days, then 365, then all time), each group ranked among itself. A product that never sold is never listed: a short list is shorter, not padded.
  • Per category, too. Every category gets its own list (anchor categories include their subcategories), and every product knows its place in each category.
  • Anywhere in the store, three looks. A horizontal carousel or a grid of the theme’s own product cards (Hyvä’s product list item, the same as on the category page), or a compact vertical numbered list for sidebars. Placed with Magento’s widgets: on all pages, chosen categories, product pages, the cart or any CMS page or block, in the spot you choose.
  • “Sort by: Bestsellers” in the catalog. A sort option on category and search pages, in the same order as the bestseller lists, with the products that did not sell after them — on Magento’s own search engine (OpenSearch) and on ElasticSuite alike. It can be the store’s default order.
  • “#2 in Bags” on the product page. Under Add to Cart, for products that rank well in their categories, with links to those categories. Limits and wording are settings.
  • Fresh, cheap recalculation. Daily, every 6 hours or hourly (for a sale), plus a Recalculate Now button and a command. Each run re-reads only the orders created or changed since the previous one, and the full page cache is refreshed only for pages whose lists actually changed.
  • Your theme’s look. Built only on the theme’s CSS variables and Hyvä’s own product cards and slider: no Tailwind build, no template of the theme replaced.
  • For developers: a read API (BestsellerProviderInterface) for lists, places, sales and sort scores, and an event after every recalculation.
  • English and Romanian included.
Magento Open Source / Adobe Commerce 2.4.8 (tested on 2.4.8-p5)
Hyvä Theme 1.4 or newer (tested on 1.5.3)
PHP 8.3 – 8.4 (as required by Magento 2.4.8)
Magento cron running (for the scheduled recalculation)

The carousel uses Hyvä’s snap slider; Luma storefronts are not supported. hyva-themes/magento2-theme-module comes from Hyvä’s own Composer repository, which every Hyvä store already has configured.

Add the repository you received with your license to composer.json (once), then:

Terminal window
composer require slova/module-bestsellers:^1.0
bin/magento module:enable Slova_Bestsellers
bin/magento setup:upgrade
bin/magento setup:di:compile # production mode
bin/magento cache:flush
bin/magento slova:bestsellers:recalculate --full
bin/magento indexer:reindex catalogsearch_fulltext

Without Composer, unpack the zip into app/code/Slova/Bestsellers (so that app/code/Slova/Bestsellers/registration.php exists) and run the same bin/magento commands.

setup:upgrade creates two tables: slova_bestsellers_sales (units, revenue and orders per day, store view and product) and slova_bestsellers_rank (the lists), and the hidden product attribute slova_bestseller_rank behind the sort option. The first recalculation reads the orders of the longest period you use; on a large store run it from the command line as above rather than waiting for the schedule. The catalog search reindex adds the sort field to the search index; if you skip it, Magento’s cron rebuilds the index by itself, since installing marks it Reindex required.

Terminal window
composer update slova/module-bestsellers
bin/magento setup:upgrade
bin/magento setup:di:compile # production mode
bin/magento cache:flush

Read CHANGELOG.md first; settings and lists are kept across versions.

First remove the Bestsellers widgets you inserted into CMS pages and blocks ({{widget type="Slova\Bestsellers\Block\Bestsellers" …}}): Magento cannot find them for you. Widget instances made in Content → Widgets are removed with the module.

Installed with Composer:

Terminal window
bin/magento module:uninstall Slova_Bestsellers --remove-data

Installed from the zip (app/code):

Terminal window
bin/magento module:uninstall Slova_Bestsellers --non-composer
rm -rf app/code/Slova/Bestsellers
bin/magento setup:upgrade

Either way the module removes everything it added: both tables, the slova_bestseller_rank attribute with its values, its settings (slova_bestsellers/*) and cron schedule, the last-run report, and the Bestsellers widget instances with their page placements. Orders are not touched. If Bestsellers was the default sort order (Magento’s catalog setting or a category’s), choose another one first.

Stores → Configuration → Slova → Bestsellers → Status → Add a List to the Store opens Magento’s new-widget form with the Bestsellers type and your theme already chosen (it is the same as Content → Widgets → Add Widget):

  1. Storefront Properties: a name for the admin, the store views, and Add Layout Update for every place it should appear — Display on: all pages, anchor / non-anchor categories (all or some), all product types or some, or a specified page (home page, cart, …); Container: the spot on the page (main content top or bottom, sidebar, …); Template: Horizontal carousel, Grid or Vertical numbered list.
  2. Widget Options:
Option
Title Shown above the list. Empty: no title.
Number of Products How many to show (at most). Fewer if fewer sold.
Products From The whole store, The category being browsed (on category pages: that category’s list; elsewhere the whole store) or A category (chosen below).
Category For A category.
Products Per Row Carousel and grid, on wide screens (2–6); fewer on tablets and phones.

In a CMS page or block, Insert Widget → Bestsellers offers the same options plus Display As (carousel, grid or list).

Examples: the store’s top 8 as a carousel at the bottom of every product page; The category being browsed as a numbered list in the sidebar of every category; a grid of a chosen category on a landing page.

Developers can also place the block in layout XML:

<block class="Slova\Bestsellers\Block\Bestsellers" name="my.bestsellers"
template="Slova_Bestsellers::widget/list.phtml">
<arguments>
<argument name="title" xsi:type="string">Bestsellers</argument>
<argument name="product_count" xsi:type="number">5</argument>
<argument name="category_id" xsi:type="string">current</argument> <!-- or an id; empty = store -->
</arguments>
</block>

Catalog sorting: “Sort by: Bestsellers”

Section titled “Catalog sorting: “Sort by: Bestsellers””

With Catalog Sorting → Offer “Sort by: Bestsellers” on (the default), category and search pages get a Bestsellers sort option, named in the store view’s language or as set in Option Name. The order is the store view’s ranking — the same one the lists use, with the fallback periods — and the products that did not sell follow, in the search engine’s usual order. Shoppers can reverse it; it always starts with the best seller, also on search results (which otherwise default to descending).

To make it the default order: Stores → Configuration → Catalog → Catalog → Storefront → Product Listing Sort by → Bestsellers, or per category in Display Settings → Default Product Listing Sort By (a category limiting Available Product Listing Sort By must include it).

How it works: every recalculation writes each product’s place (1 = best seller) to the hidden, store-view-scoped attribute slova_bestseller_rank — only where a place changed — and reindexes those products in the catalog search index (with Update by Schedule, Magento’s indexer cron picks them up). Because it is an ordinary attribute used for sorting, Magento’s search engine and ElasticSuite index and sort it without anything engine-specific. Pages sorted by bestsellers carry the module’s cache tag and are refreshed when the order changes.

Turning the option on (or installing the module) marks the catalog search index Reindex required: the field enters the search index with the next full reindex, which Magento’s cron runs by itself. Turning it off removes the option and the values.

A box under Add to Cart (container product.info.additional) listing the categories where the product ranks best, each linked to its category. Shown only for places within Show Places Up To (default: top 10), at most Categories Shown At Most of them (default 3), and only for categories the product is assigned to directly — “#1 in Default Category” through a parent says nothing the store-wide list does not. Needs Rank Within Each Category.

Stores → Configuration → Slova → Bestsellers

Status — the last recalculation (when, what started it, how long), the schedule, each store view’s periods and list sizes; Recalculate Now (re-reads every order of the longest period and rebuilds all lists) and Add a List to the Store.

Ranking (per store view)

Setting Default
Period Last 30 days The sales that rank the lists. Today is the last day of the period.
Number of Days 14 For Custom number of days.
Fill From Longer Periods 90 days, 365 days When the period has too few sold products. Only real sales; shortest first.
Rank By Units sold Or revenue. Ties: the other one, then the number of orders, then the product ID — the same sales always give the same order.
Count Orders From All store views of the website For store views that translate the same shop. Or this store view only, or all store views.

What Counts as a Sale (whole installation)

Setting Default
Ignore Orders In State Canceled, Pending Payment, Payment Review Whole orders left out. Cancelled units of other orders never count anyway.
Ignore Free Items Yes See How sales are counted.
Subtract Refunded Units Yes
A Bundle Sale Counts For The bundle product Or the products inside it, or both.
Ignore Orders Before empty YYYY-MM-DD, store time: test orders before the launch, imports from an old shop.
Ignore Orders From E-mails empty One per line.

Changing one of these rebuilds the sales history at the next recalculation (saving the settings starts it).

Lists (per store view)

Setting Default
Leave Out Product Types none Their sales are still recorded; they just do not appear in lists (e.g. bundles, when they count for their contents).
Rank Within Each Category Yes Category lists and product page ranks.
Hide Out-of-Stock Products Yes Checked when a list is shown: a product that sells out drops off at once.
Positions Stored Per List 100 How many places each list remembers — not how many a widget shows. Keep it at least as long as the longest list on the site.

Catalog Sorting — Offer “Sort by: Bestsellers” (Yes; whole installation), Option Name (per store view; empty = “Bestsellers” in the store view’s language).

Product Page (per store view) — Show Category Ranks (Yes), Show Places Up To (10), Categories Shown At Most (3), Heading (empty = “Bestseller”), Rank Line (empty = “#%1 in %2”; %1 = place, %2 = category link). Texts left empty follow the store view’s language.

Schedule — Recalculate: daily, every 6 hours or hourly; Time (store time): the daily run, the first of the 6-hour runs, or the minute of the hourly ones.

Every setting shows, under Use system value / Use Default / Use Website, the value it would inherit. Saving the settings recalculates the lists right away when a setting that affects them changed.

  • Free lines. A line is free when row total + tax − discount (plus the discount tax compensation) is below half a cent: what the customer paid for it. This catches gift modules, 100%-off rules and zero-price items alike, and a 90% discount still counts. A bundle’s items are judged by the bundle line.
  • Units. Units ordered, minus units cancelled, minus units refunded (if set). A partly refunded line counts the units kept.
  • Configurable products count as the configurable product, never the chosen variant. Bundles count as set in A Bundle Sale Counts For.
  • Days are store days (the default-scope time zone); orders are read from sales_order, not from Magento’s bestseller report tables (which keep sales of deleted orders and cannot leave out free lines).
  • Listed products are enabled, visible in the catalog, in the store view’s website, and not of a left-out type; the stock check happens when a list is shown.

A run reads the orders created or updated since the previous run (a cancellation or refund of an old order re-reads that order’s day), drops days older than the longest period, then rebuilds each store view’s lists. A change of What Counts as a Sale rebuilds the history from scratch. Runs never overlap (a lock); a run started while another is running is skipped, with a message.

Ways to start one: the schedule (Magento cron, job slova_bestsellers_recalculate), Recalculate Now, saving the settings, or:

Terminal window
bin/magento slova:bestsellers:recalculate # what changed since the last run
bin/magento slova:bestsellers:recalculate --full # every order of the longest period

Pages showing a list carry the cache tag slova_bestsellers_s<store id>; it is cleaned — in Magento’s cache and in Varnish — only when a run actually changed that store view’s lists. An hourly schedule therefore rebuilds those pages at most once an hour, and only when the ranking moved.

Slova\Bestsellers\Api\BestsellerProviderInterface (all reads are indexed lookups in precomputed lists):

Method
getProductIds($storeId, $limit, $categoryId = 0) A list, best first. Category 0 = whole store.
getPosition($storeId, $productId, $categoryId = 0) Place in a list (1 = best), null if not listed.
getCategoryPositions($storeId, $productId) Category id ⇒ position, list_size.
getSalesQty($storeId, $productIds = [], $days = null) Units sold, counted as the lists count them (default: the main period).
getSortScores($storeId, $productIds = []) A sort score per product: N for the best seller down to 1, 0 = did not sell. Same order as the lists and the slova_bestseller_rank attribute (place = N + 1 − score).
getPeriods($storeId) The periods behind the lists.

Event slova_bestsellers_recalculated after every successful run: store_ids (store views whose lists or sort order changed) and product_ids (products whose sort score changed), both empty when nothing moved — for example to warm caches or feed another index.

Product list badges (“Bestseller” on a product card) are theme territory in Hyvä, which has no extension point in the product card: call getPosition() from your theme’s Magento_Catalog::product/list/item.phtml.

  • Sort order on category pages is the store-wide ranking restricted to the category’s products — the same order as that category’s own list.
  • One time zone for the day boundaries (the default-scope locale setting). On daylight-saving change days a day boundary can be off by an hour.
  • Store currency: revenue (for Rank By: Revenue) is summed in the base currency of the orders.

English (built in) and Romanian (i18n/ro_RO.csv). Other languages: translate the phrases of i18n/ro_RO.csv into a language pack; the product page texts can also be set per store view.

Slova — https://slova.tech