Designing 404 and 500 Error Pages in the Hyvä Theme, the Right Way
AI generated
Hyvä
phtml
Hyvä · 404 · 500 · SEO
404 and 500 Error Pages in the Hyvä Theme
built as engineering, not just repainted

Serving a generic error message with an HTTP status of 200 for a URL that does not exist quietly costs both search engine trust and real users. This article shows how to properly implement 404 and 500 error pages in the Hyvä theme, from the noroute controller through the correct HTTP status code to an Nginx fallback for when PHP-FPM itself stops responding.

12 min read Noroute controller · pub/errors · Nginx fallback Magento 2.4.8 · Hyvä CSP

1. Why default error pages cost you conversions

In most Magento installations, the error page is the least scrutinized template in the entire shop. Anyone who treats 404 and 500 error pages in the Hyvä theme as a purely visual task, slapping a new logo and a handful of Tailwind classes onto the default CMS content, has not actually solved any of the underlying problems. An error page is a technical edge case of the framework, it decides which HTTP status code gets communicated to search engines and monitoring systems, whether any PHP code can run at all, and whether a visitor who hit a dead link stays on the site or leaves for good.

The difference between a purely cosmetic error page and a technically sound solution only shows up under load and in edge cases: when a category URL breaks after a restructuring, when a Redis outage forces a 500 page, or when PHP-FPM itself is unreachable and no Magento code runs at all. The following nine sections cover exactly these cases and show how to design Hyvä error pages so they are correct for search engines, offer visitors a genuine next step, and still deliver something useful even when the application server itself has gone down.

2. Understanding Magento's noroute mechanism

Before building a custom 404 page in the Hyvä theme, it is worth understanding the mechanism Magento already ships with. When the router cannot find a matching action for a requested URL, the front controller does not throw a classic exception, it catches the missing route and forwards the request to whatever noroute action is configured. Which action that is gets decided by the web/default/no_route configuration value, found in the admin under Stores > Configuration > General > Web > Default Pages > CMS No Route Page. Out of the box this points to a plain CMS page, rendered by Magento\Cms\Controller\Noroute\Index, a very thin controller that essentially just injects the configured CMS content into the standard page structure.

For a project that needs more than plain CMS text, such as search suggestions, custom redirect logic, or its own logging, a dedicated noroute handler is the cleaner solution rather than bending the CMS page to do things it was never meant to do. Such a module does not declare its own frontname in routes.xml, instead it overrides the layout handle that cms/noroute/index uses when rendering, and hooks in its own block together with a ViewModel. That leaves Magento's entire noroute mechanism untouched, while 404 and 500 error pages in the Hyvä theme can still run fully custom code.


<?xml version="1.0"?>
<!-- File: app/code/Mironsoft/ErrorPages/view/frontend/layout/cms_noroute_index.xml -->
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"
      layout="1column">
    <body>
        <referenceContainer name="content">
            <!-- Remove the default CMS block content of the no-route page -->
            <referenceBlock name="cms_page" remove="true"/>
            <block class="Magento\Framework\View\Element\Template"
                   name="mironsoft.error.404"
                   template="Mironsoft_ErrorPages::noroute/index.phtml">
                <arguments>
                    <argument name="view_model" xsi:type="object">Mironsoft\ErrorPages\ViewModel\ErrorSuggestions</argument>
                </arguments>
            </block>
        </referenceContainer>
    </body>
</page>

3. A custom 404 template in the Hyvä theme

A Hyvä 404 template differs fundamentally from the Luma variant, not just visually but structurally. The Luma error page pulls in Knockout.js components, UI component layouts and several legacy script bundles, even though the actual page shows nothing more than a handful of links. Designing Hyvä error pages means something different: a single phtml template, styled exclusively with Tailwind utility classes, with no Knockout bindings, no data-mage-init attributes, and no UI component rendering. Interactivity, such as a live filter for search suggestions, comes entirely from Alpine.js, which is already loaded in every Hyvä theme anyway.

