Docs / tech

XM Cloud で管理している記事の RSS フィードを作成する

2024-07-04 2025-07-29 |

今回は XM Cloud のコンテンツに関して、GraphQL から記事の一覧を取得して RSS フィードを作成する手順を紹介します。

前提条件

今回は RSS フィードのサンプルを作ることを前提として、最小限のフィールドとサンプルで作成をしていきます。実際のページテンプレートに同様のフィールドを設定すれば、記事だけでなく様々な RSS を実装することが可能です。

前提条件は以下のようにします。

  • RSS フィードとして、対象のテンプレートの以下の項目を取得します
  • タイトル
  • 概要
  • 公開日
  • URL
  • 画像
  • Next.js App Router の仕組みを利用して実装する

JSS 22.0 の Next.js は 14.1 でサイト全体としては Page Router で構築されています。Page Router と App Router は共存することが可能です。RSS に関しては GraphQL を利用してデータを取得、XML でデータを返すという実装をしたいと考えています。そこで今回は App Router で実装していきます。

News テンプレートの作成

テンプレートの作成にあたって、Page テンプレートを複製して新しく Article テンプレートを作成します。/sitecore/templates/Project/Tailwindcss/Page のテンプレートを右クリックして、Duplicate をクリックしてください。

今回は News というテンプレートで作成します。既存の Title はそのまま利用する形として、以下の3つの項目を追加してください。

  • Description - Multi-line Text
  • PublishDate - Datetime
  • Image - Image

これでテンプレートが完成です。実装においては、News のテンプレートを利用してアイテムを作成することができるように、どのタイプの下に作れるのか、なども定義する必要がありますが、今回は RSS フィードを作成するのが目的のため省略します。

サンプルのアイテムを作成する

今回は5つほどの記事をサンプルとして作成をして、RSS として表示するように作っていきます。実際に対象となるコンテンツの本数が増えた場合は、複数回に分けて GraphQL でデータを取得する必要が出てきますが、RSS の最小限のサンプルを作るのが今回のコンセプトのため省略します。

今回は RSS のサンプルを以下のような形でいくつかのページの下に作成をしてみました。上記で作成した News テンプレートを利用して、いくつかのパスでアイテムが作成されているのがわかります。

Postman を利用してデータを取得

上記のアイテムに関して、GraphQL からデータを取得するクエリを作成していきます。条件は以下の通りとなります。

  • News テンプレート ID のコンテンツの一覧を取得
  • 英語のアイテムを取得
  • アイテムのフィールドのデータ表示
  • 日付順に並べ替える

それでは実際にクエリを作成していきます。

News コンテンツ一覧の取得

問い合わせとしては、Sitecore が管理しているアイテムに対して News テンプレートとして利用している ID で問い合わせをします。以下の問い合わせとなります。

query Search ($templateId: String!){
    search(
        where: {
            name: "_templates"
            value: $templateId
            operator: EQ
        }
    ) {
        total
    }
}

今回は $templateId 対して、GraphQL Variables として以下のデータを設定しています。

{
    "templateId": "B9453B23-0E09-4D98-99C0-EAA0F16DD6DA"
}

以下の結果を取得しました。

{
    "data": {
        "search": {
            "total": 6
        }
    }
}

5つアイテムを作成したはずですが、このテンプレートで作っているアイテムの数は 6 となっています。これは後ほど修正します。

英語のアイテムを取得

続いて対象となるコンテンツを英語飲みに絞り込みをします。言語を切り替えて別の RSS を作成する形も考えられるため、この値も GraphQL Variables として持つように以下のように変更します。

{
    "language": "en",
    "templateId": "B9453B23-0E09-4D98-99C0-EAA0F16DD6DA"
}

続いて言語の絞り込みをする際には、where でテンプレート ID に対して AND のルールを追加して、言語に関してしていするようにします。

query Search($language: String!, $templateId: String!) {
    search(
        where: {
            name: "_templates"
            value: $templateId
            operator: EQ
            AND: { name: "_language", value: $language, operator: EQ }
        }
    ) {
        total
    }
}

