Choosing a Storefront
Luma, Hyvä, PWA Studio or custom: what each one assumes about your team, your modules and your checkout.
Four options are realistic on Magento Open Source 2.4.9. The choice is less about the code than about who maintains it and how much third-party frontend you own.
| Option | Browser stack | Team it assumes | Third-party frontends | Checkout |
|---|---|---|---|---|
| Luma | RequireJS, jQuery, Knockout, LESS | One Magento frontend developer | Work as shipped | Magento's own |
| Hyvä | Alpine.js, Tailwind | Magento developer who writes Tailwind | Each needs a compat module or a rewrite | Luma's, unless you license Hyvä Checkout |
| PWA Studio | React over GraphQL, Node host | React team plus a Magento backend developer | Rebuilt in React | Built from cart mutations |
| Fully custom headless | Any framework over GraphQL | A frontend team that owns the storefront | Rebuilt | Built per payment provider |
Measure before you choose
Count the non-Magento modules that ship view/frontend templates; that number is the porting bill for Hyvä and the rewrite bill for anything headless. List the payment methods actually enabled, since each one is a checkout frontend. Then ask how many people will still be maintaining this in two years.
Adobe Commerce only
Adobe Commerce Storefront on Edge Delivery Services is Adobe's strategic option, and it needs an Adobe Commerce backend — Magento Open Source is not supported. Leave it out of an Open Source decision.
What trips people up
Luma is a defensible answer. A store with heavy module use, one developer and a working checkout usually gets more from fixing images, caching and critical CSS than from a stack change.
Example
<?php
// app/code/Acme/Storefront/Console/Command/FrontendInventory.php
declare(strict_types=1);
namespace Acme\Storefront\Console\Command;
use Magento\Framework\Component\ComponentRegistrar;
use Magento\Framework\Filesystem\Driver\File;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class FrontendInventory extends Command
{
public function __construct(
private readonly ComponentRegistrar $registrar,
private readonly File $file,
?string $name = null
) {
parent::__construct($name);
}
protected function configure(): void
{
$this->setName('acme:frontend:inventory');
$this->setDescription('Count non-Magento frontend templates per module');
parent::configure();
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$total = 0;
foreach ($this->registrar->getPaths(ComponentRegistrar::MODULE) as $module => $path) {
$dir = $path . '/view/frontend/templates';
if (str_starts_with($module, 'Magento_') || !$this->file->isDirectory($dir)) {
continue;
}
$found = array_filter(
$this->file->readDirectoryRecursively($dir),
static fn (string $entry): bool => str_ends_with($entry, '.phtml')
);
$total += count($found);
$output->writeln(sprintf('%-30s %d', $module, count($found)));
}
$output->writeln(sprintf('%-30s %d', 'total', $total));
return Command::SUCCESS;
}
}
// bin/magento acme:frontend:inventory
// Zephyr_Reviews 31
// Northwind_Gdpr 6
// Acme_Storefront 3
// total 40When to use it
- Decide between Hyvä and staying on Luma for a store with twenty third-party modules.
- Produce the porting estimate a storefront replacement proposal needs.
- Explain to a merchant why Edge Delivery Services is not available on their Open Source install.
More examples
Registering the command
A console command exists only once it is added to the CommandList argument. Run bin/magento cache:flush to pick up the new di.xml, and bin/magento setup:di:compile in production mode. If bin/magento list does not show the command, the cache or the compiled DI is stale, not the class.
<!-- app/code/Acme/Storefront/etc/di.xml -->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
<type name="Magento\Framework\Console\CommandList">
<arguments>
<argument name="commands" xsi:type="array">
<item name="acme_frontend_inventory" xsi:type="object">
Acme\Storefront\Console\Command\FrontendInventory
</item>
</argument>
</arguments>
</type>
</config>The third-party modules you would be porting
Read this list alongside the template counts. A module with no frontend templates is free to carry across any storefront choice; one with thirty is a week of work per storefront rewrite. For Hyvä, check each vendor for a published compatibility module before you estimate.
bin/magento module:status | grep -v '^Magento_'
# List of enabled modules:
# Zephyr_Reviews
# Northwind_Gdpr
# Acme_StorefrontThe payment methods that define the checkout work
Every active method with a hosted or redirecting frontend is a separate build in PWA Studio or a custom headless storefront, and may need a compat module under Hyvä. An offline method such as check or money order is trivial; a card form is not. Do this audit before promising a date.
bin/magento config:show payment | grep '/active'
# payment/checkmo/active - 1
# payment/free/active - 1
Discussion