Managing URL Rewrites in Magento 2: Redirects, SEO Slugs, Bulk Maintenance
AI generated
M2
di.xml
Magento 2 · URL Rewrites · SEO · Redirects
Managing URL Rewrites in Magento 2
Redirects, SEO slugs and bulk maintenance without data loss

The url_rewrite table in Magento 2 connects every SEO-friendly URL to its internal target, and it keeps growing with every category change, every product move and every attribute edit. Anyone who maintains URL rewrites only through the admin interface quickly loses track once there are several thousand entries, risking redirect chains and 404 errors. This article shows how to generate URL rewrites programmatically via Service Contracts, maintain them in bulk via CSV import, and monitor them long term.

18 min read url_rewrite · UrlPersistInterface · CSV import · Redirect types Magento 2.4.8 · PHP 8.4 · Hyva

1. Context: what the url_rewrite table actually stores

The url_rewrite table is the backbone of every SEO-friendly URL in Magento 2. It bridges what a visitor sees in the address bar, for example women/dresses/summer-dress-blue.html, and the internal controller path Magento actually executes, for example catalog/product/view/id/482. Without this table, every product and category page would only be reachable under cryptic, technical paths, which would be unusable for both search engines and customers. URL rewrites are therefore not a side detail, they are a central part of the storefront architecture.

The crucial point that many projects underestimate: the table is not a static export generated once and then left alone. It is continuously written to by indexers, admin actions and custom code. Every category rename, every move of a product into a different category, every change to the URL key attribute creates new entries and can invalidate old ones. Anyone who treats URL rewrites as a one-time setup topic overlooks that maintaining them is an ongoing process that has to be actively managed across the entire lifecycle of a shop, much like DNS records in a growing infrastructure.

This ongoing maintenance is not limited to catalog entities. CMS pages, custom redirects from marketing campaigns, and manually created redirects all land in the same table and follow the same rules. A clean understanding of the structure is therefore the prerequisite for everything solved programmatically, via bulk import, or via monitoring scripts in the following sections.

2. Table structure: request_path, target_path, redirect_type, entity_type, store_id

The most important columns of the table are quickly named, but their interplay is the real core. request_path is the incoming URL without a leading slash, as called in the browser. target_path is the internal path Magento redirects to or renders directly. redirect_type distinguishes between a direct rewrite without a visible redirect (value 0) and an actual HTTP redirect with 301 or 302. entity_type indicates what kind of object the entry belongs to, typically product, category, cms-page or custom for manually created URL rewrites. store_id, finally, makes every entry store-specific, since the same product path can carry different URL keys in different store views.

Internally, Magento does not distinguish catalog from CMS rewrites through a separate table, only through the entity_type field combined with entity_id. For category rewrites, the metadata column additionally comes into play, storing a serialized array with the category_id path needed to resolve nested category trees. The is_autogenerated column marks whether an entry was generated by an indexer or created manually, an important distinction for cleanup and monitoring scripts, as shown later in the monitoring section.


-- Typical url_rewrite query: catalog and CMS entries mixed together
SELECT
    url_rewrite_id,
    entity_type,
    entity_id,
    request_path,
    target_path,
    redirect_type,
    store_id,
    is_autogenerated,
    description
FROM url_rewrite
WHERE store_id = 1
  AND entity_type IN ('product', 'category', 'cms-page', 'custom')
ORDER BY request_path
LIMIT 20;

-- redirect_type: 0 = direct rewrite without redirect, 301 = permanent, 302 = temporary
-- is_autogenerated: 1 = generated by a Magento indexer, 0 = manually created entry

3. Programmatic rewrites via Service Contracts

Direct INSERT or UPDATE statements against url_rewrite are a common anti-pattern, because they bypass Magento's caching, event dispatching and validation. The correct way to build programmatic URL rewrites is through UrlPersistInterface. This Service Contract provides the replace() method, which accepts an array of UrlRewrite objects, detects duplicates and reports conflicts via a UrlAlreadyExistsException instead of silently overwriting them. The objects themselves are not instantiated with new, but created through UrlRewriteFactory, matching Magento's object creation conventions and greatly simplifying testing with mocks.

In PHP 8.4, such a service can be written cleanly with constructor property promotion. Instead of assigning dependencies individually inside the constructor body, UrlPersistInterface and UrlRewriteFactory are declared directly as private readonly properties. This reduces boilerplate while making the class strictly typed and immutable after construction, exactly the right semantics for a service that only accepts and persists redirect definitions.

