SalesforceCMSのコンテンツを自作のLightning Web Componentで表示させてみた

# 背景
昨年リリースされたSalesforce CMSは今Salesforce機能の一つとして成長してます。
以下主な目的でSalesforce CMS利用できると思います。
- [Workspaceを作成しその中にChannelを作成することができます](https://help.salesforce.com/articleView?id=cms_cmsworkspace_overview.htm&type=5)。
- [作成されたコンテンツをカスタマーコミュニティサイトなどに表示させることもできます](https://help.salesforce.com/articleView?id=community_managed_content_adding_to_site.htm&type=5)。
- [CMS Connectで外部CMSとの連携もできます](https://help.salesforce.com/articleView?id=communities_cms_connect.htm&type=5)。

その流れで、自作のカスタムLightningコンポーネントでCMSのコンテンツを表示させることは簡単にできるだろうと思いきや、詳しいやり方やベストプラクティスは公式ドキュメントにも中々見つかりませんでした。一つの手がかりがこちらの記事です。
https://developer.salesforce.com/blogs/2019/11/content-delivery-api-to-extend-or-integrate-content.html
上記の記事通りで実際にやってみると意外と落とし穴がありましたね。

# よしやってみよう！
全てのソースコードは[Github](https://github.com/arufian/Salesforce-CMS-LWC/tree/main/force-app/main/default)にあげました。参考してもらえれば嬉しいです。

## まずはAPEXクラスを作成
CMSコンテンツデータを取得するためにはLWC（Lightning Web Component）のみで取得することができないので、Apexを経由して取得する必要があります。

書いたApexのコードはこちらです：

```Apex
public with sharing class MyContentManager {
 
    @AuraEnabled(cacheable=true)
    public static List<ConnectApi.ManagedContentVersion> initMethod(){
        MCController mcController = new MCController();
        return mcController.results;
    }
 
    public class MCController{
        private String communityId;
        public List<ConnectApi.ManagedContentVersion> results;
 
        public MCController(){
            communityId = Network.getNetworkId();
            getMContent();
        }
         
        public void getMContent() {
            try{
                String language = 'ja';
                ConnectApi.ManagedContentVersionCollection contentList = ConnectApi.ManagedContent.getAllManagedContent(communityId, 0, 25, language, 'cms_image', true);
                System.debug(contentList);
                results = contentList.items;           
            }
            catch(ConnectApi.ConnectApiException e){
                System.debug('Error Message : '+e);
                results = new List<ConnectApi.ManagedContentVersion>();
            }           
        }
    }   
}
```

一つ一つのコードを見ていきましょう。

### MCController

こちらのクラスは`ConnectApi`よりSalesforceCMSのコンテンツを取得するためのでクラスです。ConnectAPIの仕様は[こちら](https://developer.salesforce.com/docs/atlas.ja-jp.apexcode.meta/apexcode/apex_classes_connect_api.htm)で参考できます。

今回SalesforceCMSのコンテンツは特定のコミュニティーページに作成されましたので、以下の用に`MCController`のコンストラクターでコミュニティーIDを取得します。

```
communityId = Network.getNetworkId();
```

### MCControllerのgetMContentメソッド
Apexクラスのメイン処理はこちらのメソッドにあります。
[ConnectApi.ManagedContent.getAllManagedContent](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_ConnectAPI_ManagedContent_static_methods.htm)でCMSコンテンツを取得します。以下はgetAllManagedContentメソッドの仕様：

```
getAllManagedContent(String communityId, Integer pageParam, Integer pageSize, String language, String managedContentType, showAbsoluteUrl)
```
パラメータ説明：

```
- communityId: コミュニティID
- pageParam :  ページ数、０を指摘するとページ１のコンテンツが返ってきます
- pageSize: ページ毎の最大コンテンツ数
- language: コンテンツは言語毎に分かれるのでどの言語のコンテンツを取得するのを指摘する必要があります
- managedContentType: 一番重要なパラメータで、コンテンツの種類名を指摘します
- showAbsoluteUrl: 取得するコンテンツのURLが絶対URLか相対かを指摘することができます。外部サービスやコミュニティーページには絶対URLを取得した方が無難です。
```

コンテンツの種類名はSalesforce上で拝見することができないので、メーターデータを覗きながら取得しないといけません。以下の画像（GIF）をご参考ください。
![contenttype-metadata.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2262/84dfccc5-28e7-14fe-1724-15d9f3c9419f.gif)

```Apex
results = contentList.items;
```
得されたコンテンツのアイテムを`results`変数に渡します。後でこちらの変数はLWC側に渡します。

### DEMJContentManagementクラスのinitMethodメソッド
```Apex
@AuraEnabled(cacheable=true)
public static List<ConnectApi.ManagedContentVersion> initMethod(){
    MCController mcController = new MCController();
    return mcController.results;
}
```
こちらのメソッドはLWC側にコンテンツを渡すためのメソッドです。先ほど取得された`results`変数はこちらのメソッドでLWCに渡されます。

## LWCコンポーネント

LWCでは CSS、HTML、JS、メターデータのファイルで分かれてます。今回はCSSとメタデータは使ってないので、JSとHTMLのみ解説します。

### JavaScript

全体的な書いたJSコードはこちら：

```JavaScript
import { LightningElement, wire, track } from 'lwc';

import initMethod from '@salesforce/apex/MyContentManager.initMethod';

export default class DemjCMSContent extends LightningElement {
  @track results;
  @track datas = [];
  @track loaded = false;

  @wire(initMethod)
  loadRecord({ error, data }) {
    if (error) console.error(error);
    if (data) {
      this.results = data;
      this.populateData();
    }
  }

  populateData = async () => {
    try {
      this.results.forEach(val => {
        const { source, title } = val.contentNodes;
        this.datas.push({
          id: val.managedContentId,
          image: source.url,
          title: title.value,
          description: (val.contentNodes.altText) ? val.contentNodes.altText.value : '',
          url: (val.contentNodes.thumbUrl) ? val.contentNodes.thumbUrl.value : '#',
        });
      });
      this.datas.sort((prev, next) => {
        const nameA = prev.title;
        const nameB = next.title;
        if (nameA < nameB) {
          return -1;
        }
        if (nameA > nameB) {
          return 1;
        }
        return 0;
      })
      this.loaded = true;
    } catch (e) {
      console.error(e)
    }
  }
}
```

上を分解して、下に解説します：

#### State
こちらは今回のLWCコンポーネントで使ったState。

```JavaScript
@track results; // 上記で取得されたコンテンツを保管するための変数です。
@track datas = []; // レンダリングする際に必要な変数
@track loaded = false; // 非同期処理を待つ間にローディング画面を表示するための変数
```

#### loadRecordメソッドのwire
この`wire`のメソッドはApexで取得されたコンテンツを非同期で取得するためのメソッドです。

```JavaScript
/**
 *  ApexのinitMethodを非同期処理で呼び込む
 **/
@wire(initMethod)
loadRecord({ error, data }) {
  if (error) console.error(error);
  if (data) {
    this.results = data; // initMethodの戻り値はこちらの変数に保管
    this.populateData();
  }
}
```
#### populateDataメソッド
データが全て取得されたら、今度は取得されたデータをコンポーネント上でレンダリングします。
そして`Array.sort`でタイトル順で並び替える

```JavaScript
/**
 * レンダリングする際に必要なデータを生成
 **/
populateData = async () => {
  try {
    this.results.forEach(val => {
      const { source, title } = val.contentNodes;
      // レンダーリングに必要なデータを生成
      this.datas.push({
        id: val.managedContentId,
        image: source.url,
        title: title.value,
        description: (val.contentNodes.altText) ? val.contentNodes.altText.value : '', // altTextはオプショナルなので、念のためチェックしデフォルト値を設定
        url: (val.contentNodes.thumbUrl) ? val.contentNodes.thumbUrl.value : '#',　// thumbUrlもオプショナルなので、念のためチェックしデフォルト値を設定
      });
    });
    // 生成されたデータをタイトル順で並び替える
    this.datas.sort((prev, next) => {
      const nameA = prev.title;
      const nameB = next.title;
      if (nameA < nameB) {
        return -1;
      }
      if (nameA > nameB) {
        return 1;
      }
      return 0;
    })
    this.loaded = true;
  } catch (e) {
    console.error(e)
  }
}
```
注意：LWCあるあるかもしれませんが、Apexより取得された値がNullの場合、JSONオブジェクトのIndex自体がなくなります。Salesforce CMSでは`contentNodes.altText`と`contentNodes.thumbUrl`は必須項目ではないなので、念のためNullの場合デフォルト値を与えます。

### HTML

柔軟性を表現させるため、今回レンダリングするUIは２種類用意します。カルーセルとカード型。

```html
<template>
  <!-- 非同期処理が終わるまでローディングスピナーを回す -->
  <template if:false={loaded}>
    <lightning-spinner alternative-text="Loading" size="medium"></lightning-spinner>
  </template>
  <template if:true={loaded}>
    <!-- カルーセル型でデータをレンダリングする -->
    <div class="container">
      <lightning-carousel>
        <template for:each={datas} for:item="data">
          <lightning-carousel-image key={data.id} src={data.image} header={data.title} description={data.description}
            href={data.url}>
          </lightning-carousel-image>
        </template>
      </lightning-carousel>
    </div>

    <div class="card-container">
      <template for:each={datas} for:item="data">
        <!-- カード型でデータをレンダリングする -->
        <div class="card-sizer" key={data.id}>
          <a href={data.url} target="_blank">
            <lightning-card>
              <p class="slds-p-horizontal_small centering">
                <img src={data.image} />
              </p>
              <p slot="footer" class="footer">
                <span class="title">{data.title}</span>
                <span class="description">{data.description}</span>
              </p>
            </lightning-card>
          </a>
        </div>
      </template>
    </div>
  </template>
</template>
```

以上、Salesforce CMSのコンテンツを自作のLWCコンポーネントで表示させてみました。

# 最後に
上ではSalesforce CMSのコンテンツを取得する方法を書いたですが、逆にApexやLWCでコンテンツ作成することはできるのか？
残念ながらそのインターフェスはまだ用意されてませんね。根拠はこちら：
- [ManagedContentクラスをみてみるととGet（取得）しかできません](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_ConnectAPI_ManagedContent_static_methods.htm#apex_ConnectAPI_ManagedContent_static_methods)
- [RESTAPIも同じく取得しかできません](https://developer.salesforce.com/docs/atlas.en-us.chatterapi.meta/chatterapi/connect_resources_managed_content_resources.htm)
リリースされてからまだ一年しか立ってないので、今後のアップデートに期待してます。