In practice this means a Hyvä 404 template needs very little markup: a plain Template block, a ViewModel for the dynamic data, and a phtml file that relies directly on Tailwind classes and Alpine directives. The snippet below shows exactly this pattern, including a small Alpine widget that filters user input client-side against a list of popular search terms, without needing an extra server request for every keystroke.


<?php
/** @var \Magento\Framework\View\Element\Template $block */
/** @var \Hyva\Theme\Model\ViewModelRegistry $viewModels */
/** @var \Hyva\Theme\ViewModel\HyvaCsp $hyvaCsp */
/** @var \Mironsoft\ErrorPages\ViewModel\ErrorSuggestions $errorSuggestions */
$hyvaCsp = $viewModels->require(\Hyva\Theme\ViewModel\HyvaCsp::class);
$errorSuggestions = $viewModels->require(\Mironsoft\ErrorPages\ViewModel\ErrorSuggestions::class);
$categories = $errorSuggestions->getPopularCategories();
$searchTerms = $errorSuggestions->getPopularSearchTerms();
?>
<!-- File: Mironsoft_ErrorPages/templates/noroute/index.phtml -->
<div
    x-data="{
        query: '',
        terms: <?= /* @noEscape */ json_encode($searchTerms) ?>,
        get suggestions() {
            if (this.query.length < 2) return [];
            return this.terms.filter((t) => t.toLowerCase().includes(this.query.toLowerCase()));
        }
    }"
    class="not-prose max-w-3xl mx-auto py-16 px-4 text-center"
>
    <p class="text-7xl font-bold text-slate-800 mb-4">404</p>
    <p class="text-xl font-semibold text-slate-700 mb-8">This page was not found, but you were probably looking for one of these.</p>

    <div class="mb-10">
        <input
            type="text"
            x-model="query"
            placeholder="What were you searching for?"
            class="w-full border border-slate-300 rounded-xl px-4 py-3 text-base"
        >
        <ul x-show="suggestions.length" class="mt-3 text-left bg-white border border-slate-200 rounded-xl divide-y divide-slate-100">
            <template x-for="term in suggestions" :key="term">
                <li class="px-4 py-2">
                    <a :href="'/catalogsearch/result/?q=' + encodeURIComponent(term)" x-text="term" class="text-orange-700 hover:underline"></a>
                </li>
            </template>
        </ul>
    </div>

    <div class="grid grid-cols-2 sm:grid-cols-3 gap-3">
        <?php foreach ($categories as $category): ?>
            <a href="<?= $block->escapeUrl($category['url']) ?>" class="bg-slate-100 hover:bg-slate-200 rounded-lg px-4 py-3 text-sm font-semibold text-slate-800">
                <?= $block->escapeHtml($category['name']) ?>
            </a>
        <?php endforeach; ?>
    </div>
</div>

<script>
    // Notify listeners that a 404 has actually been rendered, see section 9
    document.addEventListener('DOMContentLoaded', () => {
        document.dispatchEvent(new CustomEvent('error-page:404', { detail: { path: window.location.pathname } }));
    });
</script>
<?= $hyvaCsp->registerInlineScript() ?>

4. Forcing the correct HTTP status code

The most expensive mistake in 404 and 500 error pages in the Hyvä theme happens invisibly to the visitor but very visibly to Google: the so-called soft 404. If the noroute controller renders the page correctly but never sets an explicit HTTP status code, Nginx defaults to returning 200 OK, because from the webserver's point of view the request was answered successfully. Google Search Console classifies such pages as soft 404s and treats them inconsistently, some get indexed, some get filtered out, and either way crawl budget is wasted on pages that should not exist in the first place.

The fix is a single line that is nonetheless mandatory, either in your own noroute controller or in a plugin on the default one: $this->getResponse()->setHttpResponseCode(404). Anyone implementing a custom noroute action as described in section two should place this call directly in the execute() method, before the result is returned, not later in the template. A test with curl -I https://shop.example.com/a-url-that-does-not-exist must reliably show HTTP/1.1 404 Not Found afterwards, anything else is a configuration mistake that only shows up months later as worse rankings.

5. Helpful content instead of a dead end