It is important to clearly separate responsibilities: the service class only knows the domain a redirect originates from, for example a marketing campaign or a URL migration, and delegates the actual persistence entirely to the framework. This keeps URL rewrites generated programmatically consistent with those coming from the admin grid and the indexers.


<?php

declare(strict_types=1);

namespace Mironsoft\SeoSuite\Service;

use Magento\UrlRewrite\Model\UrlRewriteFactory;
use Magento\UrlRewrite\Model\OptionProvider;
use Magento\UrlRewrite\Service\V1\UrlPersistInterface;
use Magento\Framework\Exception\UrlAlreadyExistsException;

/**
 * Creates a single custom redirect via the UrlPersistInterface service contract.
 */
class CustomRedirectCreator
{
    /**
     * @param UrlPersistInterface $urlPersist Persists url_rewrite entries through the service contract.
     * @param UrlRewriteFactory $urlRewriteFactory Builds UrlRewrite value objects.
     */
    public function __construct(
        private readonly UrlPersistInterface $urlPersist,
        private readonly UrlRewriteFactory $urlRewriteFactory,
    ) {
    }

    /**
     * Creates or replaces a permanent redirect between two request paths.
     *
     * @param string $requestPath Old, SEO relevant request path without leading slash.
     * @param string $targetPath New target path the request should redirect to.
     * @param int $storeId Store view the redirect applies to.
     * @return void
     * @throws UrlAlreadyExistsException
     */
    public function createPermanentRedirect(string $requestPath, string $targetPath, int $storeId): void
    {
        $urlRewrite = $this->urlRewriteFactory->create()
            ->setEntityType('custom')
            ->setEntityId(0)
            ->setRequestPath($requestPath)
            ->setTargetPath($targetPath)
            ->setRedirectType(OptionProvider::PERMANENT)
            ->setStoreId($storeId)
            ->setDescription('Programmatic redirect via Service Contract')
            ->setIsAutogenerated(0);

        $this->urlPersist->replace([$urlRewrite]);
    }
}

4. Bulk maintenance: a CSV import module for redirects

Once more than a handful of redirects need to be maintained, for example after a domain migration or a category relaunch, the admin interface under Marketing becomes a bottleneck. The robust approach is a small dedicated module with a Symfony console command, registered via di.xml under Magento\Framework\Console\CommandListInterface, that translates a CSV file row by row into UrlRewrite objects. Instead of persisting every redirect individually, the command collects the objects in batches and passes them bundled to UrlPersistInterface::replace(), which drastically reduces the number of database round trips.

There is a trade-off with batch size: batches that are too large increase memory needs and the risk that a single faulty row fails the entire batch, batches that are too small lose the performance benefit of bundling. In practice, batch sizes between 100 and 500 rows have proven effective, depending on server hardware and whether the import runs during live operation or in a maintenance window. For URL rewrites coming from external systems such as a PIM or an old shop export, it is additionally worth validating the columns upfront, before any factory call happens at all.

The di.xml binding itself is unspectacular but crucial: without the entry in the CommandListInterface array, the command does not show up in bin/magento list. This binding belongs to the basic setup of any module that manages URL rewrites via CLI, and should be shipped from the start just like acl.xml and system.xml.


<?php

declare(strict_types=1);

namespace Mironsoft\SeoSuite\Console\Command;

use Magento\UrlRewrite\Model\OptionProvider;
use Magento\UrlRewrite\Model\UrlRewriteFactory;
use Magento\UrlRewrite\Service\V1\UrlPersistInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

/**
 * Imports a batch of URL rewrite redirects from a CSV file in a single persistence call per batch.
 */
class ImportRedirectsCommand extends Command
{
    private const BATCH_SIZE = 200;

    /**
     * @param UrlPersistInterface $urlPersist Persists batches of url_rewrite entries.
     * @param UrlRewriteFactory $urlRewriteFactory Builds UrlRewrite value objects per CSV row.
     * @param string|null $name Optional Symfony command name.
     */
    public function __construct(
        private readonly UrlPersistInterface $urlPersist,
        private readonly UrlRewriteFactory $urlRewriteFactory,
        ?string $name = null,
    ) {
        parent::__construct($name);
    }

