Step-by-Step Hyvä Compatibility — Porting Mageplaza Layered Navigation
Most Hyvä compatibility tutorials stop at "install hyva-themes/magento2-default-theme and pray". This one ports a real Luma-only filter extension — Mageplaza Layered Navigation — to Hyvä across five concrete phases with diffs at every step. Phase 1: composer require the compat-module shell. Phase 2: copy the phtml and strip Knockout data-bind. Phase 3: rebuild the live-filter UI in Alpine.js against the existing Magento AJAX endpoint. Phase 4: swap Luma selectors for Tailwind utility classes that follow the Hyvä theme tokens. Phase 5: a Magewire wire-up for the count badges that need server-side reactivity. Plus a decision matrix on when to ditch the port and buy the Hyvä Catalog Add-on instead.
Hyvä extension compatibility is the engineering pattern that adapts a Luma-only Magento 2 extension — Knockout templates, RequireJS modules, jQuery selectors — to run inside a Hyvä storefront on Magento 2.4.4 — 2.4.9 by replacing the frontend layer with Alpine.js, Tailwind, and optionally Magewire while keeping the original PHP code untouched. The work breaks cleanly into five phases. Below is the complete commit history for porting Mageplaza Layered Navigation, the most-requested third-party filter extension on Hyvä projects in 2026.[1]
Hyvä compatibility is not a rewrite — it is a frontend transplant
The PHP layer of a well-built Luma extension — observers, plugins, blocks, models, repositories, GraphQL resolvers — runs unchanged on Hyvä. What does not run is the Knockout binding layer, the RequireJS module graph, and the jQuery widgets the Luma phtml templates depend on. The Hyvä compat module replaces those three things and nothing else.[2]
Every Luma-only extension is 80% PHP that works on Hyvä and 20% frontend that does not. The compat module replaces only the 20%.
The five phases below isolate the work into reviewable commits. Each phase has a single concern. If the merchant pulls the engagement after phase 3, they have a working extension — the next two phases are polish.
The extension we are porting
Mageplaza Layered Navigation is a paid Magento 2 extension that adds price sliders, multi-select swatches, AJAX-refreshing counts, and a sticky filter sidebar to the category page. It ships exclusively as a Luma extension — Knockout templates, jQuery $.ajax, RequireJS module IDs. On a Hyvä storefront the templates silently fall back to nothing, and the category page renders without any filters at all.[3]
Same pattern applies to Amasty Improved Layered Navigation, BSS Commerce Shop By Brand, and roughly two dozen other "filter sidebar" extensions sold for Luma. The phase breakdown below transfers directly — only the selector names and the AJAX endpoint differ.
What the merchant sees before the port
<!-- Hyvä category page, Mageplaza module enabled -->
<main class="category-view">
<aside class="sidebar-additional">
<!-- Empty. Mageplaza injects nothing because Hyvä strips Knockout. -->
</aside>
<div class="product-listing">
<!-- Products render fine — the listing has no Knockout. -->
</div>
</main>The filter sidebar simply does not appear. The category PLP works but loses every filter beyond the default Magento price and category_ids.
Phase 1 — Compat-module shell via composer
The compat module is a thin layer with one job: register itself, declare a dependency on the original Mageplaza module, and own the view/frontend/templates/ directory that overrides the Luma templates. Nothing more.
composer.json
{
"name": "your-agency/mageplaza-layerednavigation-hyva-compat",
"description": "Hyvä compatibility for Mageplaza Layered Navigation on Magento 2.4.4 — 2.4.9",
"type": "magento2-module",
"version": "1.0.0",
"require": {
"php": "~8.2.0||~8.3.0||~8.4.0",
"magento/framework": ">=103.0.0",
"mageplaza/module-layered-navigation": "^4.0",
"hyva-themes/magento2-default-theme": "^1.3"
},
"autoload": {
"files": ["registration.php"],
"psr-4": {"YourAgency\\MageplazaLayeredNavigationHyvaCompat\\": ""}
}
}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="YourAgency_MageplazaLayeredNavigationHyvaCompat">
<sequence>
<module name="Mageplaza_LayeredNavigation"/>
<module name="Hyva_Theme"/>
</sequence>
</module>
</config>registration.php
<?php
use Magento\Framework\Component\ComponentRegistrar;
ComponentRegistrar::register(
ComponentRegistrar::MODULE,
'YourAgency_MageplazaLayeredNavigationHyvaCompat',
__DIR__
);The Hyvä theme fallback registration
This is the part Hyvä newcomers miss. The compat module's view/frontend/templates/ path only gets picked up if the Hyvä theme's fallbacks list includes it. Add a single entry to app/design/frontend/YourBrand/default/Magento_Theme/templates/Hyva/fallback.xml:
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modules>
<module name="Mageplaza_LayeredNavigation"
fallback="YourAgency_MageplazaLayeredNavigationHyvaCompat"/>
</modules>
</config>That tells Hyvä: any template the Mageplaza module declares, look in the compat module first. The original Mageplaza module stays installed and untouched — its PHP classes still run.
The first commit
+ composer.json
+ etc/module.xml
+ registration.php
+ app/design/frontend/YourBrand/default/Magento_Theme/templates/Hyva/fallback.xml
$ composer require your-agency/mageplaza-layerednavigation-hyva-compat:^1.0
$ bin/magento module:enable YourAgency_MageplazaLayeredNavigationHyvaCompat
$ bin/magento setup:upgrade
$ bin/magento setup:di:compile
$ bin/magento cache:flushAfter phase 1, the category page still renders without filters — the compat module exists but has no templates yet. That is correct. Phases 2 through 5 fill it in.
Phase 2 — Copy the Luma phtml and strip Knockout
The Mageplaza module ships its main filter template at vendor/mageplaza/module-layered-navigation/view/frontend/templates/layer/view.phtml. Copy it into the compat module at the same relative path:
cp vendor/mageplaza/module-layered-navigation/view/frontend/templates/layer/view.phtml \
app/code/YourAgency/MageplazaLayeredNavigationHyvaCompat/view/frontend/templates/layer/view.phtmlThe copied template contains markup that depends on Knockout. Every data-bind="...", every <!-- ko if: ... --> virtual element, every x-magento-init JSON script tag has to go. Here is the before-and-after on the filter-options block:
Luma original (the part that breaks)
<div class="filter-options-item" data-bind="foreach: { data: filters, as: 'filter' }">
<div class="filter-options-title" data-bind="click: $parent.toggleFilter">
<span data-bind="text: filter.label"></span>
</div>
<div class="filter-options-content" data-bind="visible: filter.isOpen">
<ol class="items">
<!-- ko foreach: filter.options -->
<li class="item">
<a data-bind="attr: { href: $data.url }, click: $parent.$parent.applyFilter">
<span data-bind="text: $data.label"></span>
<span class="count" data-bind="text: '(' + $data.count + ')'"></span>
</a>
</li>
<!-- /ko -->
</ol>
</div>
</div>
<script type="text/x-magento-init">
{ "*": { "Mageplaza_LayeredNavigation/js/view/filter": { "ajaxUrl": "<?= $block->getAjaxUrl() ?>" } } }
</script>After phase 2 strip (markup only, no behaviour yet)
<?php /** @var $block \Mageplaza\LayeredNavigation\Block\Navigation\FilterRenderer */ ?>
<?php $filters = $block->getFilters(); ?>
<?php foreach ($filters as $filter): ?>
<div class="filter-options-item">
<div class="filter-options-title">
<span><?= $block->escapeHtml($filter->getName()) ?></span>
</div>
<div class="filter-options-content">
<ol class="items">
<?php foreach ($filter->getItems() as $option): ?>
<li class="item">
<a href="<?= $block->escapeUrl($option->getUrl()) ?>">
<span><?= $block->escapeHtml($option->getLabel()) ?></span>
<span class="count">(<?= (int)$option->getCount() ?>)</span>
</a>
</li>
<?php endforeach; ?>
</ol>
</div>
</div>
<?php endforeach; ?>The diff for phase 2
--- a/view/frontend/templates/layer/view.phtml
+++ b/view/frontend/templates/layer/view.phtml
@@
-<div class="filter-options-item" data-bind="foreach: { data: filters, as: 'filter' }">
- <div class="filter-options-title" data-bind="click: $parent.toggleFilter">
- <span data-bind="text: filter.label"></span>
- </div>
- <div class="filter-options-content" data-bind="visible: filter.isOpen">
- <!-- ko foreach: filter.options -->
- ...
- <!-- /ko -->
- </div>
-</div>
-<script type="text/x-magento-init">...</script>
+<?php foreach ($block->getFilters() as $filter): ?>
+ <div class="filter-options-item">
+ <div class="filter-options-title">
+ <span><?= $block->escapeHtml($filter->getName()) ?></span>
+ </div>
+ ...
+ </div>
+<?php endforeach; ?>After phase 2 the sidebar renders. It does not refresh on click, the dropdowns do not collapse, and there is no AJAX — but the markup is in the DOM and Tailwind classes can be applied. The merchant can already preview the structure on staging.
Phase 3 — Alpine.js port of the live-filter UI
Alpine.js is the reactive layer Hyvä ships by default. The same toggle and AJAX-refresh behaviour Knockout was driving in Luma maps almost one-for-one onto Alpine directives. There is no new server code — the original Mageplaza AJAX endpoint at catalogsearch/result/index accepts the same query parameters.
Wrap each filter group in x-data
<?php foreach ($block->getFilters() as $filter): ?>
<div class="filter-options-item" x-data="{ open: true }">
<button type="button"
class="filter-options-title"
x-on:click="open = !open"
:aria-expanded="open">
<span><?= $block->escapeHtml($filter->getName()) ?></span>
<svg x-bind:class="open ? 'rotate-180' : ''">...</svg>
</button>
<div class="filter-options-content" x-show="open" x-collapse>
<!-- options list -->
</div>
</div>
<?php endforeach; ?>The live-filter fetch() wrapper
Live filtering — the "apply filter without full page reload" behaviour the merchant paid Mageplaza for — comes from a single root x-data component that intercepts filter-link clicks, posts to the Magento AJAX endpoint, and swaps the product grid HTML.
// view/frontend/web/js/alpine/layered-filter.js
window.layeredFilter = () => ({
loading: false,
activeFilters: [],
init() {
// Read filters already applied via URL on page load
const url = new URL(window.location.href);
url.searchParams.forEach((value, key) => {
if (key !== 'p') {
this.activeFilters.push({ key, value });
}
});
},
async applyFilter(event) {
event.preventDefault();
this.loading = true;
const targetUrl = event.currentTarget.getAttribute('href');
try {
const response = await fetch(targetUrl, {
headers: {
'X-Requested-With': 'XMLHttpRequest',
'Accept': 'text/html'
},
credentials: 'same-origin'
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const html = await response.text();
const doc = new DOMParser().parseFromString(html, 'text/html');
// Swap the product grid
const newGrid = doc.querySelector('.products-grid');
const currentGrid = document.querySelector('.products-grid');
if (newGrid && currentGrid) currentGrid.innerHTML = newGrid.innerHTML;
// Swap the filter sidebar (counts will have changed)
const newSidebar = doc.querySelector('.filter-options');
const currentSidebar = document.querySelector('.filter-options');
if (newSidebar && currentSidebar) currentSidebar.innerHTML = newSidebar.innerHTML;
// Update the browser URL without a reload
window.history.pushState({}, '', targetUrl);
} catch (e) {
console.error('Layered filter failed, falling back to full reload:', e);
window.location.href = targetUrl;
} finally {
this.loading = false;
}
}
});The root container wiring
<div x-data="layeredFilter()" x-cloak>
<div class="filter-options">
<?php foreach ($block->getFilters() as $filter): ?>
<!-- group with x-data="{ open: true }" -->
<ol class="items">
<?php foreach ($filter->getItems() as $option): ?>
<li class="item">
<a href="<?= $block->escapeUrl($option->getUrl()) ?>"
x-on:click="applyFilter($event)">
<span><?= $block->escapeHtml($option->getLabel()) ?></span>
<span class="count">(<?= (int)$option->getCount() ?>)</span>
</a>
</li>
<?php endforeach; ?>
</ol>
<?php endforeach; ?>
</div>
<div x-show="loading" class="loading-mask"><span>Filtering...</span></div>
</div>Register the Alpine component with Hyvä
<!-- view/frontend/layout/catalog_category_view.xml -->
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<body>
<referenceBlock name="hyva.script">
<arguments>
<argument name="jsLayout" xsi:type="array">
<item name="layeredFilter" xsi:type="string">
YourAgency_MageplazaLayeredNavigationHyvaCompat::js/alpine/layered-filter.js
</item>
</argument>
</arguments>
</referenceBlock>
</body>
</page>After phase 3 the filter is live. Click a swatch, watch the grid and the sidebar swap without a full reload. The behaviour matches the Luma original — the implementation is one-fifth the size.
Phase 4 — Tailwind class swap matching Hyvä tokens
Phase 2 left the Luma class names in place — .filter-options-title, .filter-options-content, .swatch-attribute. None of them exist in Hyvä's CSS bundle. The sidebar renders unstyled. Phase 4 replaces every Luma selector with Tailwind utilities that resolve through the Hyvä theme tokens so dark mode and brand colour overrides keep working.
The full Luma → Hyvä → effort table
| Luma selector | Hyvä Tailwind port | Effort |
|---|---|---|
.filter-options-title | flex items-center justify-between w-full px-3 py-2 font-medium text-primary border-b border-container | 0.4 h |
.filter-options-content | px-3 py-2 space-y-1 max-h-64 overflow-y-auto | 0.3 h |
.filter-options-item | bg-container rounded-md mb-2 overflow-hidden | 0.2 h |
.swatch-attribute | grid grid-cols-5 gap-1.5 mt-2 | 0.4 h |
.swatch-option.color | w-6 h-6 rounded-full border-2 border-container hover:border-primary cursor-pointer | 0.5 h |
.swatch-option.text | min-w-[2.5rem] h-8 px-2 inline-flex items-center justify-center border border-container rounded text-sm | 0.5 h |
.count | text-xs text-secondary ml-1 | 0.1 h |
.loading-mask | absolute inset-0 bg-white/70 flex items-center justify-center text-primary | 0.3 h |
.price-slider-handle | w-4 h-4 rounded-full bg-primary border-2 border-white shadow cursor-grab | 0.6 h |
.filter-current .item | inline-flex items-center gap-1 px-2 py-1 bg-primary/10 text-primary text-sm rounded | 0.3 h |
The diff that swaps the classes
--- a/view/frontend/templates/layer/view.phtml
+++ b/view/frontend/templates/layer/view.phtml
@@
-<div class="filter-options-item" x-data="{ open: true }">
- <button type="button" class="filter-options-title" x-on:click="open = !open">
- <span><?= $block->escapeHtml($filter->getName()) ?></span>
- </button>
- <div class="filter-options-content" x-show="open" x-collapse>
+<div class="bg-container rounded-md mb-2 overflow-hidden" x-data="{ open: true }">
+ <button type="button"
+ class="flex items-center justify-between w-full px-3 py-2 font-medium text-primary border-b border-container"
+ x-on:click="open = !open">
+ <span><?= $block->escapeHtml($filter->getName()) ?></span>
+ <svg class="w-4 h-4 transition-transform" x-bind:class="open ? 'rotate-180' : ''"
+ fill="none" stroke="currentColor" viewBox="0 0 24 24">
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
+ </svg>
+ </button>
+ <div class="px-3 py-2 space-y-1 max-h-64 overflow-y-auto" x-show="open" x-collapse>Why text-primary instead of text-black
The Hyvä theme exposes a palette through CSS variables (--color-primary, --color-container, --color-secondary) compiled out of tailwind.config.js. Using text-primary instead of text-black means the merchant can rebrand by editing one config file — every Tailwind class in the compat module follows the theme token through to the brand colour. Hard-coding text-black looks the same on launch day and breaks every rebrand after.[4]
Run Tailwind JIT after the swap
cd app/design/frontend/YourBrand/default/web/tailwind
npm run build-prod
# tailwind.config.js purge already includes the compat module's view/frontend/templates pathThe first JIT run picks up every Tailwind utility used in the new template and compiles it into the merchant's styles.css. After this phase the sidebar matches the Hyvä storefront visually — same spacing, same brand colours, same dark-mode behaviour.
Phase 5 — Magewire for server-side reactivity (when you need it)
Phase 3's Alpine.js + fetch() implementation handles the common case. The edge case is when filter counts depend on data the client cannot compute — stock levels that change as another shopper checks out, B2B customer-group-specific filter visibility, or a saved-search badge that ticks up when a new product matches the active filter set.
For those, Magewire is the right tool. It renders server-side on every filter event and pushes only the changed DOM back to the client. Most merchants do not need it. Build phases 1 — 4 first and only add phase 5 if the spec demands it.[5]
The Magewire component
<?php
// Magewire/Catalog/LayerFilter.php
namespace YourAgency\MageplazaLayeredNavigationHyvaCompat\Magewire\Catalog;
use Magewirephp\Magewire\Component;
use Magento\Catalog\Model\Layer\Resolver;
class LayerFilter extends Component
{
public array $activeFilters = [];
public int $productCount = 0;
public function __construct(private Resolver $layerResolver) {}
public function mount(): void
{
$this->recalculate();
}
public function applyFilter(string $code, string $value): void
{
$this->activeFilters[$code] = $value;
$this->recalculate();
}
public function removeFilter(string $code): void
{
unset($this->activeFilters[$code]);
$this->recalculate();
}
private function recalculate(): void
{
$layer = $this->layerResolver->get();
$collection = $layer->getProductCollection();
foreach ($this->activeFilters as $code => $value) {
$collection->addFieldToFilter($code, $value);
}
$this->productCount = $collection->getSize();
}
}The Magewire template
<!-- view/frontend/templates/magewire/catalog/layer-filter.phtml -->
<div wire:id="layer-filter" class="bg-container rounded-md p-3">
<p class="text-sm text-secondary mb-2">
Showing <span class="font-semibold text-primary">
<?= (int)$magewire->productCount ?>
</span> products
</p>
<?php foreach ($block->getFilters() as $filter): ?>
<button wire:click="applyFilter('<?= $filter->getCode() ?>', '<?= $filter->getValue() ?>')"
wire:loading.class="opacity-50"
class="block w-full text-left px-2 py-1 hover:bg-primary/10 rounded">
<?= $block->escapeHtml($filter->getName()) ?>
</button>
<?php endforeach; ?>
</div>Pick one: Alpine.js or Magewire, not both
Running Alpine.js fetch() and Magewire side-by-side on the same component is a guaranteed race condition — both want to own the filter state. Pick Alpine.js for read-mostly filter UIs (the 95% case). Pick Magewire when the count must be server-authoritative on every click. Do not mix.
The decision matrix — port or buy Hyvä Catalog Add-on?
Before quoting the merchant, run this matrix. The Hyvä team sells a Catalog Add-on that replaces Mageplaza Layered Navigation entirely with a Hyvä-native implementation, and on many engagements that is the better answer.[6]
| Constraint | Port Mageplaza to Hyvä | Buy Hyvä Catalog Add-on |
|---|---|---|
| Merchant already paid Mageplaza license, refund not possible | Yes — sunk cost favours port | No — duplicate spend |
| Custom Mageplaza features in use (SEO URLs, slug rules, price step config) | Yes — port preserves admin config | No — features must be re-built |
| Generic price + multi-select swatches only | No — over-engineering | Yes — drop-in |
| Long-term support contract for Hyvä-only stack | No — vendor mismatch grows | Yes — single vendor |
| One-off ecommerce with a small budget | No — port costs more | Yes — lower TCO |
| Multi-store with both Luma and Hyvä storefronts active | Yes — same extension serves both | No — Hyvä-only |
| In-house team that maintains custom extensions | Yes — port stays in repo | Either |
| Budget under €1,500 | No | Yes |
The merchant scoring the engagement on kishansavaliya.com gets a one-page decision summary like the table above before any code is written. About 40% of the time the right answer turns out to be Catalog Add-on, the engagement turns into a migration sprint instead of a port, and the merchant saves 10–15 hours of work.
The compat-module ship checklist
Across roughly 20 Hyvä compatibility ports shipped in 2025–2026, these eight items are the ones that come up in code review every single time. Run through them before tagging the v1.0.0 release.
- The original extension is untouched. No edits to
vendor/mageplaza/. The compat module owns everything new. - The fallback.xml entry is committed. Without it, Hyvä loads the broken Luma template instead of the compat one.
- Every
data-bind,x-magento-init, and<!-- ko -->is gone. Rungrep -r 'data-bind\|x-magento-init\|ko if\|ko foreach' view/frontend/templates. - Alpine directives are namespaced. Use
x-data="layeredFilter()", notx-data="filter()"— collisions with other Hyvä components are a real problem. - Tailwind classes resolve through tokens. No
text-black,bg-white,border-gray-200— usetext-primary,bg-container,border-container. - The AJAX endpoint is the existing Magento URL. No new
webapi.xmlroute. The compat module does not add server endpoints — it consumes what is already there. - Graceful fallback on fetch() error. Network failure must trigger
window.location.href = targetUrlso the page still functions. - Browser back button works.
history.pushStateon every filter click and apopstatelistener that re-renders from the URL.
What the merchant pays for
The five-phase port above takes 18–28 hours on a fresh Mageplaza Layered Navigation engagement, 8–14 hours on the second similar extension we port for the same merchant. That includes the decision-matrix call, the compat-module scaffold, phases 2 — 4, the QA matrix on three browsers, and a 30-day patch window. Phase 5 adds 4–8 hours when the spec needs it. Compare against a full rebuild of the filter behaviour from scratch (~80 hours) or a buy-the-add-on migration (6–10 hours but loses Mageplaza-specific features).
Common variations we ship
Multi-select swatches with brand-specific styling
Most Mageplaza filter customers run colour + size swatches. The phase-4 table covers the generic styling — when the merchant has brand-specific swatch sizing (40×40 instead of 24×24, square instead of round, label visible on hover), customize through Tailwind variants rather than CSS overrides: md:w-10 md:h-10 lg:w-12 lg:h-12 group-hover:opacity-100.
Price slider with two handles
The default Mageplaza price slider uses jQuery UI Slider. Replace with the Alpine.js + noUiSlider pattern — same look, no jQuery dependency, 4 KB instead of 80 KB.
Sticky sidebar on scroll
Single class swap on the container: md:sticky md:top-20 md:self-start. Tailwind handles the sticky behaviour with no JavaScript. Mageplaza shipped this as a paid feature in Luma — on Hyvä it is one class.
FAQ
Does Hyvä officially support porting Mageplaza Layered Navigation?
Hyvä provides a compatibility-module pattern and a fallback.xml mechanism, but it does not ship an official port of Mageplaza Layered Navigation. The Hyvä Catalog Add-on is the recommended replacement when the merchant is open to swapping vendors. Otherwise the port is a five-phase compat-module job as documented above.
Will the original Mageplaza module still work on Luma stores after we install the compat module?
Yes. The compat module only adds templates inside its own view/frontend/templates/ directory and registers a fallback for the Hyvä theme. Luma storefronts continue to use the original Mageplaza templates because Luma does not read the Hyvä fallback config.
How long does the full port take?
18–28 hours for the first Mageplaza port on a new merchant. About 8–14 hours for the second similar extension on the same store because the compat-module scaffold and Tailwind token mapping are already in place.
Do we need Magewire at all?
No, in 80% of cases. Phase 3's Alpine.js + fetch() pattern handles toggle, AJAX refresh, count display, and URL pushState. Magewire only earns its keep when filter counts depend on server-state that changes outside the current shopper's session — stock thresholds, B2B group visibility, saved-search badges.
What if Mageplaza ships an update to the original extension after our port?
Updates that touch only PHP (block classes, models, plugins) work transparently — the compat module does not override them. Updates that change the original phtml structure require us to diff the new vendor/mageplaza/.../view.phtml against the compat copy and merge the new markup. Pin the Mageplaza version in composer.json with ^4.0 not * so updates are reviewable.
Why not just disable the Mageplaza module entirely and re-implement?
The Mageplaza PHP layer runs the filter logic, the SEO URL rewriting, the admin config, the indexer hooks. Re-implementing all of that costs 80+ hours. The port keeps every line of working PHP and replaces only the broken frontend. That is the value of the compat-module pattern — minimum surface area changes.
Does the port survive Magento 2 upgrades from 2.4.4 through 2.4.9?
Yes. The compat module pins to the Mageplaza module's interface, not Magento core. Across the 2.4.4 → 2.4.9 upgrade path we have run this port through three Mageplaza minor versions and one Magento minor version with zero merge conflicts. The Alpine.js and Tailwind layers do not touch Magento core APIs.
Related reading
- How to make any Magento 2 extension Hyvä-compatible
- Magento TTFB optimization — 1.8s to 180ms
- Hyvä theme development service
References
- Production port engagements from kishansavaliya.com Hyvä compatibility audits, October 2025 — May 2026. Mageplaza Layered Navigation versions 4.0.1 through 4.0.7.
- Hyvä Themes documentation, Extension Compatibility, hyva.io/docs/compatibility-modules. Module-naming convention, fallback.xml mechanism, and supported Magento versions.
- Mageplaza GitHub, module-layered-navigation, github.com/mageplaza/magento-2-layered-navigation. Luma-only template references and AJAX endpoint definitions.
- Hyvä Themes documentation, Tailwind configuration and theme tokens, hyva.io/docs/tailwind-config. Primary, container, and secondary palette tokens compiled from tailwind.config.js.
- Magewire project, Magewire — Livewire for Magento 2, github.com/magewirephp/magewire. Component lifecycle, wire:click, wire:loading directives.
- Hyvä Themes commerce page, Hyvä Catalog Add-on, hyva.io/hyva-catalog. Drop-in replacement for Mageplaza, Amasty, and Manadev layered-navigation extensions on Hyvä stores.
I am Kishan Savaliya, an Adobe-Certified Magento + Hyvä developer. I run fixed-scope Hyvä compatibility ports with the five-phase commit pattern above, the decision-matrix call, and a 30-day post-deploy patch window. Fixed quote from $499 audit · $2,499 sprint · ~24h @ $25/hr. See Hyvä theme development service or hire me.