Skip to main content
Version: 2.x

Handle dynamic URLs with the Dispatcher

In some cases, you will need more control over URL formats. For instance, you may prefer to have "/my-product" instead of "/product/my-product-slug" for SEO reasons. That is what Front-Commerce's Dispatcher is responsible for and what we will cover in this documentation.

URLs formats introduced in the Add a new page guide are statically defined. Front-Commerce’s Dispatcher allows to handle dynamically defined URLs.

This page explains how it works and how to use it for your own dynamic contents.

What is the goal of the Dispatcher?

The dispatcher is actually a component within Front-Commerce that will be displayed in case no other route was found. Its goal will be to ask the server (by using the route root query in your GraphQL Schema) what kind of page is associated with the current URL and will display the page's component accordingly.

Below is a flowchart illustrating the URL resolution logic:

If you come from a Magento background, this is the concept behind URL Rewrites.

In Front-Commerce’s core integrations (such as Magento2), the association between a URL and a page is already implemented for entities like Products, Categories, CMS pages… But depending on your own site, you might need to add new ones.

To do so, you will need to proceed in three steps:

  • Register a new urlMatcher
  • Add GraphQL type to the dispatcher query
  • Add the mapping between the type returned by route and the page component that should be displayed

Register a new urlMatcher

Magento

If the url you are trying to add are managed by Magento, you don't need any of this. You should instead add a url rewrite directly in your backend, since the mechanism already exists in the Magento module of Front-Commerce.

To add a new urlMatcher to a module you need to register FrontCommerce/Core as a module dependency and then use registerUrlMatcher to add a custom urlMatcher:

import typeDefs from "./schema.gql";
import myCustomUrlMatcher from "./urlMatcher.js";
import MyCustomLoader from "./loader.js";
import { makeUserClientFromRequest } from "server/modules/magento2/core/factories";

const moduleDefinition = {
namespace: "MyCustomModule",
dependencies: ["Front-Commerce/Core"],
typeDefs,
contextEnhancer: ({ req, loaders }) => {
const axiosInstance = makeUserClientFromRequest(req);
const CustomLoader = MyCustomLoader(axiosInstance);
loaders.Page.registerUrlMatcher(myCustomUrlMatcher(CustomLoader));
return {
MyCustomLoader: CustomLoader,
};
},
};

export default moduleDefinition;
urlMatcher.js
const myCustomUrlMatcher = (loader) => async (url, match) => {
if (match.__typename) {
// a match has already been loaded (you can still override it or return it as is, like we do here)
return match;
}
const myMatch = await loader.loadByUrl(url);
if (!myMatch) {
return match; // return original match if none is found, (⚠️ this is required!)
}
return {
// return your custom routable entity
path: myMatch.canonical_url || url,
__typename: "MyCustomType", // the GraphQL type of your custom type (should be added in schema.gql),
...myMatch, // or some adaptation of it. this will be available to your component defined bellow on a prop called `matched`
};
};

export default myCustomUrlMatcher;
schema.gql
MyCustomType implements Routable {
path: String! # needed for Routable
title: String! # example field
... # define own schema
}
loader.js
const MyCustomLoader = (axiosInstance) => {
return {
loadByUrl: (url) => {
return (
// example: get data by issuing an external api call
axiosInstance
.get(`/my/api/my-custom-entity?url=${encodeURIComponent(url)}`)
.then((response) => response.data)
.catch((err) =>
err.response?.status === 404 ? null : Promise.reject(err)
)
);
},
};
};
note

See Loaders documentation to learn how to instantiate your loader and add it to your GraphQL context.

Once this is done, you should be able to test that everything works as expected by using the GraphQL Playground at http://localhost:4000/playground and executing the following query:

{
route(url: "some-dynamic-custom-url") {
path
... on MyCustomType {
title # example
}
}
}

Add GraphQL type to the dispatcher query

Override DispatcherQuery if you have not already done so. You can find it under src/web/theme/modules/Router/DispatcherQuery.gql.

Add an inline fragment to the DispatcherQuery for your newly created GraphQL type:

my-module/web/theme/modules/Router/DispatcherQuery.gql
#import "theme/pages/Product/ProductFragment.gql"
#import "theme/pages/Category/CategoryFragment.gql"
#import "theme/pages/CmsPage/CmsPageFragment.gql"
#import "theme/pages/Product/ProductCacheControlFragment.gql"
#import "theme/pages/Category/CategoryCacheControlFragment.gql"
#import "theme/pages/CmsPage/CmsPageCacheControlFragment.gql"
#import 'path/to/your/fragment/MyCustomTypeFragment.gql'
#import 'path/to/your/fragment/MyCustomTypeCacheControlFragment.gql'

query MatchUrls($url: String!, $params: QueryInput) {
route(url: $url) {
path
__typename
... on RedirectEntity {
redirectTo
redirectType
}
... on Product {
...ProductFragment
...ProductCacheControlFragment
}
... on Category {
...CategoryFragment
...CategoryCacheControlFragment
}
... on CmsPage {
...CmsPageFragment
...CmsPageCacheControlFragment
}
... on MyCustomType {
...MyCustomTypeFragment
...MyCustomTypeCacheControlFragment # optional check note below for link
}
}
}
path/to/your/fragment/MyCustomTypeFragment.gql
fragment MyCustomTypeFragment on MyCustomType {
title
# more fields go here
}
note

For more info related to setting up cache control check the cache control and CDN documentation

Add the mapping between the __typename and the page component

Once your server is correctly configured, you need to map the __typename of your GraphQL datatype to a custom component.

To do so, you need to create the my-module/web/moduleRoutes.js file in your module that will contain the mapping. You might have already created it if you followed the Add a new page guide. But instead of using the default export, you will need to export a named object dispatchedRoutes.

This object has __typenames as keys (in our case MyCustomType), and a render function as values that will tell the application what to render for a specific key (in our case, it renders MyCustomPage). This will give you something like this:

my-module/web/moduleRoutes.js
import React from "react";
import MyCustomPage from "theme/pages/MyCustomPage";

export const dispatchedRoutes = {
MyCustomType: (props) => <MyCustomPage dataPropName={props.matched} />,
};

// you can still export your static routes here
// export default () => [
// <Route ... />
// ];

In the props passed to a render function (L6), you will have access to a matched property which is the object returned by your matchUrl function.

Advanced queries

If your route contains parameters for the GraphQL query (e.g: ?page=2), you should declare a variables property in your custom dispatchedRoutes component.

This variables function will be called by Front-Commerce when configuring the GraphQL query. It allows you to provide custom variables for the MatchUrls GraphQL query defined in DispatcherQuery.gql.

Here is an example of a custom type providing a $page: Int variable to the MatchUrls query:

my-module/web/moduleRoutes.js
import React from "react";
import MyCustomPage from "theme/pages/MyCustomPage";

export const dispatchedRoutes = {
MyCustomPaginatedType: {
render: (props) => <MyCustomPage dataPropName={props.matched} />,
variables: (params) => {
const searchParams = new URLSearchParams(params.location.search);
return {
page: searchParams.get("page"),
};
},
},
};

Restart the application

Once you've created your file, you can refresh your application (npm run start), and you should see your new route if you go to the /some-dynamic-custom-url URL. It will display MyCustomPage component.

And that is it! 🎉 From now on, any URL that is matched by the url matcher, will now be displayed with the render function defined in your dispatchedRoutes export under the matching key. Otherwise, a 404 page will still be displayed to the user.