Skip to main content

Custom Routing with Weaverse

This guide explains how to implement custom URL structures in your Shopify Hydrogen storefront using Weaverse, allowing you to move beyond the standard /products/:handle, /collections/:handle, or /pages/:handle paths provided by default.

Overview

While Hydrogen and React Router 7 provide powerful routing capabilities, you might want URLs that are more SEO-friendly, aligned with marketing campaigns, localized, or simply structured differently from Shopify’s defaults. Examples include:
  • /sale/:campaignName (for a marketing landing page)
  • /fr/produits/:productHandle (for a localized product page)
  • /lookbooks/:collectionHandle (for a themed collection page)
  • /guides/:pageHandle (for content pages using Weaverse’s “Custom Page” feature)
Weaverse enables this by decoupling the URL structure from the content rendering mechanism.

Core Concept: loadPage Drives Content

The key principle is that the content Weaverse renders is determined by the parameters you pass to the context.weaverse.loadPage() function within your route module’s loader, specifically the type and handle arguments.
  • type: Tells Weaverse what kind of content to load (e.g., 'PRODUCT', 'COLLECTION', 'PAGE', 'CUSTOM'). See Rendering Weaverse Pages for a full list of types.
  • handle: Tells Weaverse which specific instance of that content type to load (e.g., the handle of a specific product or collection). CUSTOM pages are resolved from the request URL, so they need no handle.
Custom routing, therefore, involves:
  1. Creating a route module for your desired URL pattern.
  2. Registering that module in app/routes.ts — Pilot uses React Router 7’s manual route registry, so creating a file under app/routes/ does not register a route by itself.
  3. Implementing a loader in that module and extracting the identifier (like a handle) from the URL parameters.
  4. Calling context.weaverse.loadPage() with the explicit type corresponding to the content you want to display, plus the extracted handle when the type needs one.
Weaverse then fetches the appropriate page data based on these parameters, regardless of the actual URL structure.
Route modules are ordinary React Router 7 modules: import LoaderFunctionArgs, MetaFunction, and useLoaderData from react-router (not from @remix-run/react or @shopify/remix-oxygen), and return plain objects from loaders instead of json().

Relationship to Custom Pages & Templates

  • Custom Pages: Custom Routing is often used to provide user-friendly URLs for Custom Pages built in Weaverse. Load those with loadPage({ type: 'CUSTOM' }) — Weaverse resolves the page from the request URL. Reserve type: 'PAGE' with a handle for Shopify Page resources, the way Pilot’s app/routes/pages/regular-page.tsx serves /pages/:pageHandle.
  • Custom Templates: Custom Routing works seamlessly with Custom Templates. If a resource (like a product) fetched via a custom route has a custom template assigned, loadPage automatically uses it. You don’t need extra logic in your custom route’s loader for this.

Implementation Steps

Let’s walk through creating a custom route to display a product page at /item/:productHandle instead of the standard /products/:productHandle.

Step 1: Create the Route Module

Create a new module in your Hydrogen project at app/routes/items/item.tsx. The file path is just a module location — the URL comes from the registration in the next step.

Step 2: Register the Route in app/routes.ts

Pilot defines its routes explicitly with React Router 7’s route helpers, so a new file under app/routes/ is not reachable until you add it to the registry. Add your pattern inside the :locale? prefix block so the route inherits the storefront’s locale prefix:
  • :locale? — the optional locale prefix Pilot already applies to storefront routes.
  • item/:productHandle — your custom URL pattern, replacing products/:productHandle.
  • The second argument is the module path relative to app/.
React Router ranks a concrete pattern like item/:productHandle above the * catch-all, so placement inside the block does not matter — but keep it inside the :locale? prefix, otherwise /fr/item/... will fall through to the catch-all.

Step 3: Implement the Loader Function

Key Points in the Loader:
  • Explicit type: "PRODUCT": This is critical. It tells Weaverse to load the content configured for a product page, even though the URL is /item/....
  • Extract productHandle: Get the handle from params.
  • Fetch Shopify Data: You still need to fetch the corresponding Shopify product data for SEO, potentially for component data binding (though Weaverse handles much of this), and analytics. Use Promise.all for efficiency.
  • Nullable Weaverse data: loadPage() never rejects on a load failure — it logs and resolves to null. A try/catch around it catches nothing, so branch on the result (or hand it to validateWeaverseData). Keep try/catch for the operations that really do throw, such as storefront.query.
If your theme renders a fallback layout when Weaverse has no page for the resource, replace validateWeaverseData(weaverseData) with your own branch instead of throwing — but do check for null. Passing a missing page straight to <WeaverseContent /> renders the error component.

Step 4: Render the Weaverse Content

The component itself remains simple, primarily rendering <WeaverseContent /> which uses the weaverseData fetched in the loader.
Now, visiting /item/your-product-handle will render the Weaverse product page associated with your-product-handle.

More Examples

Localized Collection Route

  • Desired URL: /fr/collections-en-vedette/:collectionHandle
  • Route Module: app/routes/collections/featured-collection.tsx
  • Registration: route("collections-en-vedette/:collectionHandle", "routes/collections/featured-collection.tsx") inside the :locale? block
  • Loader loadPage Call: weaverse.loadPage({ type: 'COLLECTION', handle: params.collectionHandle })
  • Loader Shopify Data Fetch: Query for the specific collection data using params.collectionHandle.

Marketing Campaign Page (using Weaverse Custom Page)

  • Assumption: You built a “Custom Page” in Weaverse Studio with the handle summer-sale-2025.
  • Desired URL: /summer-sale-2025
  • Route Module: already covered by Pilot’s catch-all, app/routes/catch-all.tsx, registered as route("*", "routes/catch-all.tsx")
  • Loader loadPage Call: weaverse.loadPage({ type: 'CUSTOM' }) — the handle comes from the request URL, so no handle argument
  • Loader Shopify Data Fetch: Likely none needed unless the page displays specific dynamic products/collections queried separately.
Root-level custom pages need no new route at all: Pilot’s catch-all already resolves any unmatched URL as a CUSTOM page. Add a dedicated route only when you want a different URL shape (e.g. nesting every campaign under /promo/...), and load it with type: 'CUSTOM' too.

Custom Routing with Localization

Custom routes can be combined with Weaverse’s localization features to support multi-locale storefronts with custom URL structures.

Explicit Locale in Custom Routes

When implementing custom routes, you can explicitly pass the locale parameter to loadPage() to ensure the correct localized content is loaded:

Localized Custom URL Patterns

Create locale-specific custom routes with different URL structures per market:

Dynamic Locale Selection in Custom Routes

Combine custom routing with dynamic locale detection:

Multi-Project Custom Routing

For top-level domain or subdomain-based localization, combine custom routing with multi-project architecture:
Benefits:
  • Completely different URL structures per market (e.g., /vara/ for Swedish, /item/ for English)
  • Independent theme settings and page structures per project
  • Support for top-level domains (mystore.se, mystore.fr) without complex routing
See the Multi-Project Architecture Guide for comprehensive documentation on multi-project routing, including advanced routing patterns, A/B testing, and multi-brand storefronts.

Important Considerations

  • Register Every Route: A module under app/routes/ is unreachable until it appears in app/routes.ts. If a custom URL 404s or renders your catch-all page, check the registry first.
  • Choose the Correct type: Always ensure the type parameter in loadPage matches the kind of content you intend to display (PRODUCT, COLLECTION, PAGE, BLOG, ARTICLE, CUSTOM, etc.). Use CUSTOM for Weaverse custom pages and PAGE + handle for Shopify Pages. Refer to the Supported Page Types.
  • Fetch Necessary Shopify Data: If your custom route replaces a standard route (like product or collection), remember to fetch the corresponding Shopify data in your loader for SEO, analytics, or any components that might need it directly.
  • Generating Links: Linking to these custom routes from within your application (e.g., in navigation or product grids) will require using the correct path (/item/..., /fr/collections-en-vedette/...) instead of the default Shopify paths. You might need custom link-building logic or helper functions.
  • SEO & Sitemaps: Standard sitemap generation might not automatically include these custom routes. You may need to manually add them to your sitemap.xml logic. Ensure canonical URLs are correctly set (perhaps pointing to the standard Shopify URL if preferred, or self-referencing the custom URL if it’s the primary one). Consult SEO best practices.

This approach provides significant flexibility in structuring your Hydrogen storefront’s URLs while leveraging Weaverse for visual page building and content management.