If the standard route does not meet your needs and you require more complex dynamic routes, you can make use of URL rewrites. To implement it, follow the sequential steps:

1 - Implement a Magento query to determine the type associated with a URL

In ScandiPWA, URL rewrites are determined by the Magento backend, which associates the URL with a specific type.

By default, a GraphQL request is sent to the urlResolver query. However, this tutorial will handle the default types with it and create a new query to handle the new routes.

This query should return everything needed to start rendering the component you want to use in the route. For instance, the urlResolver query returns the type, id, and sku when the type is PRODUCT.

Tutorial: How to implement a resolver using ResolverInterface

<aside> ℹ️ The tutorial utilizes a Magento module called ScandiPWA_UrlRewriteTutorial, located in the app/code/ScandiPWA/ScandiPWA_UrlRewriteTutorial directory.

</aside>

  1. Define the schema.graphqls file:

    The query should accept the path parameter, and according to it return the type

    <aside> ℹ️ This tutorial defines a tutorialUrlResolver query, but you can give it a different name and output.

    </aside>

    type Query {
        tutorialUrlResolver(path: String): TutorialUrlResolverOutput @resolver(class: "ScandiPWA\\\\UrlRewriteTutorial\\\\Model\\\\Resolver\\\\TutorialUrlRewriteResolver")
    }
    
    type TutorialUrlResolverOutput {
        type: String,
        id: Int
    }
    
  2. Create the resolver

    The schema file specifies that the TutorialUrlRewriteResolver is responsible for returning the type and id.

    The resolver below follows a simple rule for determining the type and ID. It checks whether the path begins with /rewrite/ and is followed by either dog or cat.

    You should customize it to return the correct values.

    <?php
    
    declare(strict_types=1);
    
    namespace ScandiPWA\\UrlRewriteTutorial\\Model\\Resolver;
    
    use Magento\\Framework\\GraphQl\\Config\\Element\\Field;
    use Magento\\Framework\\GraphQl\\Query\\ResolverInterface;
    use Magento\\Framework\\GraphQl\\Schema\\Type\\ResolveInfo;
    
    class TutorialUrlRewriteResolver implements ResolverInterface
    {
    
        public function resolve(
            Field $field,
            $context,
            ResolveInfo $info,
            array $value = null,
            array $args = null
        ) {
            $path = $args['path'];
            $routes = explode('/', $path);
    
            $isValidRewrite = count($routes) && $routes[0] == 'rewrite';
    
            if (!$isValidRewrite) {
                return null;
            }
            if ($routes[1] == 'dog') {
                return [
                    'type' => 'DOG_FAN',
                    'id' => 1
                ];
            } elseif ($routes[1] == 'cat') {
                return [
                    'type' => 'CAT_FAN',
                    'id' => 2
                ];
            }
    
            return null;
        }
    
    }
    

    To test the implementation, you can make a simple GraphQL request. See a request and response example below:

    query {
    	 tutorialUrlResolver(path: "rewrite/dog") {
    		id
    		type
    	}
    }
    
    {
    	"data": {
    		"tutorialUrlResolver": {
    			"id": 1,
    			"type": "DOG_FAN"
    		}
    	}
    }
    

2 - Create the ScandiPWA query file

ScandiPWA provides a pattern to avoid hardcoding GraphQL queries when making requests.

How to work with queries?

To define the query file to the tutorialUrlResolver query, create a file TutorialUrlResolver.query.js under src/query directory:

import { Field } from 'Util/Query';

/** @namespace Pwa/Query/TutorialUrlResolver/Query */
export class TutorialUrlResolverQuery {
    getQuery({ urlParam }) {
        return new Field('tutorialUrlResolver')
            .addArgument('path', 'String!', urlParam)
            .addFieldList(this.getTutorialUrlResolverFields());
    }

    getTutorialUrlResolverFields() {
        return [
            'id',
            'type',
        ];
    }
}

export default new TutorialUrlResolverQuery();

Note that:

3 - Update the UrlRewrites dispatcher to include the query file in the request

The UrlRewritesDispatcher class is responsible for making the request to the urlResolver.