A 404 page in the Hyvä theme that only says "page not found" is a dead end from the visitor's perspective. The visitor had an intent, clicked a link, typed a URL, or used a bookmark, and that intent should be carried forward on the error page as far as possible instead of evaporating completely. In practice that means offering popular categories as direct entry points and surfacing the most frequently used search terms from the shop itself as suggestions, instead of pointing the visitor at an empty search box.

Technically, this job falls to a ViewModel that implements ArgumentInterface and combines two data sources: the active top-level categories from the catalog structure, and the most popular search terms from Magento's search query log. Both sources already exist, they are simply not used for the error page in a stock Hyvä setup. Constructor injection with property promotion in PHP 8.4 keeps the class compact and type-safe, without extra setters or public properties.


<?php

declare(strict_types=1);

namespace Mironsoft\ErrorPages\ViewModel;

use Magento\Catalog\Model\ResourceModel\Category\CollectionFactory as CategoryCollectionFactory;
use Magento\Framework\View\Element\Block\ArgumentInterface;
use Magento\Search\Model\Query;
use Magento\Search\Model\ResourceModel\Query\CollectionFactory as SearchQueryCollectionFactory;

/**
 * Supplies popular categories and frequent search terms for the custom
 * 404 template, so a dead end becomes a set of concrete next steps.
 */
final class ErrorSuggestions implements ArgumentInterface
{
    private const CATEGORY_LIMIT = 6;
    private const SEARCH_TERM_LIMIT = 8;

    /**
     * @param CategoryCollectionFactory $categoryCollectionFactory Builds active top-level category collections
     * @param SearchQueryCollectionFactory $searchQueryCollectionFactory Builds popular search term collections
     */
    public function __construct(
        private readonly CategoryCollectionFactory $categoryCollectionFactory,
        private readonly SearchQueryCollectionFactory $searchQueryCollectionFactory
    ) {
    }

    /**
     * Returns the most relevant active categories to offer as navigation shortcuts.
     *
     * @return array<int, array{name: string, url: string}>
     */
    public function getPopularCategories(): array
    {
        $collection = $this->categoryCollectionFactory->create();
        $collection->addAttributeToSelect(['name', 'url_key'])
            ->addAttributeToFilter('is_active', 1)
            ->addAttributeToFilter('level', 2)
            ->addAttributeToSortFilter('position', 'ASC')
            ->setPageSize(self::CATEGORY_LIMIT);

        $result = [];
        foreach ($collection as $category) {
            $result[] = [
                'name' => (string) $category->getName(),
                'url' => (string) $category->getUrl(),
            ];
        }

        return $result;
    }

    /**
     * Returns the most searched terms from Magento's search query log as suggestions.
     *
     * @return string[]
     */
    public function getPopularSearchTerms(): array
    {
        $collection = $this->searchQueryCollectionFactory->create();
        $collection->setPopularQueryFilter()
            ->setPageSize(self::SEARCH_TERM_LIMIT);

        return array_map(
            static fn (Query $query): string => (string) $query->getQueryText(),
            $collection->getItems()
        );
    }
}

6. 500 errors and pub/errors

While 404 pages are about missing routes, 500 errors happen when Magento itself is still running, but an unhandled exception occurs, for example a database timeout, a broken plugin, or a faulty third-party extension. Magento catches such errors through the reporting mechanism stored in pub/errors. By default there are two directories there, default and local, each with its own local.xml that determines which template gets rendered on failure, and whether debug information such as stack traces is shown.

For production environments, the production-grade error handler is activated via bin/magento or by copying local.xml into pub/errors/local/, which never leaks stack traces to visitors and instead shows a generic but branded message. Important for 404 and 500 error pages in the Hyvä theme: this template lives outside the regular layout and theme system, it renders before Magento's own bootstrap even runs, which means it cannot use a Tailwind build or the Hyvä block structure. A lean, self-contained HTML file with minimal inline CSS is enough here, containing nothing more than a logo, brand colors, and a link back to the homepage.

7. A fallback at the webserver level

