Magento GraphQL: How to resolve URL

Magento GraphQL: How to resolve URL

In the last article, I described the basics of Magento GraphQL. This time I want to show you a practical example of how to scaffold a headless app connected with Magento GraphQL and fetch appropriate data from Magento based on a given URL. This article is about:

  • concept/idea of how to create routing in a custom Magento headless project

  • which GraphQL is appropriate to resolve URLs

  • how to fetch information about categories, products, and CMS pages from API based on a given URL

  • how to implement the URL resolving functionality in the frontend app (in the big picture)

Magento GraphQL routing concept

Let me explain the idea of routing in a headless app from not the best pattern I have already seen. Typically in an eCommerce app, you have the following pages: home/cms pages, category page, product page, my account, checkout, etc.

So you can create some custom routes in headless app to handle all of them, for example:

  • Home - baseUrl/ - to handle homepage

  • Page - baseUrl/:pageId - to handle CMS pages

  • Category - baseUrl/category/:categoryId

  • Product - baseUrl/product/:productId

  • Checkout - baseUrl/checkout

  • My account - baseUrl/account

That solution works, but the huge disadvantage is that the URLs are not (user and SEO) friendly. In Magento Luma (Monolith version), you can visit pages like in following links:

The route query

Fortunately, Magento GraphQL supports URL resolving for the category, product, and CMS pages. There is the route query that receives the URL key and returns information about the entity that matches the given URL key or null if it's not possible to resolve the URL.

1query resolveRoute($url: String!) {
2  route(url: $url) {
3    redirect_code,
4    relative_url,
5    type
6    ... on SimpleProduct {
7      sku
8      url_key
9      uid
10      type
11      name
12    }
13    ... on CategoryTree {
14      name
15      product_count
16      uid
17    }
18    ... on CmsPage {
19      identifier
20      content_heading
21      content 
22    }
23  }
24}

Complete documentation about the route query: https://devdocs.magento.com/guides/v2.4/graphql/queries/route.html

The route query returns three fields:

  • redirect_code (Int) - Contains 0 when there is no redirect error. A value of 301 indicates the URL of the requested resource has been changed permanently, while a value of 302 indicates a temporary redirect.

  • relative_url (String) - The relative internal URL. If the specified URL is a redirect, the query returns the redirected URL, not the original

  • type (UrlRewriteEntityTypeEnum) - One of PRODUCT, CATEGORY, or CMS_PAGE

The route query has one obligatory parameter: "URL," which is a string.

Moreover, the route query is implemented by SimpleProduct, DownloadableProduct, BundleProduct, GroupedProduct, VirtualProduct, ConfigurableProduct, CategoryTree, and CmsPaage so that you can get information about those entities in the route query. In the example above, I used SimpleProduct, CmsPage, and CategoryTree.

Types of pages in Magento

Three types of URLs can be resolved using Magento GraphQL API and the route query.

CMS

CMS pages contain content like about us, homepage, privacy policy, etc.

Take a look at an example response for the About us page:

1// variables:
2{
3   "url": "about-us"
4}
5
6// results: 
7{
8  "data": {
9    "route": {
10      "redirect_code": 0,
11      "relative_url": "about-us",
12      "type": "CMS_PAGE",
13      "identifier": "about-us",
14      "content_heading": "About us",
15      "content": "<div class=\"about-info cms-content\">\n      <p class=\"cms-content-important\">With more than 230 stores spanning 43 states and growing, Luma is a nationally recognized active wear manufacturer and retailer. We’re passionate about active lifestyles – and it goes way beyond apparel.</p>\n\n      <p>At Luma, wellness is a way of life. We don’t believe age, gender or past actions define you, only your ambition and desire for wholeness... today.</p>\n\n      <p>We differentiate ourselves through a combination of unique designs and styles merged with unequaled standards of quality and authenticity. Our founders have deep roots in yoga and health communities and our selections serve amateur practitioners and professional athletes alike.</p>\n\n      <ul style=\"list-style: none; margin-top: 20px; padding: 0;\">\n          <li><a href=\"https://magento2demo.frodigo.com/contact/\">Contact Luma</a></li>\n          <li><a href=\"https://magento2demo.frodigo.com/customer-service/\">Customer Service</a></li>\n          <li><a href=\"https://magento2demo.frodigo.com/privacy-policy/\">Luma Privacy Policy</a></li>\n          <li><a href=\"https://magento2demo.frodigo.com/\">Shop Luma</a></li>\n      </ul>\n  </div>\n"
16    }
17  }
18}

The following query that received the "about-us" URL returns type equals CMS_PAGEand information about the CMS page that we wanted to fetch

Product

Product pages present information about products. Take a look at the example "Joust Duffle Bag" product that has the "joust-duffle-bag.html" URL key:

1// variables:
2{
3   "url": "joust-duffle-bag.html"
4}
5
6// results:
7{
8  "data": {
9    "route": {
10      "redirect_code": 0,
11      "relative_url": "joust-duffle-bag.html",
12      "type": "PRODUCT",
13      "sku": "24-MB01",
14      "url_key": "joust-duffle-bag",
15      "uid": "MQ==",
16      "name": "Joust Duffle Bag"
17    }
18  }
19}

The following query that received the "joust-duffle-bag.html" URL returns type equals PRODUCT and information about the product we wanted to fetch.

Category

Category pages display information about the category. Moreover, Magento has a pretty nice feature called Category Landing Pages that allows rendering CMS blocks (you can display any CMS content on the category page).

Take a look at examples for the women category.

1// variables:
2{
3  "url": "women.html"
4}
5
6// results:
7{
8  "data": {
9    "route": {
10      "redirect_code": 0,
11      "relative_url": "women.html",
12      "type": "CATEGORY",
13      "name": "Women",
14      "product_count": 0,
15      "uid": "MjA="
16    }
17  }
18}

