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.
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:
| URL | Matched Route |
|---|---|
/foo/a | routes/foo.$.tsx |
/bar/a | routes/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), somatch.dataandmatch.handlecontain the expected values directly. - During CSR, the match corresponds to the
routes/_main.$.tsxcatch-all placeholder. This route'sclientLoaderwraps the actual data into a different structure:match.data.loaderDatacontains the actual route's loader datamatch.data.dynamicRouteModulecontains the actual route module (including itshandle,meta,linksexports)match.handleisundefined(the_main.$.tsxcatch-all does not export ahandle)
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:
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:
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:
- Restarting your development server
- 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.