The pub/errors mechanism described in section six assumes PHP can run at all. That assumption often breaks exactly when the server truly crashes: if the PHP-FPM pool is exhausted, has crashed, or was restarted mid-deployment, not a single phtml template can render anymore, because the PHP process behind it simply does not respond. A purely Magento-side approach to 404 and 500 error pages in the Hyvä theme is therefore not enough, a second, completely independent layer is needed directly in the webserver.

Nginx offers exactly that through error_page combined with an internal location that, on status codes 502, 503 and 504, meaning Bad Gateway, Service Unavailable and Gateway Timeout, serves a static HTML file straight from the filesystem without forwarding the request to PHP-FPM again. That file must be fully self-contained, with inline CSS and no dependency on external assets, because if the application server is down, the rest of the static content path is often not guaranteed to be reachable either. Anyone running Varnish in front configures the same logic in VCL as well, so requests that bypass the cache get the same static fallback.


# File: /etc/nginx/conf.d/mironsoft-error-fallback.conf
# Serves a fully static HTML page when PHP-FPM itself is unreachable,
# because a phtml template cannot render if the PHP process behind it is dead.

server {
    listen 443 ssl http2;
    server_name mironsoft.de;

    # ... existing ssl_certificate and root directives ...

    error_page 502 503 504 = @php_fpm_down;

    location @php_fpm_down {
        internal;
        root /var/www/html/pub/errors/static;
        rewrite ^ /500-static.html break;
    }

    location ~ \.php$ {
        try_files $uri =404;
        fastcgi_pass unix:/var/run/php-fpm/mironsoft.sock;
        fastcgi_intercept_errors on;
        fastcgi_read_timeout 60s;
        include fastcgi_params;
    }
}

8. CSP on error pages

A detail that gets overlooked far too often when teams start designing Hyvä error pages: the Content Security Policy behaves completely differently across the two error layers. The Hyvä 404 page from section three runs entirely inside Magento and therefore inside the Magento_Csp module. Every inline script block in that template still has to go through $hyvaCsp->registerInlineScript(), exactly like in any other Hyvä template, otherwise the browser blocks it anyway, even on an error page.

The static Nginx fallback page from section seven, on the other hand, lives entirely outside Magento. There is no CSP middleware there, no nonce generation, and no registerInlineScript() call, because there is no PHP code left running that could make one. For that page there are two viable approaches: either skip inline JavaScript entirely and ship plain HTML with inline CSS only, or set a fixed CSP via an extra add_header Content-Security-Policy directly in the Nginx location, with a statically computed hash for the one permitted inline block. The first option is the more robust one, because it offers no script-injection surface at all on the last line of defense for the shop.

9. 404 tracking and monitoring

A well-designed 404 page in the Hyvä theme treats the symptom but does not remove the cause: broken internal links, stale backlinks, or misconfigured redirects after a category restructuring. Without systematic tracking these sources stay invisible, the shop keeps bleeding traffic without anyone noticing where the 404 hits are actually coming from. A GA4 event fired on every rendered 404 template makes that gap visible and lets it be broken down by path and referrer in reports.

Technically, a small script listening for the custom event error-page:404 fired by the phtml template from section three is enough, writing a corresponding entry to window.dataLayer. Anyone who also wants server-side logging adds a simple logger call in the noroute controller with the requested URL and the referrer header, which allows analysis even when a visitor has JavaScript disabled or a consent banner blocks the tracking script.


// File: web/js/error-tracking.js
// Fires a GA4-compatible dataLayer event for every rendered 404 page,
// so broken internal links surface in analytics instead of staying invisible.
window.dataLayer = window.dataLayer || [];

document.addEventListener('error-page:404', (event) => {
    window.dataLayer.push({
        event: 'error_404',
        error_path: event.detail.path,
        error_referrer: document.referrer || '(direct)',
    });
});

Put together, a clear picture emerges of how Magento's default delivery and a deliberately engineered solution for 404 and 500 error pages in the Hyvä theme differ from each other.

