How to add support for Category Landing Pages to PWA Studio

How to add support for Category Landing Pages to PWA Studio
Category Landing Page is a special type of Category Page. In Magento, Merchants are able to set special content for a specific category. Check how to implement this cool feature in PWA Studio.

A Category Landing Page is a special type of Category Page. In Magento, a Merchant can set unique content for a specific category. Take a look at this sample Magento Luma Category Landing Page:

Magento category landing page

How does it work

In the Magento category page configuration, there is a config field called Display Mode and there are three available options:

  • Products only
  • Static block only
  • Static block and products
how to enable category landing page in Magento 2

If you select the ‘Static block only’ option and set up a static block for the category, you will see your static block on the frontend for the Category, and this is precisely what the Category Landing Page means.

Does PWA Studio support Category Landing Pages?




Shocked man

Is it possible to add support for CLP in PWA Studio?

If you read my blog….

Definitely YES.

Happy man

Let’s do this!

Install PWA Studio

The very first thing that we need to do is create a new instance of PWA Studio.

$ yarn create @magento/pwa
$ cd <directory where PWA Studio has been installed
$ yarn buildpack create-custom-origin ./

Also, we need to have our own Magento 2 instance. This time I am using Magento 2.4.2 with sample data installed. If you don’t have your own local Magento 2 installation, I recommend using Mark Shust’s Magento Docker.

People often ask me:

What is the best way to install Magento in the local environment?

Then I tell them:

Use Mark’s Docker – it’s the best

thank you

The Idea

To achieve our goal, we are going to use the PWA Studio Extensibility framework. We will create an improvedCategoryContent component which will be a wrapper for CategoryContent PWA Studio’s component. We will add additional logic there and display Category Landing page content instead of an empty page.


Create theme

Before we start coding, let’s create a directory to keep all modifications related to the Category Landing Page.

First, create @theme/category folder in pwa_studio_root/src directory.

Second, let’s link this folder as a package in the package.json

dependencies": {
    "@magento/pwa-buildpack": "~9.0.0",
    "@theme": "link:src/@theme"

Add improvedCategoryContent component

Create file src / @theme / category / components / ImprovedCategoryContent / ImprovedCategoryContent.js with the following content:

import React from 'react';
import PropTypes from 'prop-types';

import CategoryContent from '@magento/venia-ui/lib/RootComponents/Category/categoryContent';
import LoadingIndicator from '@magento/venia-ui/lib/components/LoadingIndicator/indicator'
import { useImprovedCategoryContent } from '../../talons/useImprovedCategoryContent';
import CategoryLandingPage from '../CategoryLandingPage';

 * The ImprovedCategoryContent wraps CategoryContent @components and allows to display Category Landing Pages
 * @param {object} props
 * @param {number} props.categoryId - Category's ID
 * @param {object} props.classes - additional CSS classes that will be applied to the component
 * @param {object} - Category data
 * @param {object} props.pageControl - Pagination data
 * @param {number} props.pageSize - Page size
 * @param {object} props.sortProps - Sort props
