Tailwind Logo

Create RSS feeds for articles managed by XM Cloud - Part 2

XM CloudNext.js

Published: 2024-07-05

Up to the last article, we were able to create a template of the target item in XM Cloud and retrieve the sample item in Json data using GraphQL. In the second half of this article, we will make the data viewable via RSS.

Call GraphQL with Next.js

First, we need a generic function to retrieve data using GraphQL's Endpoint. This code is a bit long, so please refer to the following URL

Here fetchGraphQL is provided and works as follows.

  • fetchGraphQL(query: string), where query is a GraphQL string
  • The following two keys are available
    • apiKey : need to specify SITECORE_API_KEY environment variable in evn.local
    • endpointUrl : GRAPH_QL_ENDPOINT as GraphQL endpoint in evn.local
  • Using the above two keys, fetch queries a GraphQL endpoint and returns the results

This time, add the following definition to .env.local

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

Now you can use fetchGraphQL.

Create rss.xml

Since we will be using Next.js' App Router, we will create a route.ts file in the following path, assuming that rss.xml will be returned in the route path.

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

Creating Interfaces

The first step is to create an Interface to handle the Json data returned based on the last data obtained in the previous step. This time, we created the following interface.

TypeScript
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>[];
    };
  };
}

Creating a Query

For the GraphQL query that worked last time, we create the following to call it passing parameters

TypeScript
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
            }
          }
        }
      }
    `;
};

When this is called, a query is generated to retrieve the needed data using the values of the language, the ID of the site root and the ID of the target template.

Retrieve data

Using the code prepared so far, prepare a function that can retrieve data as follows: Since we are calling fetchGraphQL, please also add the code import { fetchGraphQL } from 'src/utils';.

TypeScript
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;
}

Creating GET Functions

Finally, we create a GET function that returns data when requested. In this case, we first prepared the following code to check whether Json data was obtained.

TypeScript
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' },
  });
}

To confirm this result, after running the command npm run start:connected, I accessed http://localhost:3000/rss.xml and was able to confirm that the data was retrieved.

xmcrss03.png

Process data

We will modify the route.ts file to display the results in RSS format using the acquired data. This time, we will use the following package.

Execute the following command to add the package

PowerShell
npm install feed

First, define the URLs to be used by RSS as follows

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

Next, in the GET, we will set up the information required by the feed.

TypeScript
  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: 'support@sitecoredemo.jp',
      link: baseUrl,
    },
  });

Next, the acquired data is added to the created feed.

TypeScript
  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',
      },
    });
  });

Finally, the generated feed data is returned as a response.

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

In fact, when I access http://localhost:3000/rss.xml using Postman, I am able to get data as followsす。

xmcrss04.png

Convert Date format

The date data managed by Sitecore is the following string.

  • 20240730T150000Z

As a result, Invalid Date is displayed for dates. To correct this, the date data is converted to be handled by a regular expression.

TypeScript
    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'
    );

Rewrite the part that passes data as follows

TypeScript
      date: new Date(isoDateString),

The date can now be displayed correctly.

Endpoint and API keys are invalid

So far, it has been working fine using data and executing, but when GRAPH_QL_ENDPOINT and SITECORE_API_HOST are empty (first time Build situation), a Build error occurs. To work around this, we have updated the sample code by adding try - catch to avoid errors during Build and redirect processing if it fails to do so correctly.

The final sample code is as follows

Summary

In order to implement RSS, the first part of this project was to acquire data, and the second part was to process RSS data using that data. However, it is possible to cut the path or change the file name in case of different RSS. To make it somewhat generic, the language, template ID, and site root ID are called as parameters.

Tags