How to Work with Redux?

Example of a request made on a category page:

{
    query: "query($url_1:String!){urlResolver(url:$url_1){sku,type,id,display_mode}}",
    variables: { 
        url_1: "men.html" 
    }
}

<aside> ℹ️ The query used in a request can be viewed in the developer tools console.

</aside>

You should extend the prepareRequest function of the UrlRewritesDispatcher dispatcher to also include the query file created.

Create a plugin file UrlRewrites.dispatcher.plugin.js in the src/plugin directory:

import TutorialUrlResolverQuery from '../query/TutorialUrlResolver.query';

const addTutorialUrlResolverQuery = (args, callback, instance) => {
    const [options] = args;

    return [
        ...callback(...args),
        TutorialUrlResolverQuery.getQuery(instance.processUrlOptions(options))
    ];
};

export default {
    'Store/UrlRewrites/Dispatcher': {
        'member-function': {
            prepareRequest: addTutorialUrlResolverQuery
        }
    }
}

Now the request includes the query:

{
    query: "query($url_1:String!,$path_1:String!){urlResolver(url:$url_1){sku,type,id,display_mode},tutorialUrlResolver(path:$path_1){id,type}}",
    variables: {
        url_1: "women.html", 
        path_1: "women.html" }
}

4 - Save the response data in the Redux Store

ScandiPWA uses the query when making the request, but it does not save the response data. The above state should be a valid one.

To store the response data you should also extend the onSuccess function of the UrlRewritesDispatcher to include the response data when dispatching the updateUrlRewrite action:

import TutorialUrlResolverQuery from '../query/TutorialUrlResolver.query';

const addTutorialUrlResolverQuery = (args, callback, instance) => {...}

const onSuccess = (args) => {
    const [{ tutorialUrlResolver, urlResolver }, dispatch, { urlParam }] = args;
    dispatch(
        updateUrlRewrite(
            tutorialUrlResolver || urlResolver || { notFound: true },
            urlParam
        )
    );
};

export default {
    'Store/UrlRewrites/Dispatcher': {
        'member-function': {
            prepareRequest: addTutorialUrlResolverQuery,
						onSuccess
        }
    }
}

The onSuccess plugin is overriding the original to include the tutorialUrlResolver response.

amUrlResolver || urlResolver || { notFound: true } will check the object from left to right, if amUrlResolver is valid, it will be used, if not will consider the urlResolver, and if not will use the { notFound: true }.

5 - Add a route to the urlRewrite component

Notice that there is nothing rendered when entering a valid rewrite URL. This is because there is no component associated with this type.

In ScandiPWA the renderContent function of the UrlRewrites component is responsible for rendering the component according to the type.

To add a new route, you should create a plugin file UrlRewrite.component.plugin.js located in the src/plugin directory:

import { lazy } from 'react';

//vv import the component you want to use
// always use the lazy import when defining new routes
export const TeamCat = lazy(() => import(
    /* webpackMode: "lazy", webpackChunkName: "teamCat" */ '../component/TeamCat'
));
export const TeamDog = lazy(() => import(
    /* webpackMode: "lazy", webpackChunkName: "teamCat" */ '../component/TeamDog'
));

export const TYPE_DOG = 'DOG_FAN';
export const TYPE_CAT = 'CAT_FAN';

const addTeamCatAndTeamDogToUrlRewrite = (args, callback, instance) => {
    const { props, type } = instance.props;
    const result = callback(...args);

    switch (type) {
    case TYPE_CAT:
        return <TeamCat { ...props } />;
    case TYPE_DOG:
        return <TeamDog { ...props } />;
    default:
        return result;
    }
};

export default {
    'Route/UrlRewrites/Component': {
        'member-function': {
            renderContent: addTeamCatAndTeamDogToUrlRewrite,
        },
    },
};

In this example, it’s creating the addTeamCatAndTeamDogToUrlRewrite function to plugin to the renderContent of the UrlRewrites component.

<aside> ℹ️ Note that the renderContent function must pass the props inside the component props, this happens because the UrlRewrites container passed the type and the props separately.

</aside>