    /**
     * Configures the command name, description and CSV path argument.
     *
     * @return void
     */
    protected function configure(): void
    {
        $this->setName('mironsoft:redirects:import')
            ->setDescription('Bulk imports URL rewrite redirects from a CSV file')
            ->addArgument('file', InputArgument::REQUIRED, 'Path to the redirects CSV file');
    }

    /**
     * Reads the CSV file and persists redirects in fixed size batches.
     *
     * @param InputInterface $input Console input, holds the file argument.
     * @param OutputInterface $output Console output for progress messages.
     * @return int Symfony exit code.
     */
    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $handle = fopen((string) $input->getArgument('file'), 'r');
        $batch = [];
        $imported = 0;

        while (($row = fgetcsv($handle)) !== false) {
            [$requestPath, $targetPath, $storeId, $type] = $row;

            $batch[] = $this->urlRewriteFactory->create()
                ->setEntityType('custom')
                ->setEntityId(0)
                ->setRequestPath($requestPath)
                ->setTargetPath($targetPath)
                ->setRedirectType($type === '302' ? OptionProvider::TEMPORARY : OptionProvider::PERMANENT)
                ->setStoreId((int) $storeId)
                ->setIsAutogenerated(0);

            if (count($batch) >= self::BATCH_SIZE) {
                $this->urlPersist->replace($batch);
                $imported += count($batch);
                $batch = [];
            }
        }

        if ($batch !== []) {
            $this->urlPersist->replace($batch);
            $imported += count($batch);
        }

        fclose($handle);
        $output->writeln(sprintf('<info>%d URL rewrite redirects imported.</info>', $imported));

        return Command::SUCCESS;
    }
}

5. Using redirect types correctly

The difference between a 301 and a 302 redirect for URL rewrites is not a formality, it has a direct impact on SEO rankings. A 301, represented in Magento through the constant OptionProvider::PERMANENT, signals to search engines that the old URL has been permanently replaced by the new one, and transfers most of the link equity to the new target. A 302, represented through OptionProvider::TEMPORARY, signals a temporary redirect, the search engine keeps the old URL in the index and keeps crawling it regularly. Anyone who mistakenly maps a permanent domain or category migration with a 302 gives away SEO value, because the ranking signal is not fully transferred.

A second, often overlooked problem is redirect chains: URL A redirects to URL B, which in turn redirects to URL C. Every additional hop costs load time, confuses crawlers, and increases the risk that a search engine gives up following the chain after a few hops and stops indexing the target page altogether. Chains typically arise when an old redirect is not updated after a further migration, but instead has a new redirect appended to it. The only sustainable solution is, on every new migration, to redirect existing URL rewrites pointing to the same path directly to the final target, instead of appending another hop.


<?php

declare(strict_types=1);

namespace Mironsoft\SeoSuite\Service;

use Magento\UrlRewrite\Model\OptionProvider;
use Magento\UrlRewrite\Model\ResourceModel\UrlRewriteCollectionFactory;

/**
 * Detects redirect chains by following target_path recursively.
 */
class RedirectChainDetector
{
    private const MAX_HOPS = 10;

    /**
     * @param UrlRewriteCollectionFactory $collectionFactory Loads url_rewrite rows by request_path.
     */
    public function __construct(
        private readonly UrlRewriteCollectionFactory $collectionFactory,
    ) {
    }

    /**
     * Follows a request path through successive redirects and returns the hop count.
     *
     * @param string $requestPath Starting request path to trace.
     * @param int $storeId Store view to search within.
     * @return int Number of hops until a non redirect target is reached.
     */
    public function countHops(string $requestPath, int $storeId): int
    {
        $currentPath = $requestPath;
        $hops = 0;

        while ($hops < self::MAX_HOPS) {
            $collection = $this->collectionFactory->create();
            $collection->addFieldToFilter('request_path', ['eq' => $currentPath]);
            $collection->addFieldToFilter('store_id', ['eq' => $storeId]);

            /** @var \Magento\UrlRewrite\Model\UrlRewrite|null $rewrite */
            $rewrite = $collection->getFirstItem();
            $redirectType = (int) $rewrite->getRedirectType();

            if ($rewrite->getEntityId() === null
                || !in_array($redirectType, [OptionProvider::PERMANENT, OptionProvider::TEMPORARY], true)
            ) {
                break;
            }

            $currentPath = (string) $rewrite->getTargetPath();
            $hops++;
        }

        return $hops;
    }
}