結果は同じく 6 が今回も返されました。実際に他の言語のコンテンツを作成しても、Total の数字が変わらない形になりますので、絞り込みができているかの確認は CMS 上で別の言語を作成して確認をしてください。

アイテムのフィールドを追加する

アイテムのデータを取得する際の GraphQL に関して、項目を今回は絞って実行したいと思います。まずは id および URL を取得するようにします。

query Search($language: String!, $templateId: String!) {
    search(
        where: {
            name: "_templates"
            value: $templateId
            operator: EQ
            AND: { name: "_language", value: $language, operator: EQ }
        }
    ) {
        total
        results {
            id
            url {
                hostName
                path
            }
        }
    }
}

これで実行をすると6つのアイテムの path を確認することができます。結果を見ると、実は以下のアイテムの取得ができていることがわかります。

{
  "id": "D6C15B8571874F96A684CA8655E615AA",
  "url": {
    "hostName": "xmcloudcm.localhost",
    "path": "/en/sitecore/templates/Project/Tailwindcss/PageType/News/__Standard-Values"
  }
}

つまりテンプレートを作成して、そのテンプレートの定義の __Standard-Values のアイテムが同じ ID を利用していることになります。そこで、今回はコンテンツツリーの配下のアイテムのみを取得するように、Where に siteRootId を追加して変数として持てるようにします。

query Search($language: String!, $siteRootId: String!, $templateId: String!) {
    search(
        where: {
            name: "_templates"
            value: $templateId
            operator: EQ
            AND: [
                { name: "_path", value: $siteRootId, operator: CONTAINS }
                { name: "_language", value: $language, operator: EQ }
            ]
        }
    ) {

この時の Variables 配下の通りです。

{
    "language": "en",
    "siteRootId": "E66EE43B-398B-486E-9F7F-5FE36A4093D3",
    "templateId": "B9453B23-0E09-4D98-99C0-EAA0F16DD6DA"
}

この _path の value に関しては、コンテンツツリーのトップのアイテムの ID を利用しています。これにより、Article のアイテムでもツリーの配下のみのデータを取る形となります。

続いて必要なフィールドのデータを取るために、results の下を以下のように書き換えます。

results {
    id
    url {
        hostName
        path
    }
    title: field(name: "Title") {
        value
    }
    publishDate: field(name: "PublishDate") {
        value
    }
    description: field(name: "Description") {
        value
    }
    image: field(name: "Image") {
        jsonValue
    }
}

これで RSS で必要となるデータが揃ってきました。

公開日順で取得

取得しているデータのうち、publishDate の項目を新着順にするための並び順を Where の中に記述をします。

以下が、完成した GraphQL の問い合わせとなります。

query Search($language: String!, $siteRootId: String!, $templateId: String!) {
    search(
        where: {
            name: "_templates"
            value: $templateId
            operator: EQ
            AND: [
                { name: "_path", value: $siteRootId, operator: CONTAINS }
                { name: "_language", value: $language, operator: EQ }
            ]
        }
        orderBy: { direction: DESC, name: "publishDate" }
    ) {
        total
        results {
            id
            url {
                hostName
                path
            }
            title: field(name: "Title") {
                value
            }
            publishDate: field(name: "PublishDate") {
                value
            }
            description: field(name: "Description") {
                value
            }
            image: field(name: "Image") {
                jsonValue
            }
        }
    }
}

Next.js 経由で GraphQL を呼び出す

まず GraphQL の Endpoint を利用してデータを取得するための汎用関数を用意します。

type GraphQLResponseWithErrors = {
  errors: unknown[];
};

export async function fetchGraphQL(query: string): Promise<unknown> {
  const apiKey = process.env.SITECORE_API_KEY;
  const endpointUrl = process.env.GRAPH_QL_ENDPOINT;

  try {
    return await fetch(endpointUrl, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-GQL-Token': apiKey,
      },
      body: JSON.stringify({ query }),
    })
      .then((response: Response) => {
        const jsonResponsePromise = response.json();
        jsonResponsePromise.then((jsonResponse: unknown) => {
          const responseWithErrors = jsonResponse as GraphQLResponseWithErrors;
          if (responseWithErrors.errors && responseWithErrors.errors.length > 0) {
            console.error(
              'An error was returned by a GraphQL query. See the associated logged object for details.',
              responseWithErrors
            );
          }
        });
        return jsonResponsePromise;
      })
      .catch((error) => {
        return console.log(error);
      });
  } catch (error) {
    return console.log(error);
  }
}

