How to use the SEO meta tag rules module for Page Designer in SFCC
Tags: commercecloud, salesforce, javascript, webdev
Published: 2022-11-05 15:21:00

## Table of contents
- [Introduction](#introduction)
- [The problem](#the-problem)
- [The solution](#the-solution)
- [Alternative solutions to SEO meta tag rules](#alternative-solutions-to-seo-meta-tag-rules)
- [Conclusion](#conclusion)
- [Additional Links](#additional-links)

## Introduction
We have a great feature available to us in Salesforce Commerce Cloud (SFCC) called [SEO meta tag rules](https://trailhead.salesforce.com/en/content/learn/modules/b2c-seo-meta-tags/b2c-seo-meta-tag-explore-rules). This is a module inside of SEO in Business Manager (BM).
However, currently there is **only support for [PDP/PLP pages made in page designer](https://documentation.b2c.commercecloud.salesforce.com/DOC1/topic/com.demandware.dochelp/content/b2c_commerce/topics/page_designer/b2c_use_page_meta_tag_rules_for_pd.html)** that support these meta tag rules.

In this blog post we'll see how to support SEO meta tag rules for **all pages made in page designer**.

## The problem

Before going any further, I want to mention some disclaimers. At the time of writing this, not all pages in page designer support this. This could be added to SFCC and be directly supported without custom development. I posted this idea as a [feature enhancement for Salesforce in IdeaExchange](https://ideas.salesforce.com/s/idea/a0B8W00000JJF4rUAH/support-all-page-designer-pages-for-seo-meta-tag-rules).

With that said, let's take a step back and understand the problem.

The SEO experts on a business operate mostly on the SEO meta tag rules BM module. If they need more granular level of SEO customization, most System Objects (e.g. Products, Content Assets, Categories) of SFCC support SEO fields like `pageUrl`, `pageTitle`, `pageDescription`. SEO experts can define rules that act as fallbacks, and then if some merchants don't define these fields for particular categories or pieces of content. You still have some meta tags being added to the page by these rules.

Now, even though this feature supports many types of pages, like home page, product pages or product listing pages. It doesn't support all types of Page Designer (PD) pages.
You can also take a look at the [Page class](https://documentation.b2c.commercecloud.salesforce.com/DOC1/index.jsp?topic=%2Fcom.demandware.dochelp%2FDWAPI%2Fscriptapi%2Fhtml%2Fapi%2Fclass_dw_experience_Page.html), and check that it doesn't have the `pageMetaTags` field like the [Content class](https://documentation.b2c.commercecloud.salesforce.com/DOC1/index.jsp?topic=%2Fcom.demandware.dochelp%2FDWAPI%2Fscriptapi%2Fhtml%2Fapi%2Fclass_dw_content_Content.html) does.

Page Designer pages are very similar to Content Assets, in the sense that underneath, [these pages are persisted as the same content objects on the SFCC's database](https://documentation.b2c.commercecloud.salesforce.com/DOC2/topic/com.demandware.dochelp/content/b2c_commerce/topics/page_designer/b2c_pg_comp_types_content_assets.htm). But there are still differences, the support for meta tag rules feature is one of them.

Ultimately, these differences makes it harder for merchants or SEO experts that want to leverage the same functionality available on the rest of the site.

Now that you have a better understanding about the problem, let's shift our focus into a solution.

## The solution

### Content Assets

The whole premise of this solution is that it's supported by the SEO meta tag rules module of BM. In this module we have support for the following:
- Homepage
- Product pages
- Content Detail pages
- Content Listing pages

The only one that fits well with a page designer page is **Content Detail page**. Meaning we'll create content assets as a way to support these SEO rules for Page Designer.
Each page can have it's corresponding content asset, where the content ID follows a naming convention like "<pageId>-seo". This way in `Page.js` you can get the meta tags for that page, through that content asset. Here is an example of the `Page-Show` extension:
```js
server.prepend("Show", function (req, res, next) {
   var PageMgr = require('dw/experience/PageMgr')
   var ContentModel = require('*/cartridge/models/content')
   var pageMetaHelper = require("*/cartridge/scripts/helpers/pageMetaHelper")

    var page = PageMgr.getPage(req.querystring.cid);
    if (page != null && page.isVisible()) {
        var pageContent = ContentMgr.getContent(page.ID + "-seo")
        if (pageContent) {
            var content = new ContentModel(pageContent, "content/content")

            pageMetaHelper.setPageMetaData(req.pageMetaData, content)
            pageMetaHelper.setPageMetaTags(req.pageMetaData, content)
        }
    }
    next()
})
```

Now you don't need to create a content asset for every since page from PD. For example, if you business doesn't use the syntax `${Content.pageTitle}` in your meta tag rules, or any expression regarding the object `Content`. Then in that case you could simply create one content asset for each folder the business wants to have.
You might be wondering why I'm mentioning folders and how they fit in the solution, so let's take a deeper look.

### Folders

Folders are the way you can group multiple pages, and make all of them inherit the same meta tag rules. Once a folder is created, the business can use it in the SEO meta tag rules module inside BM.
Imagine you have a group of pages that are part of the same marketing campaign. As an SEO expert, of course you'd like to have page titles, descriptions, open graph and other meta tags for all of them.
But maybe the content team hasn't reached the maturity level to configure this for every since page. So you create a meta tag rule, acting as a fallback in case the content team doesn't configure some pages.

So far the solution revolves around the business creating manually folders, assigning them to pages and creating content assets for every page (in case they want to leverage page properties) or every folder.
I believe we can improve on this solution, so let's jump in to some automation.

### Automation

This is a critical step, since having all this manual work doesn't make sense for business people. As engineers we can do better and automate the creation of these content assets. We can develop two different pieces that play together:
- a **job** to ensure every page from Page Designer, has it's associated content asset and all the rest
- a **new BM module** to automate folder creation, specifically creating multiple folders

Let's go through the job first, its responsible to update the content assets associated with each page. This means creating that content asset if it doesn't already exist, and then assign it to the same folders as the page. To implement this job you need:
1. Get the list of all Page Designer pages
2. Iterate through all pages
3. For each page, create the content asset and assign the appropriate folders

Here is a code snippet (in ES6) representing a possible implementation:
```js
const libraryGateway = require("*/cartridge/scripts/gateways/libraryGateway")
const pagesList = getAllPageDesignerPages()

pagesList.forEach(page => {
    const contentId = `${page.ID}-seo`
    const result = libraryGateway.createContentAsset({
        id: contentId,
        pageTitle: page.pageTitle
    })
    if (result.error) {
        throw new Error("Error creating content asset")
    }
    
    page.folders.forEach(folder => {
        const result = libraryGateway.assignContentAssetToFolder(contentId, folder.ID)
    })
})

function getAllPageDesignerPages() {
    const ContentSearchModel = require("dw/content/ContentSearchModel")
    const apiContentSearchModel = new ContentSearchModel()
    const libraryID = "someId"
    
    apiContentSearchModel.setRecursiveFolderSearch(true)
    apiContentSearchModel.setFilteredByFolder(false)
    apiContentSearchModel.setFolderID(libraryID)
    
    apiContentSearchModel.search()
    const contentSearchResultIterator = apiContentSearchModel.getContent()
    const count = Number(apiContentSearchModel.getCount())
    
    const pages = []
    if (contentSearchResultIterator && count > 0) {
        while (contentSearchResultIterator.hasNext()) {
            const contentResult = contentSearchResultIterator.next()
            if (contentResult?.page) {
                // transform some contentResult fields to other types...
                pages.push(contentResult)
            }
        }
    }
    
    return pages
}
```

In the implementation above, getting all pages from Page Designer is done through the `ContentSearchModel` API. Although this works, it's not ideal in my opinion since these pages are required to be **searchable** (a setting on all pages) and **online**. If they aren't, then they won't be on the [Content index](https://documentation.b2c.commercecloud.salesforce.com/DOC2/topic/com.demandware.dochelp/content/b2c_commerce/topics/search_and_navigation/b2c_index_creation.html), which seems to be the only way to get all page designer pages on a given site.
To create content assets and assigning them to folders, we delegate that responsability to the `libraryGateway` module. To implement this module we need to use OCAPI.

### OCAPI

We can use OCAPI Data APIs to create content assets/folders and assign content assets to folders. While researching I tried finding another way, but I didn't find anything easier. The [Salesforce Commerce API](https://documentation.b2c.commercecloud.salesforce.com/DOC1/topic/com.demandware.dochelp/DWAPI/scriptapi/html/api/packageList.html?cp=0_20_2) (_dw_ library accessible server-side) doesn't seem to have APIs for these operations. I didn't find any pipelets or jobs steps that do this either... perhaps by [exporting the content library](https://documentation.b2c.commercecloud.salesforce.com/DOC1/topic/com.demandware.dochelp/DWAPI/jobstepapi/html/api/jobstep.ExportContent.html), then creating a custom job that reads that file, edits it with the new content objects, and finally runs a job step to import that edited file.

Interacting with OCAPI is not an expensive development effort, so you can develop a custom cartridge for this. We won't go through the details about that cartridge, perhaps on a separate blog post :slight_smile:

### OCAPI endpoints
All the operations we want to do can be found on the Libraries resource from the Data API. To create a content asset we can use [this endpoint](https://documentation.b2c.commercecloud.salesforce.com/DOC1/index.jsp?topic=%2Fcom.demandware.dochelp%2FOCAPI%2Fcurrent%2Fdata%2FResources%2FLibraries.html&anchor=id893141162__id696504113), to assign it to a folder we can use [this endpoint](https://documentation.b2c.commercecloud.salesforce.com/DOC1/index.jsp?topic=%2Fcom.demandware.dochelp%2FOCAPI%2Fcurrent%2Fdata%2FResources%2FLibraries.html&anchor=id893141162__id-2058222274). One thing to keep in mind, creating a content asset is an **idempotent** operation. This means it creates the object if it doesn't already exist, but if it does it ignores the existing object and writes a new one on top.

In practice, this means if someone edits these content assets (e.g. through BM, locking the resource and updating the description field), that modification will be lost.
If you don't want to lose those edits, you should consider using the endpoint to get the content asset, and use that data on your PUT request payload. In my case, these content assets are supposed to be "hidden", so no one should need to edit them.

### BM extension to create folders
Now let's discuss the custom BM module to create folders. If business people want to create multiple filters in one go, instead of doing it through the BM UI and then going to Page Designer to assign pages to folders, they input the filter names in an input box and click a button. We can simply develop a web page that has this input box and a button (and some instructions on how to use it). This is possible by [extending BM](https://documentation.b2c.commercecloud.salesforce.com/DOC1/topic/com.demandware.dochelp/content/b2c_commerce/topics/site_development/b2c_customize_business_manager.html) and building a custom cartridge, with this UI and the controller that executes the creation of folders.

We won't go to much into detail about this custom cartridge, I've added additional links at the bottom to help you building this cartridge.

## Alternative solutions to SEO meta tag rules

In the case where the content object quotas are hit, we can consider building something entirely custom. What I mean is we would not use the SEO meta tag rules module from BM anymore. We would build our piece of software to handle this scenario - **all page designer page types**. Of course that means building software that does the same as Salesforce's built-in module of BM, considering:
- Storage to store these rules, meta tag definitions, and others
- An API that at least exposes a way to get meta tags for a given page, taking into account API design, SLA (important if you'll call this in a middleware of the Page.js controller), etc

This is a discussion you must have with your business/client, explaining the trade-offs of each scenario.

In my opinion, it's generally often better to reuse existing functionality or an off-the-shelf solution like a plugin cartridge.
Something that either is standardized and known in the community, instead of your own solution... but as always, this depends on the context we're in.
For SEO meta tag rules of SFCC in particular, we [can't extend this module](https://trailhead.salesforce.com/trailblazer-community/feed/0D54S00000Hk216SAB). So if we really needed to support this feature and we hit the API quotas of the SFCC platform, we would consider building a cost-effective solution outside SFCC, considering the need of a custom parser for `if` statements, context variables... again a discussion to be made with the business and architects.

## Improvements to this solution

One important note about this solution is that pages would **only inherit meta tags assigned to the default folder** (primary folder). From my research, a content asset can only have one default folder, and that is the folder the meta tags come from.
In my use case the ideal was to setup some hierarchy, where a page could have 3 folders, and you could get all meta tags assigned to those folders. Now you kind of have this [hierarchy](https://documentation.b2c.commercecloud.salesforce.com/DOC1/topic/com.demandware.dochelp/content/b2c_commerce/topics/search_engine_optimization/b2c_meta_tag_rules.html) (primary folder, then parent folders, up to root), but it's not the exact behavior I needed.

## Conclusion
In conclusion, you can develop a custom cartridge with this functionality, and support this for your business or clients. In the future, this might be supported out of the box by SFCC. The greatest challenges were: analyzing ways to extend the SEO meta tag rules module; an API to get all page designer pages and understanding the limitations of our solution.

Let us know in the comments if this feature is something you'd like to have, or vote and comment on the [IdeaExchange's post](https://ideas.salesforce.com/s/idea/a0B8W00000JJF4rUAH/support-all-page-designer-pages-for-seo-meta-tag-rules). I hope this has been helpful 👍.

Check out my other blog post on [session management for SFCC](https://blogit.create.pt/davidpereira/2022/06/06/session-management-in-salesforce-b2c-commerce-cloud/).

## Additional Links
Here are some links to documentation and other resources, that you may find useful if you're interested in building this feature for your business:
- [IdeaExchange's post](https://ideas.salesforce.com/s/idea/a0B8W00000JJF4rUAH/support-all-page-designer-pages-for-seo-meta-tag-rules)
- [How to get a list of pages of Page Designer using code?](https://trailhead.salesforce.com/trailblazer-community/feed/0D54S00000IRNXRSA5)
- [OCAPI Data Libraries resource](https://documentation.b2c.commercecloud.salesforce.com/DOC1/topic/com.demandware.dochelp/OCAPI/current/data/Resources/Libraries.html)
- [Configuring the Business Manager Site](https://documentation.b2c.commercecloud.salesforce.com/DOC1/topic/com.demandware.dochelp/content/b2c_commerce/topics/admin/b2c_configuring_a_business_manager_site.html)