// OptionProvider::PERMANENT = 301, OptionProvider::TEMPORARY = 302, 0 = direct rewrite

6. Conflicts on category and attribute changes

When a category is renamed or a product is moved into a different category, Magento does not immediately generate new URL rewrites, it marks the affected entities as invalidated for the catalog_url_rewrite indexer. Only the next reindex, whether in Full or On Save mode, writes the new entries and, if enabled in the settings, automatically sets a redirect from the old to the new path. Anyone who does not reindex after bulk category changes is working with stale URL rewrites, which leads to discrepancies between what is visible in the frontend and what is actually in the database.

A key configuration option here is the strategy under Catalog > Search Engine Optimization, which controls whether a redirect is generated automatically on category and product renames or whether the old path is simply discarded with no_selection. With no_selection, the old entry disappears without replacement, which in production environments with existing organic traffic practically always leads to avoidable 404 errors. The recommended setting instead automatically generates a 301 redirect from the old to the new path, so external backlinks and search engine rankings are preserved.


#!/usr/bin/env bash
# Reindex catalog_url_rewrite after bulk category or attribute changes
bin/magento indexer:reindex catalog_url_rewrite_product
bin/magento indexer:reindex catalog_url_rewrite_category

# Inspect current indexer mode and status for all url_rewrite related indexers
bin/magento indexer:status | grep -i url_rewrite
bin/magento indexer:show-mode catalog_url_rewrite_product catalog_url_rewrite_category

# Switch to "Update by Schedule" to avoid long running saves in the admin
bin/magento indexer:set-mode schedule catalog_url_rewrite_product catalog_url_rewrite_category

7. SEO slug strategy and duplicate content

The quality of URL rewrites stands and falls with the URL key convention underlying a shop. Consistently lowercase slugs, separated with hyphens instead of underscores, free of stop words and without redundant category names in the product path, are the foundation for readable and crawlable URLs. A common mistake is embedding the full category path into every product URL key, which triggers a cascade of new URL rewrites on every category rename, even though nothing changed on the product itself.

The suffix, typically .html, is controlled centrally through the setting for product and category URL suffixes and should stay consistent project-wide. Changing the suffix while live creates a new row in url_rewrite for every affected entity and strictly requires automatic redirects, otherwise every previously indexed URL leads nowhere. Duplicate content in practice usually arises when a product is reachable via multiple category paths at once and Magento creates its own, technically valid rewrite for each path.

This is exactly where the canonical tag plays its role alongside URL rewrites: it signals to search engines which of several reachable URLs counts as the authoritative version, even when the url_rewrite table contains multiple valid paths to the same product. Canonical tags do not replace clean rewrites, they complement them, particularly when multiple category assignments have to remain for business reasons and consolidating the URLs is not practical.

8. Monitoring: 404 reports and orphaned rewrites

Without active monitoring, problems with URL rewrites stay undetected for a long time, because neither customers nor the team spontaneously report that a URL leads nowhere. The 404 report, which Magento provides via the built-in logging of the NoRouteHandler class or via external tools like Google Search Console, is the first and most important source. Recurring 404 paths with noticeable traffic are a strong signal of missing or faulty redirects and should be prioritized for a manual or automated redirect.

Orphaned rewrites are the counterpart to the 404 problem: entries in url_rewrite whose referenced entity_id was deleted long ago, for example because a product was disabled and removed but the associated custom redirect was never removed. Such leftovers accumulate over the years, unnecessarily bloat the table, and in extreme cases slow down URL resolution, because the underlying index grows larger than necessary. A periodic script that checks entity_id references against the respective catalog and CMS tables reliably uncovers such orphaned URL rewrites.

Redirect chains, as shown in the section on redirect types, can be detected programmatically by a script that recursively follows every request_path through target_path until a final target without further redirect is reached. Such a script should be a fixed part of every CI pipeline, or at least of a monthly maintenance run, since chains multiply on their own with every further migration if no one actively counteracts them.

9. Rewrite management approaches compared

The table below compares common approaches to maintaining URL rewrites against their respective risks and the recommended alternatives. The choice of approach has a direct impact on data consistency, SEO stability, and the shop's maintainability over multiple years.