ここでは fetchGraphQL が用意されており、動作は以下の通りです。

  • fetchGraphQL(query: string) としており、query には GraphQL の文字列を指定します
  • 以下の2つのキーを用意しています
    • apiKey : evn.local に SITECORE_API_KEY の環境変数を指定する必要があります
    • endpointUrl : evn.local に GraphQL のエンドポイントとして GRAPH_QL_ENDPOINT を指定します
  • 上記2つのキーを利用して fetch で GraphQL エンドポイントに対して問い合わせ、その結果を返します

今回は .env.local に以下の定義を追加します。

GRAPH_QL_ENDPOINT=https://xmcloudcm.localhost/sitecore/api/graph/edge
SITECORE_API_KEY=1830F204-87EB-4132-9A7A-C9E4CD7C9A19

これで fetchGraphQL を利用することが可能となりました。

rss.xml の作成

今回は Next.js の App Router を利用するため、ルートのパスで rss.xml を返すことを想定して、以下のパスに route.ts ファイルを作成します。

  • src\tailwindcss\src\app\rss.xml\route.ts

インターフェイスの作成

前回、最後に取得したデータをもとに返ってくる Json のデータを扱うための Interface をまず作成します。今回は以下のように作りました。

interface News {
  id: string;
  url: {
    hostName: string;
    path: string;
  };
  title: {
    value: string;
  };
  publishDate: {
    value: string;
  };
  description: {
    value: string;
  };
  image: {
    jsonValue: {
      value: {
        src: string;
        alt: string;
        width: string;
        height: string;
      };
    };
  };
}

interface AllNewsResponse {
  data: {
    search: {
      total: number;
      results: Partial<News>[];
    };
  };
}

Query の作成

前回動作した GraphQL のクエリに関して、パラメーターを渡して呼び出すために、以下のように作成をします。

const AllNewsQuery = (language: string, siteRootId: string, templateId: string) => {
  return `
      query {
        search(
          where: {
            name: "_templates"
            value: "${templateId}"
            operator: EQ
            AND: [
              {
                name: "_path"
                value: "${siteRootId}"
                operator: CONTAINS
              }
              { name: "_language", value: "${language}", operator: EQ }
            ]
          }
          orderBy: { direction: DESC, name: "publishDate" }
        ) {
          total
          results {
            id
            url {
              hostName
              path
            }
            title: field(name: "Title") {
              value
            }
            publishDate: field(name: "PublishDate") {
              value
            }
            description: field(name: "Description") {
              value
            }
            image: field(name: "Image") {
              jsonValue
            }
          }
        }
      }
    `;
};

これを呼び出す際には、言語、サイトのルートの ID および対象となるテンプレートの ID の値を利用して、必要となるデータを取得するためのクエリが生成されます。

データを取得する

これまで用意したコードを利用して、以下のようにデータを取得することができる関数を用意します。fetchGraphQL を呼び出しているため、import { fetchGraphQL } from ‘src/utils’; のコードも追加してください。

async function getAllArticle(language: string, siteRootId: string, templateId: string) {
  const results: AllNewsResponse = (await fetchGraphQL(
    AllNewsQuery(language, siteRootId, templateId)
  )) as AllNewsResponse;

  const news: Partial<News>[] = [];

  results.data.search.results.forEach((post: Partial<News>) => {
    news.push({
      id: post.id,
      url: post.url,
      title: post.title,
      description: post.description,
      publishDate: post.publishDate,
      image: post.image,
    });
  });

  return news;
}

GET 関数の作成

最後に、リクエストされた際にデータを返す GET 関数を作成します。今回は Json のデータを取得できているのかを確認するために、以下のようなコードをまず用意しました。

export async function GET() {
  const posts = await getAllArticle(
    'en',
    'E66EE43B-398B-486E-9F7F-5FE36A4093D3',
    'B9453B23-0E09-4D98-99C0-EAA0F16DD6DA'
  );

  return new Response(JSON.stringify({ posts }), {
    headers: { 'Content-Type': 'application/json' },
  });
}

この結果を確認するために、npm run start:connected のコマンドを実行したあと、http://localhost:3000/rss.xml にアクセスをするとデータが取れていることを確認できました。

データを加工する

取得したデータを利用して RSS 形式のデータで結果を表示するように route.ts ファイルを変更していきます。今回はパッケージとして、以下のパッケージを利用します。

以下のコマンドを実行してパッケージを追加します。

npm install feed

まず RSS が利用する URL を以下のように定義します。

const baseUrl = "https://sitecoredemo.jp";

続いて GET の中で、feed で必要となる情報を設定していきます。

const feed = new Feed({
    title: 'Sitecoredemo.jp RSS',
    description: 'This is RSS Feed about demo news',
    id: baseUrl,
    link: baseUrl,
    copyright: `${new Date().getFullYear()} Sitecoredemo.jp`,
    language: 'en',
    favicon: baseUrl + 'favicon.png',
    feedLinks: {
      rss2: baseUrl + 'rss.xml',
    },
    author: {
      name: 'Shinichi Haramizu',
      email: '[email protected]',
      link: baseUrl,
    },
  });

続いて、作成した feed に対して、取得したデータを追加していきます。

  posts.map((post) => {
    feed.addItem({
      title: post.title?.value || 'Title',
      id: post.id,
      link: baseUrl + post.url?.path,
      date: new Date(post.publishDate?.value || '2024-01-10'),
      description: post.description?.value || '',
      image: {
        url: post.image?.jsonValue.value.src || '/next.svg',
      },
    });
  });

最後に、生成された feed のデータをレスポンスとして返します。

  return new Response(feed.rss2(), {
    headers: {
      'Content-Type': 'application/atom+xml; charset=utf-8',
    },
  });

実際に Postman を利用して http://localhost:3000/rss.xml にアクセスをすると、以下のようにデータの取得ができています。

日付の修正

Sitecore で管理している日付のデータは、以下のような文字列になっています。

  • 20240730T150000Z

この結果、日付に関しては Invalid Date という結果が表示されています。これを修正するために、日付のデータに関して正規表現で扱えるように変換をします。

const publishDate = post.publishDate?.value || '20240110T00:00:00Z';

    const isoDateString = publishDate.replace(
      /(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})Z/,
      '$1-$2-$3T$4:$5:$6Z'
    );

データを渡す部分を以下のように書き換えます。

date: new Date(isoDateString),

これで正しく日付も表示ができるようになりました。

Endpoint および API キーが無効の場合

これまでデータを利用して実行して、という形で問題なく動いていましたが、 GRAPH_QL_ENDPOINT および SITECORE_API_HOST が空の場合(初回 Build の状況)では Build エラーが発生してしまいます。これを回避するために、try - catch を追加して、Build の際にエラーが出ないように、処理が正しくできなかった場合はリダイレクト処理をするように、サンプルのコードを更新しました。

まとめ

今回は RSS を実装するために、データの取得、利用して RSS のデータを加工する、という形で仕上げました。URL に関しては Next.js App Router を利用して作成しているため、今回はルートに rss.xml となっていますが、パスを切ったり、異なる RSS の場合はファイル名を変更することが可能です。ある程度汎用性を持たせるために、言語、テンプレートの ID そしてサイトのルート ID をパラメータとして呼び出すようにしてあります。