Skip to content

fix(magento-store): redirect product URLs without /p/ prefix to product route#2581

Open
hnsr wants to merge 1 commit into
graphcommerce-org:canaryfrom
hnsr:fix/redirect-product-urls-without-prefix
Open

fix(magento-store): redirect product URLs without /p/ prefix to product route#2581
hnsr wants to merge 1 commit into
graphcommerce-org:canaryfrom
hnsr:fix/redirect-product-urls-without-prefix

Conversation

@hnsr
Copy link
Copy Markdown
Contributor

@hnsr hnsr commented Jan 19, 2026

Summary

When visiting a product URL without the configured product route prefix (e.g., /my-product instead of /p/my-product), the redirectOrNotFound function returns a 404 even though Magento's route resolver successfully identifies it as a product.

The Problem

The issue is in the redirect logic. When relative_url === from (which is the case when accessing /my-product), redirectUrl is undefined, and the code falls through to notFound even though:

  1. A valid route was found
  2. The route is identified as a product type

The Fix

Added a check after the existing redirect logic: if no explicit redirect URL exists but the route is identified as a product type, redirect to the product route anyway.

Test plan

  • Visit a product URL without the /p/ prefix (e.g., /my-product-slug)
  • Verify it redirects to /p/my-product-slug
  • Verify existing redirect behavior still works (URLs with suffixes, category redirects, etc.)

…ct route

When visiting a product URL without the configured product route prefix
(e.g., /my-product instead of /p/my-product), the redirectOrNotFound
function would return a 404 even though Magento's route resolver
successfully identified it as a product.

The issue occurs when relative_url equals the incoming URL - no redirect
was triggered because the code only redirected when relative_url !== from.
However, for products accessed without the /p/ prefix, we still need to
redirect to the product route even when the URL key matches.

This fix adds a check: if the route resolver finds a product but there's
no explicit redirect URL (relative_url === from), redirect to the product
route anyway.
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Jan 19, 2026

⚠️ No Changeset found

Latest commit: 9dca5a5

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@vercel
Copy link
Copy Markdown

vercel Bot commented Jan 19, 2026

@hnsr is attempting to deploy a commit to the Reach Digital Team on Vercel.

A member of the Team first needs to authorize it.

@bramvanderholst
Copy link
Copy Markdown
Contributor

We hit an infinite redirect loop with this patch in production. Posting the
repro in case it helps shape the final implementation.

Scenario

A configurable product is enabled in Magento, but all of its simple variants
are disabled. Magento keeps the URL rewrite for the configurable alive
(because the configurable itself is enabled), so urlResolver still
resolves the URL to a product. But the catalog products(filter: ...) query
returns no items — Magento filters out products with no purchasable
variants.

What happens

  1. User visits /p/<url_key> of such a product.
  2. pages/p/[url].tsx's ProductPage2 query returns total_count: 0, so
    product is undefined.
  3. It falls back to redirectOrNotFound(client, conf, params, locale),
    where params.url === '<url_key>'from === '<url_key>'.
  4. Magento's HandleRedirect query returns
    { route: { relative_url: '<url_key>', __typename: 'ConfigurableProduct', redirect_code: 0 } }.
  5. The patched branch matches (relative_url === from, route is a product)
    and redirects to /p/<url_key>.
  6. Browser follows → back to step 1 → loop.

Why it can't be fixed inside redirectOrNotFound

The function receives identical inputs in two semantically different
situations:

  • Catchall pages/[...url].tsx rendering /<url_key>: redirect to
    /p/<url_key> is correct (legacy URL → canonical product page).
  • Product page pages/p/[url].tsx rendering /p/<url_key>: redirect to
    /p/<url_key> is a self-loop.

Same params, same Magento response. Without knowing which route the
caller is rendering, the function can't tell the two apart.

Options we considered

  • Add a currentPath parameter so callers can opt in to a self-redirect
    check. Works, but every existing caller has to pass it to get the
    protection.
  • Move this branch out of the framework into the catchall, where it
    semantically belongs ("if a bare URL resolves to a product, send the
    user to its canonical /p/ location"). That's what we landed on in our
    fork — one extra HandleRedirect query in the catchall's 404 path, and
    pages/p/[url].tsx calls vanilla redirectOrNotFound, which 404s
    cleanly on unpurchasable products.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants