Updating Content Hub One Content with JavaScript SDK and a Little Help from AI

Introduction

This post is describing some of my recent experiments with Content Hub One JavaScript SDK. In this case, I needed to touch up all blogs written by XCentium employees over the last decade. Performing this task manually would be a serious undertaking and tedious effort, but why do this manually when we have ChatGPT?

This post, plus Migrating (any) Site Content to Sitecore Content Hub One, can be a good starting point for someone just starting with Content Hub One JavaScript APIs.

I found Content Hub One, a new headless CMS from Sitecore, to be quite simple and fast, with mature and simple-to-use APIs.

Implementation Approach

In this scenario, all blogs had already been migrated to Content Hub One. I needed to format content and blogs and unify styling and structure without changing the meaning of the original posts.

The high-level approach is fairly straightforward:

  • List all content items of type Bloq Post in Content Hub One, and for each item:
    • Read the body/content of the Blog Post into a string
    • Optional: save backup copy of retrieved content, in case ChatGPT editing goes out of hand
    • Prompt ChatGPT via OpenAI API to format blog content and code with Markdown and perform light syntax check, without making any major changes, which would change the original meaning.
    • Save ChatGPT response back into Content Hub One and move on to the next item

I chose to create a simple Node.js app, written in JavaScript - using C# and .NET SDK for Content Hub One could be another viable option.

Reading Content with Content Hub One JavaScript SDK

Installing Content Hub JavaScript SDK

The process of installing Content Hub One JavaScript SDK is described in this documentation article. It comes down to installing the following NPM packages:

  • @sitecore/sc-contenthub-webclient-sdk
  • @sitecore/contenthub-one-sdk

Authenticating with Content Hub One

Follow this documentation article to create an OAuth client in Content Hub One and instantiate the Content Hub One client in your code. Here’s my take on it:

import { ClientCredentialsScheme, ContentHubOneClientFactory } from "@sitecore/contenthub-one-sdk";

//....

const clientId = process.env.CLIENT_ID;
const clientSecret = process.env.CLIENT_SECRET;

//...

export function createClient() {
  if (!clientId || !clientSecret) {
    throw new Error("Missing clientId");
  }
  if (!clientSecret) {
    throw new Error("Missing clientSecret");
  }
  const credentials = new ClientCredentialsScheme(clientId, clientSecret);
  const client = ContentHubOneClientFactory.create(credentials);

  return client;
}

Reading and writing content to Content Hub One

Get a list of all items of type Blog Post

Note using paged response to scroll through blog list, 20 items at a time. Trying to read more items in single call is going to fail, so we have to chunk the response.

export async function getItemsByType(client, entityType) {
  const allItems = [];
  let getMore = true;
  let pageNumber = 1;
  while (getMore) {
    const response = await client.contentItems.getAsync(
      new ContentItemSearchRequest()
        .withPageNumber(pageNumber++)
        .withFieldQuery(
          ContentItemSearchField.contentType,
          Equality.Equals,
          entityType //"blogTag"
        )
    );

    allItems.push(...response.data);
    getMore = response.totalCount > response.pageNumber * 20;
  }

  return allItems;
}

Read item(s) by name and type.

In this case, I’m iterating through the results of the above query and reading blog contents one by one…

export async function getItemsByName(client, entityType, itemName) {
  const response = await client.contentItems.getAsync(
    new ContentItemSearchRequest()
      .withFieldQuery(
        ContentItemSearchField.contentType,
        Equality.Equals,
        entityType
      )
      .withFieldQuery(
        ContentItemSearchField.fieldName,
        Equality.Equals,
        itemName
      )
  );

  return response.data;
}

And finally, save the updated item back to Content Hub One

The following section will describe the process of updating the blog content with OpenAI. TO wrap up the Content Hub One story, here’s an example code for saving content back to Content Hub One. Optionally, updated content can be published too.

export async function updateItem(client, contentItem, publishItems) {
  console.log(
    "updating content item, id: ",
    contentItem?.id,
    "name: ",
    contentItem?.name
  );

  const result = await client.contentItems.updateAsync(contentItem);
  console.log("updated content item, id: ", result?.id);

  if (result && result.id && publishItems) {
    await client.contentItems.publishAsync(result.id);
    console.log("published media item id: ", result.id);
  }
} 

Using OpenAI JavaScript SDK

Similarly to the above section, we need to install OpenAI SDK in order to use an OpenAI client.

This documentation article described the process of installing OpenAI JavaScript SDK and initializing the client and here’s my take on it:

import { Configuration, OpenAIApi } from "openai";

//...

const configuration = new Configuration({
    apiKey: process.env.OPENAI_API_KEY,
  });
const openai = new OpenAIApi(configuration);

Using ChatGPT via OpenAI API

The following is an example call to do light formatting in markdown format. A couple of things to note:

  • Depending on type and nature of the blog content, the prompt might need to be modified to yield best results.
  • I chose to use gpt-3.5-turbo-16k model as this provided a good results at a reasonable cost (remember, OpenAI API is not free and depending on model and number of calls, total cost may vary). Depending on the use case and available models, a different model can be a better fit. This article lists the latest list of available models from OpenAI.
//...

const formattedMd = await formatBlogMarkdown(
        openai,
        "Format this blog post in markdown, make sure to format code blocks and specify their code language.",
        content
      );

//...

async function formatBlogMarkdown(openai, prompt, content) {
  const response = await openai.createChatCompletion({
    model: "gpt-3.5-turbo-16k",
    messages: [{ role: "user", content: \${prompt}\n${md}` }],
    temperature: 1,
    max_tokens: 4000,
    top_p: 1,
    frequency_penalty: 0,
    presence_penalty: 0,
  });

  const formattedMd = response.data.choices[0].message.content;
  return formattedMd;
}

Useful Links