Method Risk Recommended pattern Benefit
Direct SQL against url_rewrite Data inconsistency, missing validation UrlPersistInterface::replace() Validation and event dispatching are preserved
Manual admin UI maintenance Not scalable beyond a few hundred redirects CSV import module with batch persistence Consistent bulk processing within seconds
Renaming a category without a redirect 404 cascades, loss of link equity Enable automatic redirect generation SEO value and rankings are preserved
302 for a permanent migration No link equity transfer, repeated crawling OptionProvider::PERMANENT (301) Rankings stay stable
Never cleaning up rewrites Table growth, redirect chains Periodic monitoring script Lean, performant url_rewrite table

The table shows a recurring pattern: every short-term convenient shortcut with URL rewrites, whether SQL instead of a Service Contract or 302 instead of 301, creates long-term costs that are noticeably higher than the initial time saved. Anyone who consistently applies the recommended patterns across all modules and migrations reduces maintenance effort measurably.

10. Summary

Maintaining URL rewrites in Magento 2 is not a one-time project, it is a continuous process spanning the entire shop lifecycle. The url_rewrite table with its columns request_path, target_path, redirect_type, entity_type and store_id forms the technical foundation, while UrlPersistInterface and UrlRewriteFactory enable safe, programmatic access to it without bypassing Magento's internal validation and event mechanisms. For bulk changes, a dedicated CSV import module with a clean di.xml binding is the only approach that stays both performant and traceable.

Just as important as the technical implementation is consistently applying the right redirect types and active monitoring. 301 instead of 302 for permanent migrations, automatic redirect generation on category renames, and a periodic script that detects redirect chains and orphaned entries prevent technical debt from accumulating in the url_rewrite table over the years. Anyone who combines these building blocks retains full control over their URL rewrites even with large catalogs.

Managing URL rewrites in Magento 2: the essentials at a glance

Table structure

request_path, target_path, redirect_type, entity_type and store_id together represent every URL rewrite, whether catalog or CMS entity.

Service Contracts instead of SQL

UrlPersistInterface::replace() with UrlRewriteFactory is the only safe way to build programmatic redirects.

Redirect types

301 (OptionProvider::PERMANENT) for permanent migrations, 302 only for genuinely temporary redirects.

Monitoring & cleanup

Evaluate 404 reports, periodically track down and clean up orphaned entries and redirect chains via script.

11. FAQ: Managing URL Rewrites in Magento 2

1What exactly does the url_rewrite table store?
It connects request_path to target_path for catalogs, CMS pages and custom redirects, each store-specific via store_id.
2How does Magento distinguish catalog from CMS rewrites?
Via entity_type (product, category, cms-page, custom) combined with entity_id.
3Why not use direct SQL statements?
SQL bypasses validation, duplicate detection and event dispatching. UrlPersistInterface keeps all mechanisms consistent.
4What does UrlPersistInterface::replace() do?
Accepts UrlRewrite objects, checks for conflicts and persists them transactionally, instead of silently overwriting.
5How do you import redirects in bulk?
A dedicated module with a CLI command, reading a CSV, persisting in batches of 100 to 500 rows via UrlPersistInterface.
6301 or 302, which one when?
301 for permanent migrations, transfers link equity. 302 only for genuinely temporary redirects.
7How do redirect chains form?
When new redirects get appended to existing ones instead of pointing directly to the final target. They cost load time and crawl budget.
8Which indexer generates rewrites for categories?
catalog_url_rewrite_product and catalog_url_rewrite_category, depending on Full, On Save, or Update by Schedule mode.
9What to watch for in a URL key strategy?
Lowercase slugs with hyphens, a uniform .html suffix, no redundant category names, canonical tags as a complement.
10How do I find orphaned URL rewrites?
Evaluate 404 reports and run a periodic script matching entity_id against catalog and CMS tables, plus detect chains by following target_path.

Mironsoft

Magento 2 development, SEO and shop operations from a single source

Bring your shop's URL rewrites under control

From a redirect audit through building a CSV import module to ongoing monitoring: we make sure your URL rewrites stay SEO-safe through every catalog change.

Assessment

Complete analysis of the url_rewrite table, including chains and duplicates

Implementation

Service-Contract-based modules for bulk maintenance and migrations

Long-term operations

Recurring monitoring and cleanup as a fixed process