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:
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>
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
}
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"
}
}
}
ScandiPWA provides a pattern to avoid hardcoding GraphQL queries when making requests.
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:
Field
parameter should be the name of the query definedaddArgument
values should match the argument defined in the schemaaddFieldList
is using the getTutorialUrlResolverFields
function to return the fields defined in the schema.UrlRewrites
dispatcher to include the query file in the requestThe UrlRewritesDispatcher
class is responsible for making the request to the urlResolver
.
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" }
}
UrlRewritesReducer
state after entering a valid URL rewrite URLScandiPWA 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 }
.
UrlRewritesReducer
state after entering the same URLurlRewrite
componentNotice 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:
index.js
TeamCat.component.js
TeamCat.container.js
TeamCat.style.scss
TeamDog
component files:
index.js
TeamDog.component.js
TeamDog.container.js
TeamDog.style.scss
UrlRewrite
container to pass the correct props to the rendered componentAfter 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:
getTypeSpecificProps
functionTo 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.
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.
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.
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
.