Chat on WhatsApp
Hyvä Theme 9 min read

How to Add a Custom Tab in the Customer Account Page in Magento 2 (Without the 404)

A custom My Account tab needs a route, a controller, and a sidebar link working together — wire up only one and you get a 404. This guide builds all three on Magento 2.4.4 — 2.4.9 (Luma + Hyvä) and explains exactly why the 404 happens.

How to Add a Custom Tab in the Customer Account Page in Magento 2 (Without the 404)

Adding a custom tab to the Magento 2 customer "My Account" page is one of the most common store customizations — a rewards balance, a downloads list, a B2B quote history, a warranty register. It is also one of the most common sources of a frustrating 404 Not Found, because the link in the sidebar and the page it points to are two separate things and people wire up only one of them. This guide builds the whole feature end to end on Magento Open Source 2.4.4 — 2.4.9, then explains exactly why the 404 happens and how to clear it.[1]

The four reasons a custom My Account tab 404s

Before the code, the diagnosis — because 90% of "my custom account tab goes to 404" tickets are one of these four:

  1. No routes.xml. Adding a link to the sidebar does not create a page. If the URL the link points at (e.g. /accounttab) has no matching frontName registered in etc/frontend/routes.xml, Magento has nothing to dispatch and returns 404.
  2. The controller does not implement an HTTP-method action interface. Since Magento 2.3, action classes must implement HttpGetActionInterface (or HttpPostActionInterface, etc.). A controller that only has an execute() method but implements none of these is treated as undispatchable — you get a 404, not an error.
  3. The frontName does not match the link path. If routes.xml declares frontName="rewards" but the sidebar link points to accounttab/index/index, the request resolves to a route that has no controller — 404.
  4. Stale generated/ or cache. Adding a brand-new route or controller requires setup:upgrade (or at minimum a config-cache flush, plus DI recompile in production mode). Until Magento re-reads the route list, the new frontName is invisible and every hit is a 404.

A fifth, softer failure: the page does resolve (HTTP 200) but renders with no My Account sidebar and no styling, which looks broken enough that people report it as a 404. That is the missing customer_account layout-handle update, covered below.

What we are building

A module Panth_AccountTab that adds a My Rewards tab to the account navigation, at the URL /accounttab, visible only to logged-in customers. Swap the names for your own feature. Final file tree:

app/code/Panth/AccountTab/
├── registration.php
├── etc/
│   ├── module.xml
│   └── frontend/
│       └── routes.xml
├── Controller/
│   └── Index/
│       └── Index.php
├── Block/
│   └── Rewards.php
└── view/
    └── frontend/
        ├── layout/
        │   ├── customer_account.xml          # adds the sidebar link
        │   └── accounttab_index_index.xml    # the page itself
        └── templates/
            └── rewards.phtml

Step 1 — Register the module

app/code/Panth/AccountTab/registration.php:

<?php
use Magento\Framework\Component\ComponentRegistrar;

ComponentRegistrar::register(
    ComponentRegistrar::MODULE,
    'Panth_AccountTab',
    __DIR__
);

app/code/Panth/AccountTab/etc/module.xml — sequence after Magento_Customer so its layout (the customer_account_navigation block) loads first:

<?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="Panth_AccountTab">
        <sequence>
            <module name="Magento_Customer"/>
            <module name="Magento_Theme"/>
        </sequence>
    </module>
</config>

Step 2 — Define the route (this is the 404 fix)

app/code/Panth/AccountTab/etc/frontend/routes.xml. The frontName is the first URL segment. Skip this file and every request to /accounttab is a 404, no matter how good your controller is.

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:App/etc/routes.xsd">
    <router id="standard">
        <route id="accounttab" frontName="accounttab">
            <module name="Panth_AccountTab"/>
        </route>
    </router>
</config>

The full URL pattern is frontName/controllerDir/action. With frontName="accounttab", the controller at Controller/Index/Index.php resolves to /accounttab/index/index — and Magento lets you shorten that to just /accounttab because index/index is the default.

Step 3 — The controller (login-gated, 2.4-correct)

app/code/Panth/AccountTab/Controller/Index/Index.php. Two non-negotiable details for 2.4.x: extend AbstractAccount (auto-redirects guests to the login page) and implement HttpGetActionInterface (without it the action is undispatchable → 404).

<?php
declare(strict_types=1);

namespace Panth\AccountTab\Controller\Index;

use Magento\Customer\Controller\AbstractAccount;
use Magento\Framework\App\Action\Context;
use Magento\Framework\App\Action\HttpGetActionInterface;
use Magento\Framework\View\Result\Page;
use Magento\Framework\View\Result\PageFactory;

class Index extends AbstractAccount implements HttpGetActionInterface
{
    public function __construct(
        Context $context,
        private readonly PageFactory $resultPageFactory
    ) {
        parent::__construct($context);
    }

    public function execute(): Page
    {
        $resultPage = $this->resultPageFactory->create();
        $resultPage->getConfig()->getTitle()->set(__('My Rewards'));

        return $resultPage;
    }
}

Because AbstractAccount forces the customer context, a guest who hits /accounttab is sent to /customer/account/login with a referer back to the tab — exactly the behaviour of every native My Account page. If you actually want a public page, implement HttpGetActionInterface on a plain \Magento\Framework\App\Action\Action instead — but then it is not an account tab.

app/code/Panth/AccountTab/view/frontend/layout/customer_account.xml. The customer_account.xml handle is merged onto every account page, so this is where Magento expects sidebar links. The block class is the SortLinkInterface — Magento_Customer already ships a DI preference mapping it to the concrete SortLink block, so you reference the interface directly.