const ImprovedCategoryContent = (props) => {
    const {
    } = props;

    const {
    } = useImprovedCategoryContent({categoryId});

    const categoryContent = isLandingPage ? <div>
        <CategoryLandingPage staticBlockId={staticBlockId}/>
    </div> : <CategoryContent

    const shouldDisplayContent = !isLoading ? categoryContent : <LoadingIndicator/>;

    return (
        <div {}>

ImprovedCategoryContent.propTypes = {
    categoryId: PropTypes.number.isRequired,
    classes: PropTypes.object,
    data: PropTypes.object,
    pageControl: PropTypes.object,
    pageSize: PropTypes.number,
    sortProps: PropTypes.array

export default ImprovedCategoryContent;

The purpose of this component is to check if the category page is a category landing page. All logic related to checking that is in useImprovedCategoryContent hook that we will create in the next step.

If the category is a landing page, the CategoryLandingPage component is rendered. Otherwise, native PWA Studio’s CategoryContent will be rendered.

Next, create the src / @theme / category / components / ImprovedCategoryContent / index.js file with following content:

export { default } from './ImprovedCategoryContent';

Add GET_CATEGORY_LANDING_PAGE query Before implementing the hook, let’s create a GraphQL query that will get information about category display mode and a Static Block ID used for a category’s content.

Create a src/@theme/category/queries.gql.js file:

import gql from 'graphql-tag'

export const GET_CATEGORY_LANDING_PAGE = gql`
    query category(
        $id: Int!
    ) {
        category(id: $id) {

The query is really straightforward. One important thing here is the ID field in results. Thanks to that ID, Apollo Client can merge these query results with other queries by ID.

Records are merged by ID, and if you have three queries for a category with the same ID, results will be stored in Apollo cache and available without querying to the server. How powerful is that?

Add useImprovedCategoryContent hook

Create a src / @theme / category / talons / useImprovedCategoryContent.js file:

import { useQuery } from '@apollo/client';
import { GET_CATEGORY_LANDING_PAGE } from '../queries.gql';

 * Returns props necessary to render the ImprovedCategoryContent @component.
 * @param {object} props
 * @param {number} props.categoryID
 * @returns {string} result.error - error message returns if something went wrong
 * @returns {bool} result.isLandingPage - flag determinates is a Category is in Statick Blocks only mode.
 *                                        This is true when display mode eauals 'PAGE'
 * @returns {bool} result.isLoading - flag determinates is data loading
 * @returns {number|null} result.staticBlockId - Static block ID set up for the Category Page or null.
export const useImprovedCategoryContent = props => {
    const {
    } = props;

    const { data, loading } = useQuery(GET_CATEGORY_LANDING_PAGE, {
        fetchPolicy: 'cache-and-network',
        nextFetchPolicy: 'cache-first',
        variables: {
            id: categoryId

    return {
        isLandingPage: data && data.category.display_mode === 'PAGE',
        isLoading: loading,
        staticBlockId: data ? data.category.landing_page : null,

Note: Because I want to keep this example simple, I do not handle errors here. If you would to take a look at how error handling can be done, check this article, please.

That hook receives one parameter – categoryId, and it gets data from the Magento backend using the already declared GET_CATEGORY_LANDING_PAGE query.

Hook returns three fields:

  • isLandingPage – this flag determines category is a landing page or not. Each category that has set up Display Mode equals Page is a landing page.
  • isLoading – determines state of data loading
  • staticBlockId – ID of static block set up for Category landing page.

Update local-intercept.js

It’s time to inject our component into Storefront. Take a look at the code below. If you are not familiar with PWA Studio extensibility framework, check this article.

File src/local-intercept.js:

const { Targetables } = require('@magento/pwa-buildpack')

module.exports = targets => {

    const targetables = Targetables.using(targets);

    const CategoryRootComponent = targetables.reactComponent(

    const ImprovedCategoryContent = CategoryRootComponent.addImport(
        "ImprovedCategoryContent from '@theme/category/components/ImprovedCategoryContent'"

    CategoryRootComponent.replaceJSX('<CategoryContent />', `<${ImprovedCategoryContent} />`)
        .setJSXProps(`ImprovedCategoryContent`, {
            'categoryId': '{id}',
            'classes': '{classes}',
            'data': '{categoryData}',
            'pageControl': '{pageControl}',
            'sortProps': '{sortProps}',
            'pageSize': '{pageSize}',

To insert the ImprovedCategoryContent component, we added import to the Category root component. We used the replaceJSX method to insert the component to the JSX (replace the native component with our new one). We passed all props from the native component to ours.

Add CategoryLandingPage component

First, create a file src / @theme / category / components / CategoryLandingPage / CategoryLandingPage.js with the following content:

import React from 'react';
import PropTypes from 'prop-types';
import PlainHtmlRenderer from '@magento/venia-ui/lib/components/RichContent';
import LoadingIndicator from '@magento/venia-ui/lib/components/LoadingIndicator/indicator';

import { useCategoryLandingPage } from '../../talons/useCategoryLandingPage';
import classes from './CategoryLandingPage.module.css';

 * The CategoryLandingPage @component displays CMS content for categories that have set up Display mode to Static block only.
 * @param {object} props
 * @param {string} props.staticBlockId - Static block's ID that provides content for the Page
const CategoryLandingPage = (props) => {
    const {
    } = props;

    const {
    } = useCategoryLandingPage({staticBlockId});

    const shouldDisplayContent = !isLoading ? <PlainHtmlRenderer html={content}/> : <LoadingIndicator/>;
    const shouldDisplayError = errorMessage ? <p>{errorMessage}</p> : null;

    return (
        <div className={classes.categoryLandingPage} {}>

CategoryLandingPage.propTypes = {
    staticBlockId: PropTypes.string.isRequired

export default CategoryLandingPage;

Keep in mind the component is rendered if a category has display mode equals Page set up. The component receives staticBlockId prop and uses it to get content for a specific category.

Content is rendered using PlainHtmlRenderer. If something went wrong, an error message is rendered.

Second, create a file src / @theme / category / components / CategoryLandingPage / index.js:

export { default } from './CategoryLandingPage';

Lastly, create a CSS module src /@theme / category / components / CategoryLandingPage / CategoryLandingPage.module.css

.categoryLandingPage {
    padding: 20px;

Add useCategoryLandingPage hook

The last thing needed to display the Category landing page’s content is the hook that collects content from Magento.

Create a file src / @theme / category / talons / useCategoryLandingPage.js:

import { useState, useEffect } from 'react';
import { useQuery } from '@apollo/client';
import { GET_CMS_BLOCKS } from '@magento/venia-ui/lib/components/CmsBlock/cmsBlock.js';

 * Returns props necessary to render the CategoryLandingPage @component.
 * @param {object} props
 * @param {number} props.staticBlockId - ID of a Static Block connected with the Category Landing Page
 * @returns {string} result.errorMessage - error message returns if something went wrong
 * @returns {bool} result.isLoading - flag determinates is data loading
 * @returns {string} result.content - HTML content of Static Block connected to the Category Landing Page
export const useCategoryLandingPage = props => {
    const {
    } = props;

    const [ content, setContent ] = useState(null);
    const [ errorMessage, setErrorMessage ] = useState(null);

    const { data, error, loading } = useQuery(GET_CMS_BLOCKS, {
        fetchPolicy: 'cache-and-network',
        nextFetchPolicy: 'cache-first',
        skip: !staticBlockId,
        variables: {
            identifiers: [ staticBlockId ]

    useEffect(() => {
        if (data && data.cmsBlocks && data.cmsBlocks.items) {

        if (!staticBlockId || error || data && data.cmsBlocks && data.cmsBlocks.items.length === ) {
            setErrorMessage('Unable to get category page content. Please try again later.')

    }, [data, staticBlockId, error])

    return {
        isLoading: loading,

The component returns three fields, and the most important for us is content, which contains the Category Landing page’s content!

Take a look at the results of our fantastic work!

After the modifications, I can see the working category landing page in PWA Studio!

As you can see, the Page works but there are missing styles. A new content renderer is probably needed here, and I think this is quite a good subject for the next article.


This time we added support for Category Landing Pages to PWA Studio Storefront. As you can see, the PWA Studio Extensibility framework is really powerful, and thanks to this we can easily extend PWA Studio with new features.

Source code

The source code for this tutorial is available on my Github.



Leave a Reply

Your email address will not be published. Required fields are marked *

Table of Contents

On Key

Related Posts

hexagonal architecture

Hexagonal architecture (ports and adapters): pros and cons

Today I will show you the hexagonal architecture, which is known as the ports and adapters pattern. You will learn the basics of this architectural pattern, as well as see its advantages and disadvantages. Additionally, I will show you cases where it is worth using this application design approach. The article discusses combining the hexagonal architecture with Domain Driven Design (DDD) modeling techniques.

serverless architecture

What do you need to know about serverless computing?

Serverless computing is a popular model in which cloud service providers take care of the server infrastructure and users just add their code. This allows users to focus on application development rather than infrastructure management. Serverless computing has many advantages, such as scalability, flexibility, low cost and rapid deployment. In this article you will learn more about serverless, how it works and what benefits it offers.

Charakterystyki architektury mikroserwisów

Microservices: pros and cons

Microservices is an approach to application development that is growing in popularity. They allow for easier application management, flexible changes and scaling. Each microservice is a separate component that can be designed according to business needs and meet user requirements more precisely. In this post, I will discuss the essential aspects of microservices architecture, the basics of the DDD approach, communication between microservices and the implications of using this approach.

refaktoryzacja kodu

Refactoring: when it is necessary and what problems it can cause

Refactoring is the process of improving the quality of source code without changing its functionality. In this article, you will learn how refactoring affects a project’s quality and stability and the techniques and signals of the need for refactoring. I will also present recommended books on refactoring.