A Centra-driven fashion storefront pairs automated category pages with Sanity editorial collections, all on Next.js Instant Navigations. Shoppers soft-navigate into a product listing page (PLP) and expect the grid frame to appear immediately, not a spinner that never resolves. Centra knows the nested category tree and the leaf URL segment per locale. Sanity knows hand-built collection pages merchandisers wrote on purpose. Both can emit paths. When those paths disagree, SEO and navigation both hurt.
CanonicalPath is my name for the Centra-owned leaf: the lowercased last segment of the nested category URI for that locale. Sanity keeps editorial slugs for curated collections. When the request path does not match CanonicalPath, the app 308s to the Centra leaf. Query strings are not a free-for-all. ?page=N when N is greater than 1 is the only query allowed on canonicals, hreflang alternates, and redirect targets. Everything else drops.
Soft-nav into a collection that already matched CanonicalPath and the shell sat on loading even though the segment was correct. That bug was sneakier than the redirect rules.
One owner per automated URL
Hreflang and sitemap URLs come from Centra translations per locale. I do not clone one English href across languages and hope Google figures it out. Each locale gets its own leaf from Centra's tree. Sanity URLs stay in the editorial lane. If merchandising wants a campaign landing page, that is a Sanity slug. If the PLP is the category tree, Centra wins.
I tried letting Sanity own the canonical slug. Editors already type slugs, so it felt cheaper than teaching the app about Centra's tree. Centra still owns nested categories per locale, including the leaf. Two owners shipped two URLs for the same grid, and the 308 never quite matched hreflang.
Instant Nav waited on an empty query
The PLP route always awaited searchParams. That made sense for pagination: you need ?page= before you render the grid. It also meant the static shell could not paint on a soft navigation when the path already matched CanonicalPath. Collection pages looked stuck on loading after Instant Nav even though the segment was correct. The shell waited for search params that were empty.
Always awaiting searchParams feels safe. Next.js makes the promise cheap in your head. Soft nav still cannot paint the collection frame until that promise resolves, and on a matching path there is nothing in the query to wait for. Never awaiting is worse. A mismatched slug with ?page=2 would 308 and drop pagination.
Split the await on path match
The fix splits on path match. When the incoming path already equals CanonicalPath, skip awaiting searchParams for the shell. Paint the collection frame immediately. On mismatch, still await so ?page= survives the 308. Redirect logic and Instant Nav shell logic share the same ownership rules but different await boundaries.
const pathMatchesCanonical = requestPath === canonicalPathForLocale;
const shell = pathMatchesCanonical ? (
<CollectionShell locale={locale} category={category} />
) : (
<CollectionShellWithSearchParams locale={locale} category={category} />
);Pagination inside the shell can still read ?page= where it matters. The outer route stops blocking the whole tree on an empty query object during soft nav.
You cannot skip searchParams on every PLP. Mismatch still needs the await so page 2 survives the redirect. Editorial Sanity paths stay editorial. They do not go through this 308. Automated commerce paths stay Centra-shaped.
Wrong slug, 308 to Centra's leaf, allowlisted query only. Matching slug, shell without the searchParams await. Instant Nav stops feeling like a loading screensaver.
Happy coding! Sander