<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <body>
        <referenceBlock name="customer_account_navigation">
            <block class="Magento\Customer\Block\Account\SortLinkInterface"
                   name="customer-account-navigation-rewards-link">
                <arguments>
                    <argument name="path" xsi:type="string">accounttab/index/index</argument>
                    <argument name="label" xsi:type="string" translate="true">My Rewards</argument>
                    <argument name="sortOrder" xsi:type="number">250</argument>
                </arguments>
            </block>
        </referenceBlock>
    </body>
</page>

The path argument must match the route from Step 2 — accounttab/index/index. A mismatch here is root cause #3. Higher sortOrder pushes the link further down the list; the native links sit between 100 ("Account Dashboard") and 400 ("Account Information"), so 250 lands it in the middle.

Step 5 — The page layout (this stops the "blank page" 404 lookalike)

app/code/Panth/AccountTab/view/frontend/layout/accounttab_index_index.xml. The filename is <frontName>_<controllerDir>_<action>.xml — get one segment wrong and Magento finds no layout for the page. The critical line is <update handle="customer_account"/>, which pulls in the My Account sidebar; without it the page resolves 200 but renders with no navigation and no account chrome.

<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      layout="2columns-left"
      xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <update handle="customer_account"/>
    <body>
        <referenceContainer name="content">
            <block class="Panth\AccountTab\Block\Rewards"
                   name="accounttab.rewards"
                   template="Panth_AccountTab::rewards.phtml"/>
        </referenceContainer>
    </body>
</page>

Step 6 — Block and template

app/code/Panth/AccountTab/Block/Rewards.php:

<?php
declare(strict_types=1);

namespace Panth\AccountTab\Block;

use Magento\Customer\Model\Session;
use Magento\Framework\View\Element\Template;
use Magento\Framework\View\Element\Template\Context;

class Rewards extends Template
{
    public function __construct(
        Context $context,
        private readonly Session $customerSession,
        array $data = []
    ) {
        parent::__construct($context, $data);
    }

    public function getCustomerName(): string
    {
        return (string) $this->customerSession->getCustomer()->getName();
    }
}

app/code/Panth/AccountTab/view/frontend/templates/rewards.phtml (Luma markup):

<?php
/** @var \Panth\AccountTab\Block\Rewards $block */
?>
<div class="block block-rewards">
    <div class="block-title">
        <strong><?= $block->escapeHtml(__('My Rewards')) ?></strong>
    </div>
    <div class="block-content">
        <p><?= $block->escapeHtml(__('Welcome back, %1.', $block->getCustomerName())) ?></p>
        <!-- Your reward balance, downloads, B2B quotes, etc. go here -->
    </div>
</div>

Step 7 — Enable, recompile, and verify

Developer mode:

bin/magento module:enable Panth_AccountTab
bin/magento setup:upgrade
bin/magento cache:flush

Production mode adds the compile + static deploy step (skip it and the new route stays a 404 — root cause #4):

bin/magento module:enable Panth_AccountTab
bin/magento setup:upgrade
bin/magento setup:di:compile
bin/magento setup:static-content:deploy -f
bin/magento cache:flush

Log in as a customer and open https://your-store.com/accounttab. You should see the My Rewards link highlighted in the left sidebar and your block in the content column. Log out and hit the same URL — you should bounce to the login page, not see the tab.

Hyvä note

Everything above is theme-agnostic — the route, controller, and customer_account.xml SortLink work identically under Hyvä, because Hyvä keeps Magento’s customer-account layout structure and block names. The only change is the template: replace the Luma block block-rewards BEM classes with Tailwind utilities so the page matches your Hyvä theme. The 404 causes and fixes are exactly the same.

FAQ

I added the sidebar link but clicking it 404s — what is the single most likely cause?

You are missing etc/frontend/routes.xml, or its frontName does not match the link’s path. The sidebar link is just a URL; it does not create a page. Add the route, flush the config cache, and the 404 clears.

The page loads but has no account sidebar and looks broken. Is that the same bug?

No — that is a 200, not a 404. It means your page layout file is missing the <update handle="customer_account"/> line, or the layout filename does not match <frontName>_<controllerDir>_<action>.xml. Add the handle update and the My Account chrome returns.

Do I really need HttpGetActionInterface?

Yes, on Magento 2.3 and up. A controller that implements none of the HTTP-method interfaces is not dispatched and you get a 404. Use HttpGetActionInterface for a normal page; add HttpPostActionInterface for a form handler.

How do I keep guests out of the tab?

Extend Magento\Customer\Controller\AbstractAccount. It enforces the customer context and redirects logged-out visitors to /customer/account/login automatically — no manual session check needed.

Where does the link’s position in the sidebar come from?

The sortOrder argument on the SortLink block. Native links run from 100 (Dashboard) to ~400 (Account Information); pick a number in the gap to slot your tab where you want it.

Why does it still 404 after I added everything correctly?

Stale state. New routes and controllers are cached. Run setup:upgrade and cache:flush in developer mode; in production mode also run setup:di:compile. Until Magento re-reads the route list, the frontName does not exist.

Citations

  1. Adobe Commerce Developer Documentation — "Routing", how routes.xml, frontName, and controller actions map to a URL. developer.adobe.com/commerce/php/development/components/routing
  2. Adobe Commerce Frontend Developer Guide — "Layout instructions", the customer_account handle and referenceBlock merge behaviour. developer.adobe.com/commerce/frontend-core/guide/layouts
Need a custom My Account tab — or a 404 fixed — this week?

I build account-area customizations on Magento 2.4.4 — 2.4.9 (Luma + Hyvä): rewards tabs, B2B quote history, downloads, warranty registers — with login gating, layout, and tests. Fixed quote from $499 audit · $2,499 sprint · ~8h @ $25/hr. See hire me or the Magento 2 development service.