Skip to main content
Version: next

Dynamic Routing

Since version 3.5

This guide explains how you can create dynamic routes for url rewrites in Front-Commerce.

What is dynamic routing?

Dynamic routes are generated based on the data available in your application, allowing for more meaningful and SEO-friendly URLs. For example, instead of a generic URL such as /product/sku-6, you can have a more descriptive URL like /acme-product.html.

Consider a Remix route for your products defined as routes/product.$id.tsx. By leveraging dynamic routes, you can replace generic product URLs with SEO-friendly alternatives.

Generic Route Example

  • URL: /product/sku-6

SEO-Friendly Route Example

  • URL: /acme-product.html

By implementing dynamic routes, you enhance the readability and search engine optimization (SEO) of your URLs, which can improve your site's visibility and user experience.

How to create dynamic routes?

Batching and Prioritizing URL Matchers

When multiple URL matchers are registered without any matcherOptions, it will default the batchOrder and priority to 0. This means that the order in which the URL matchers are registered will determine the order in which they are executed.

Let's say we have the following list of URL Matchers:

A → fast API (cached)
B → no API (static)
C → slow API (not cached)
D → no API (static)

We would ideally want this to first try to run B and D then A and finally C. To achieve this we can set the batchOrder and priority fields in the matcherOptions object.

extension/index.ts
services.DynamicRoutes.registerUrlMatcher("A", () => new UrlMatcherA(), {
batchOrder: 1,
priority: 1,
});

services.DynamicRoutes.registerUrlMatcher("B", () => new UrlMatcherB(), {
batchOrder: 0,
priority: 2,
});

services.DynamicRoutes.registerUrlMatcher("C", () => new UrlMatcherC(), {
batchOrder: 2, // only run if no other batches have matched
priority: 1,
});

services.DynamicRoutes.registerUrlMatcher("D", () => new UrlMatcherD(), {
batchOrder: 0,
priority: 1,
});

This would result in the following order of execution:

# batch 0
D → no API (static)
B → no API (static)

# batch 1
A → fast API (cached)

# batch 2 (only run if no other batches have matched)
C → slow API (not cached)

Extending the type declarations

For TypeScript support of the type field in your handle export, you can extend the DynamicRoutesCompositionList interface from the @front-commerce/types package to include your custom types.

Known Limitations

Matching Catch-All Routes (Splat Routes)

In Remix, you can create a catch-all route that matches any path, for example:

URLMatched Route
/foo/aroutes/foo.$.tsx
/bar/aroutes/bar.$.tsx

However, it's not possible to create a URL Matcher for catch-all routes because the URL will still be matched by Remix. Attempting to match /example to routes/foo.$.tsx using a URL matcher will result in the following error:

Error: Route "routes/foo.$" does not match URL "/example"

Dynamic Route Catch-All Placeholder

To implement CSR for dynamic routes, the application requires specific internal logic in the routes/_main.$.tsx file.

If you have overwritten this route, you will need to manually apply and maintain this logic to ensure proper functionality.

useMatches() returns different data in SSR and CSR

When using Remix's useMatches() hook on dynamic routes, the match object differs between

SSR

and CSR navigation.

  • During SSR, the match corresponds to the actual aliased route (e.g., routes/product.$id.tsx), so match.data and match.handle contain the expected values directly.
  • During CSR, the match corresponds to the routes/_main.$.tsx catch-all placeholder. This route's clientLoader wraps the actual data into a different structure:
    • match.data.loaderData contains the actual route's loader data
    • match.data.dynamicRouteModule contains the actual route module (including its handle, meta, links exports)
    • match.handle is undefined (the _main.$.tsx catch-all does not export a handle)

Workaround

To access data and handle properties consistently across both SSR and CSR, use optional chaining and the nullish coalescing operator to fall back to the CSR-specific path:

// Accessing loader data
const foo = match.data.foo ?? match.data.loaderData?.foo;

// Accessing handle properties (match.handle is undefined during CSR)
const bar = match.handle?.bar ?? match.data.dynamicRouteModule?.handle?.bar;

Example: reading loader data from a dynamic route

Consider a product route that returns the product name from its loader:

routes/product.$id.tsx
import { createHandle } from "@front-commerce/remix/handle";

export const handle = createHandle({
dynamicRoute: {
type: "product",
priority: 1,
},
});

export const loader = () => {
return { product: { name: "Acme product" } };
};

export default function Product() {
// ...
}

In a layout component, you can use useMatches() to read the product from the deepest matching route. You must account for the SSR/CSR difference:

components/Breadcrumb.tsx
import { useMatches } from "@remix-run/react";

function useCurrentProduct() {
const matches = useMatches();
const match = matches[matches.length - 1];

// SSR: data is available directly on the match
// CSR: data is wrapped, the actual loader data is in match.data.loaderData
return match.data?.product ?? match.data?.loaderData?.product;
}

export default function Breadcrumb() {
const product = useCurrentProduct();

if (!product) {
return null;
}

return <nav>Home / {product.name}</nav>;
}

HMR Reloads with Multiple Dynamic Route Tabs

When working in development mode with Hot Module Replacement (HMR), you might encounter issues when loading multiple dynamic routes in parallel tabs. This can cause the dynamic route manifest to become out of sync, resulting in pages not loading correctly.

Symptoms

Pages with dynamic routes stop working after HMR reloads

TypeError: Cannot read properties of undefined (reading 'module')

Solutions

You can resolve this issue by either:

  1. Restarting your development server
  2. Using client-side navigation to the affected pages, which will re-populate the manifest and restore functionality

Known issues

No routeModule available to create server routes

Internal Reference: 67201

Symptoms

In production, you might see the following error:

Error: No `routeModule` available to create server routes

Abnormal rate of 500 errors in your logs.

Cause

When a same URL is loaded in a small amount of time by the same Front-Commerce server process, a race condition occurs on the ServerBuild to be partially built to load Dynamic Routes by multiple threads.

Solution

We have implemented a new experimental way to load Dynamic Routes in memory to prevent the race condition, you can apply this patch by following the dedicated Merge Request patch.

This MR implements the use of a feature flag to enable the in-memory dynamic route matcher. However, when applying this patch to your project, we recommend you to replace the feature flag by a process.env.FRONT_COMMERCE_DYNAMIC_ROUTES_IN_MEMORY environment variable to enable it.

This way, you'll be able to enable and disable it quickly on your project without the need of a rebuild.