Shopify Hydrogen redirects are not automatic. A Hydrogen storefront only honours the URL redirects a merchant creates in Shopify admin if server.ts calls storefrontRedirect after the app has returned a 404. The utility queries the Storefront API’s urlRedirects for the request path, sends a 301 to the target if one exists, and otherwise passes the 404 through untouched. The skeleton template wires this up for you; the matching rules are where people get caught.
This post is written against @shopify/hydrogen 2026.4.x, which pins Storefront API 2026-04, and the storefrontRedirect source as it stands in October 2026.
Where do Shopify Hydrogen redirects run?
In server.ts, after the request handler has produced a response, and only when that response is a 404. The skeleton template ships this block:
const response = await handleRequest(request);
if (hydrogenContext.session.isPending) {
response.headers.set(
'Set-Cookie',
await hydrogenContext.session.commit(),
);
}
if (response.status === 404) {
/**
* Check for redirects only when there's a 404 from the app.
* If the redirect doesn't exist, then `storefrontRedirect`
* will pass through the 404 response.
*/
return storefrontRedirect({
request,
response,
storefront: hydrogenContext.storefront,
});
}
return response;
The 404 itself comes from the catch-all route, app/routes/$.tsx, whose loader throws a Response with status 404 for any path nothing else matched. Delete that route and unmatched paths become React Router errors rather than 404s, and the redirect check never runs. Keep it.
Because the lookup happens after routing, a redirect only fires for paths your app does not handle. Online Store has the same rule, phrased differently: the help centre says “You can redirect only from broken URLs.” On a theme, Shopify decides what is broken. On Hydrogen, your route table does. If you have a products.$handle.tsx route that renders a “product not found” page with a 200, an admin redirect from /products/old-handle will never be consulted. Return a real 404 from loaders that find nothing.
What does storefrontRedirect actually match?
Reading the source answers questions the reference page leaves open.
It lowercases the pathname before matching, and strips any trailing slashes, because the admin does not allow a redirect path to end in one. It drops three query parameters from consideration entirely (redirect, return_to and _routes). Then, unless you pass matchQueryParams: true, it ignores the query string altogether and matches on the path alone. The lookup is a single Storefront API call:
query redirects($query: String) {
urlRedirects(first: 1, query: $query) {
edges {
node {
target
}
}
}
}
with $query set to path:/your/old/path. first: 1 means it takes whatever the API returns first; there is no sorting and no “most specific wins” logic. It is an exact path match, not a pattern. Shopify URL redirects are exact paths to begin with, so wildcards, prefixes and regular expressions have nothing to match against.
Two special cases sit in front of the lookup. A request for exactly /admin is redirected to /admin on the shop domain the client was configured with (storefront.getShopifyDomain()) before any query runs; pass noAdminRedirect: true if you would rather it 404. And if no admin redirect matches, the utility checks the original request for a return_to or redirect query parameter and follows it, provided the value resolves to the same origin. Cross-origin values are logged with “Cross-domain redirects are not supported” and ignored; the source comment explains this as a phishing guard.
The response is always a 301. There is no option for a 302 or 308, so do not use admin redirects for anything temporary. For a normal navigation that is a plain Location header. For a React Router soft navigation, where the browser requested /old-page.data rather than /old-page, the utility strips the .data suffix before matching and answers with a 204 carrying X-Remix-Redirect and X-Remix-Status: 301 so the client router performs the move. Either way, the query string the visitor arrived with (minus those three dropped parameters) is appended to the target unless matchQueryParams is on, so UTM parameters survive the hop.
If the Storefront API call throws, the error is logged as Failed to fetch redirects from Storefront API for route <path> and the original 404 goes back to the visitor. A redirect outage degrades to a not-found page, not a 500.
What does it cost on every 404?
One Storefront API query per 404 request. storefrontRedirect calls storefront.query without a cache option, so Hydrogen’s default strategy applies: public, maxAge: 1, then stale-while-revalidate. Repeated hits on one junk path share a cached answer; each distinct path is still a sub-request on its first hit. Our write-up on Hydrogen’s sub-request and full-page caching covers what that default strategy does and how to override it, should you ever need the lookup to hold for longer.
Moving the lookup into the catch-all route’s loader is tempting and covers less. A loader in $.tsx only runs for paths no other route claimed, so a products.$handle.tsx loader that throws a 404 for an unknown handle never reaches it. In server.ts, after handleRequest, any 404 from any route gets the check, and a matched redirect never renders anything.
How do the redirects get into Shopify in the first place?
Three ways, and all three land in the same list the Storefront API reads.
Merchants create them one at a time in admin, under Content > Menus > View URL redirects. When a product or collection handle changes, admin offers to create one for the old handle, and that redirect is just another row here, which is why a handle rename on a Hydrogen store does not strand the old URL as long as storefrontRedirect is in place.
Apps and migration scripts use the Admin GraphQL API with the write_online_store_navigation scope. urlRedirectCreate takes a UrlRedirectInput of path and target:
mutation UrlRedirectCreate($urlRedirect: UrlRedirectInput!) {
urlRedirectCreate(urlRedirect: $urlRedirect) {
urlRedirect {
id
path
target
}
userErrors {
field
message
}
}
}
For a replatforming, where the 301 map runs to thousands of rows, urlRedirectImportCreate takes the staged-upload URL of a CSV and returns an import object you then confirm with urlRedirectImportSubmit. It is the API version of the CSV import button in admin. Building the map itself, deciding what /product/foo.html becomes, is the hard part, and we covered that in the WooCommerce to Shopify 301 map; the Hydrogen side is only the last step of serving it.
The admin’s own constraints still apply to everything you load. Paths under /apps, /cart, /orders, /services and /shop are refused, as are the fixed paths /products, /collections and /collections/all. The help centre also warns that paths containing query strings “might not work as expected”, which on Hydrogen translates to: they will not match unless you turn matchQueryParams on, and then they must match exactly.
What would I do on a Hydrogen build?
Leave the skeleton’s storefrontRedirect block exactly where it is and make sure every loader that fails to find its product, collection, page or article throws a 404 Response rather than rendering an empty page. That one discipline is what makes admin redirects work at all.
Keep the redirect list in Shopify, not in a Hydrogen route file or an Oxygen-side map. Merchants can edit it without a deploy, handle renames feed it, and the migration tooling targets it. The exceptions are redirects that need a 302, prefix or pattern matching, or a cross-domain return_to; for those, handle the path in server.ts before handleRequest, with your own Response, because storefrontRedirect cannot express them.
Do not switch matchQueryParams on globally to catch one legacy URL with a query string. Every other redirect then becomes sensitive to tracking parameters, and /old-page?utm_source=newsletter stops matching /old-page. Add the one exact row, or handle that path yourself.
Whoooop builds Hydrogen storefronts and moves stores onto Shopify, which usually means we are the ones loading the redirect map and checking the old URLs still resolve after cutover. If you are planning a headless build or a replatforming, our Shopify development page explains how we work.