This tutorial is making use of the TeamCat and TeamDog components just to illustrate.

TeamCat component files:

TeamDog component files:

6 - Update the UrlRewrite container to pass the correct props to the rendered component

After entering a valid rewrite URL, like /rewrite/dog, the component is loaded. But notice that the console is showing an error because the dogId or catId was defined as required in the component but is not passed.

This happens because the getTypeSpecificProps function of the UrlRewrites container is responsible for defining what properties related to the type will be passed to the UrlRewrites component. And it does not find any type match, and returns an empty object:

To allow it to pass the desired props you should extend it to match the newly created types. Create a file UrlRewrite.container.plugin.js located in the src/plugin directory:

import { TYPE_CAT, TYPE_DOG } from './UrlRewrite.component.plugin';

const getTypeSpecificProps = (args, callback, instance) => {
    //vvv define what you want to pass down
    const {
        urlRewrite: {
            id
        }
    } = instance.props;
    const result = callback(...args);

    switch (instance.getType()) {
    case TYPE_DOG:
        return { dogId: id };
    case TYPE_CAT:
        return { catId: id };
    default:
        return result;
    }
};

export default {
    'Route/UrlRewrites/Container': {
        'member-function': {
            getTypeSpecificProps,
        },
    },
};

Everything defined in the object returned by the getTypeSpecificProps will be added to the rendered component props. This example passes the dogId prop if the type is TYPE_DOG, and the catId prop id type is TYPE_CAT.

Now the error message is gone, and you can access the props in the component.

7 - Update Magento to return the correct status code

Similar to the default routes, if you check the response status code, it will show 404. Which is bad for SEO.( How to return the correct status code?)

<aside> ℹ️ This error message applies only in Magento mode

</aside>

To ensure Magento returns the correct status code, update the module where you defined the query to properly handle the path.

  1. Create a config file for the ValidationManager in the <MODULE>/etc/di.xml:

    <?xml version="1.0"?>
    <config xmlns:xsi="<http://www.w3.org/2001/XMLSchema-instance>" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
        <type name="ScandiPWA\\Router\\ValidationManager">
            <arguments>
                <argument name="validators" xsi:type="array">
                    <item name="rewrite" xsi:type="string">ScandiPWA\\UrlRewriteTutorial\\Validator\\RewriteTutorialValidator</item>
                </argument>
            </arguments>
        </type>
    </config>
    

    The name of the item should indicate the path to which the validator is to be applied. In this example, the rewrite path is being used. Any path that begins with rewrite will be processed by it. For instance, /rewrite/dog, /rewrite/cat/24123, and /rewrite/invalid are all handled by the validator.

    The content of the item, defines the validator location, in this example ScandiPWA\\UrlRewriteTutorial\\Validator\\RewriteTutorialValidator, which will be implemented in the next step.

  2. Implement the validator defined in the di.xml file:

    Create a file RewriteTutorialValidator.php in the <MODULE>\\Validator:

    <?php
    namespace ScandiPWA\\UrlRewriteTutorial\\Validator;
    
    use Magento\\Framework\\App\\RequestInterface;
    use ScandiPWA\\Router\\ValidatorInterface;
    
    class RewriteTutorialValidator implements ValidatorInterface
    {
        /**
         * Summary of validateRequest
         * @param \\Magento\\Framework\\App\\RequestInterface $request
         * @return bool
         */
        public function validateRequest(RequestInterface $request): bool
        {
            $path = $request->getPathInfo(); // "/rewrite/fasdfa"
            $paths = explode("/", $path); // ['', 'rewrite', 'fasdfa']
            if (sizeof($paths) > 2 and in_array($paths[2], ['dog', 'cat'])) {
                return true;
            }
            return false;
        }
    }
    

    The class must implement the ValidatorInterface, which requires the validateRequest function. Within this function, you should access the request object and return a boolean value indicating whether the request is valid or not.

    In this example, basic validation is performed to check if the first path is "rewrite" and the second path is either "dog" or "cat". However, in real scenarios, the validation process may be more complex.

    After entering a valid rewrite URL and checking the response status, the developer tools now show a 200 success.