When passing the 'women.html' URL, Magento returns the type equals CATEGORY and information about the category that we wanted to fetch

No route

When you pass an URL that doesn't exist in Magento, the route query will return null. Take a look at the example:

1// query: 
2query resolveRoute($url: String!) {
3  route(url: $url) {
4    redirect_code,
5    relative_url,
6    type
7  }
8}
9
10// variables:
11{
12  "url": "this-is-not-exist-i-am-sure"
13}
14
15// results: 
16{
17  "data": {
18    "route": null
19  }
20}

Once you know that the URL doesn't have corresponding information in the Magento backend, you can render the 404 page.

Other routes

Of course, there are more types of pages in eCommerce stores, like checkout pages or customer account pages, but the three types I mentioned before are crucial in terms of SEO and how they are rendered, and what URL is used.

Deprecated URL resolver query

Note there is the urlResolver GraphQL query that basically does the same as the route query, but it's deprecated and should not be used.

Sample implementation in a headless app

Today I want to show some pseudo-code and an idea in the big picture of how to implement URL resolving logic in a NextJS app.

Basically, for those three types of pages that can be resolved (category, product, and CMS), you can create a dynamic route in the pages directory: [[...slug]]. You can create separate routes for other pages like checkout, cart, and my-account. So your pages directory can look like this:

  • pages/ [[...slug]].tsx

  • pages/checkout.tsx

  • pages/cart.tsx

  • pages/account.tsx

That's it. All dynamic routes like category, product, and CMS will be handled by [[...slug]] route, and other pages will be operated by custom routes like the cart.tsx or account.tsx

Take a look at pseudo-code implementation for the [[...slug]] route:

1import React, { Suspense } from 'react'
2/** Route GraphQL Magento type: **/
3import type { Route } from '../types/magento'
4
5/**
6 * helper to fetch data on server side
7 * signature: function useData<Type>(key, fetcher): ApiData<Type>
8 * returns:
9 * interface ApiData<Type> {
10 *   data: Type,
11 *   error: Error
12 * }
13 */
14import useData from '../lib/use-data';
15/**
16 * Wrapper for GraphQL function that allows to query GraphQL API
17 * async <TYPE, VARIABLES>(query, variables = null): Promise<TYPE>
18 */
19import { query as apiQuery } from '../providers/Api';
20/**
21 * Route GraphQL queury:
22 */
23import routeQuery from "../queries/route.gql";
24import Loader from "../components/Loader"; // Loaded component
25import CmsPage from "../modules/cms/components/CmsPage/CmsPage.server"; // CMS Page component
26import ProductPage from "../modules/product/components/ProductPage/ProductPage.server"; // Product Page component
27import CategoryPage from "../modules/category/components/CategoryPage/CategoryPage.server"; // Category Page component
28
29type RouteQuery = {
30    route: Route
31}
32
33/**
34 * Enum that represent three possible types of pages
35 */
36enum PageTypes {
37    Product = 'PRODUCT',
38    Category = 'CATEGORY',
39    CmsPage = 'CMS_PAGE'
40}
41
42/**
43 * render page content based on given type and url key
44 */
45const renderPageContent = (route: Route) => {
46    const pages = {
47        [PageTypes.CmsPage]: <CmsPage data={route}/>,
48        [PageTypes.Product]: <ProductPage data={route}/>,
49        [PageTypes.Category]: <CategoryPage data={route}/>,
50
51    }
52
53    return pages[route.type]
54}
55
56export default function slug({ router }) {
57    const { query } = router;
58    const slug = query?.slug ? query?.slug?.join('/') : 'home'; // handle dynamic slugs
59
60    /**
61     * Fetch route information based on slug
62     */
63    const { data: { route }} = useData(
64        `route/${slug}`,
65        () => apiQuery<RouteQuery, { url: string }>(routeQuery, { url: slug }))
66
67    /**
68     * content to render:
69     */
70    const content = renderPageContent(route);
71
72    if (content === null) {
73        // redirect to 404 page
74    }
75
76    return <Suspense fallback={<Loader/>}>
77        {content}
78    </Suspense>
79}
80

I hope that the comments in the code are helpful. Besides explaining that code, I would say I use React Server Components (experimental). RSC allows the rendering of React components on the server-side. Because of that, components there has a `.server` prefix. Server components are game-changers in frontend development, and I can't wait where they will be released as stable features!

Moreover, I use TypeScript to that code for a better developer experience. The last thing that deserves to mention is the render page content function that receives the route data and renders an appropriate component.

It's possible to do the same using the switch statement, but I wanted to do that more declaratively.

In the GraphQL examples above, I showed you how to fetch product details, category data, and CMS pages. Components rendered by renderPageContent are perfect places to display that information. They receive the data as a prop, and then its responsibility is to say information on the screen.

Conclusion

This article showed you basic concepts about routing in headless apps connected with Magento GraphQL API.

There is the route query that resolves the URL and returns one of three page types: CMS_PAGE, Category, and Product. You can render those pages using one dynamic route on the front-end side.

You can create static routes for other pages like checkout or my account add implementation.

Thanks to state-of-the-art features like React Server Components, you can do all of that on the server-side, so the performance of a page will be outstanding.

If this article is interesting to you, let me know, and I will write more similar articles that show how to use Magento GraphQL API and create headless storefronts for Magento.

About the author

Marcin Kwiatkowski

Frontend developer, Certified Magento Full Stack Developer, writer, based in Poland. He has eight years of professional Software engineering experience. Privately, husband and father. He likes reading books, drawing, and walking through mountains.

Read more