Dimension Magento default Recommended Hyvä implementation Benefit
HTTP status on a broken URL often 200 OK (soft 404) explicit 404 via setHttpResponseCode() Correct signal to search engines
SEO impact crawl budget wasted on dead pages page correctly kept out of the index Better rankings, clean Search Console
User experience static CMS text, no suggestions search suggestions + popular categories Lower bounce rate
Behavior when PHP-FPM is down timeout or generic proxy error page static HTML fallback via Nginx Branded response even in a real outage
CSP compliance inline scripts often without a nonce registerInlineScript(), or no inline JS at all in the fallback No CSP violation, no attack surface

10. Summary

404 and 500 error pages in the Hyvä theme are not a pure design topic, they are an interplay of routing, HTTP semantics, and infrastructure resilience. Magento's noroute mechanism provides the foundation, a custom layout handle and a Hyvä-native template replace the generic CMS page with real Tailwind and Alpine.js structure. The correct HTTP status code prevents soft 404 problems, a ViewModel with search suggestions turns a dead end into a useful entry point, and pub/errors handles genuine 500 errors within the application.

The decisive, often forgotten piece is the layer beneath Magento: a static Nginx fallback kicks in exactly when PHP-FPM itself stops responding and not a single phtml file can render anymore. Combined with CSP-compliant script registration and a simple 404 tracking event, this adds up to a complete, production-ready solution for 404 and 500 error pages in the Hyvä theme, one that works reliably both for search engines and during a real outage.

404 and 500 Error Pages in the Hyvä Theme, the Essentials at a Glance

Noroute mechanism

A custom layout handle for cms/noroute/index instead of reshaping the CMS page, so a ViewModel and custom logic hook in cleanly.

Correct HTTP status

Set setHttpResponseCode(404) explicitly in the controller, otherwise a soft 404 wastes crawl budget.

Nginx fallback

Static HTML via error_page 502 503 504, because no phtml renders once PHP-FPM itself is down.

404 tracking

A GA4 event per 404 hit surfaces broken internal links and stale backlinks, and makes them measurable.

11. FAQ: 404 and 500 Error Pages in the Hyvä Theme

1Difference between 404 and 500 in the Hyvä theme?
404 happens on a missing route, the application keeps running. 500 happens on an unhandled exception inside the running application.
2Is design alone enough for the error page?
No, design alone solves neither the soft 404 problem nor the case where PHP-FPM itself is down and no template renders.
3What does web/default/no_route control?
Determines which CMS page is rendered on a missing route, read by Magento\Cms\Controller\Noroute\Index.
4When is a custom noroute controller worth it?
As soon as search suggestions, dynamic content, or custom logging are needed, via a custom layout handle for cms_noroute_index.
5What is a soft 404?
An error page that returns HTTP 200 instead of 404. Google treats it inconsistently and wastes crawl budget.
6How do I force the correct HTTP status?
Call setHttpResponseCode(404) explicitly in the controller, before returning the result. Verify with curl -I.
7What does pub/errors have to do with 500 errors?
Contains the reporting mechanism for unhandled exceptions, local.xml in pub/errors/local controls the production template.
8Why is pub/errors not enough if PHP-FPM is down?
Assumes PHP still runs. If PHP-FPM is down, no phtml renders anymore, a static Nginx fallback becomes mandatory.
9Does the fallback need to respect the Hyvä CSP?
The fallback runs outside Magento, without registerInlineScript(). Plain HTML with inline CSS and no inline JavaScript is the most robust choice.
10How do I track 404 hits?
A custom event from the template feeds window.dataLayer for GA4, plus optional server-side logging of path and referrer in the controller.

Mironsoft

Hyvä development, technical SEO and resilient Magento infrastructure

404 and 500 error pages in the Hyvä theme, built as engineering?

We build your noroute handler, the correct HTTP status code, a genuinely helpful Hyvä 404 template, and a static Nginx fallback for real outages, tested cleanly against your production CSP configuration.

SEO audit

Checking existing error pages for soft 404s and wrong HTTP status codes

Hyvä 404 template

Noroute controller, ViewModel with search suggestions and Alpine.js widget

Nginx fallback

Static HTML for the case where PHP-FPM itself stops responding