Website Optimization Using Strapi, Astro.js and OpenAI

Michael - May 8 - - Dev Community

Introduction

In today's world, we're constantly bombarded with information overload and clickbait titles, and we can find ourselves quite strapped for time. Today we will look at partially solving this problem by integrating AI into a blogging app to give the reader some information about an article before reading it. This way, they can make an informed guess as to whether investing their time in an article is worth it or not. With this approach to website optimization, we provide a seamless user experience and valuable content.

We'll use several interesting technologies to achieve this: Strapi CMS to take care of the content management and backend, Astro which is a great new technology for quickly creating blazing fast frontend apps, and ChatGPT to provide the article summaries.

Prerequisites

To follow this tutorial, we will need the following:

  • An OpenAI (ChatGPT) account and API key.
  • Package manager (yarn or npm).
  • Node.js (v16 or v18).
  • Code editor (Visual Studio code, Sublime).
  • Basic knowledge of Javascript.

Setting Up Strapi

First of all, if we're not familiar with Strapi, it's a Headless CMS (content management system) like Wordpress, but the backend is detached, and we can use whatever UI framework we want. This means we can create APIs quickly without having to worry about setting up a server and using a backend language. Strapi CMS will then serve as our backend, where we can create articles with the built-in admin panel and then connect our frontend to the API to display them.

So with that explanation out of the way let's jump in to the code:

Create a folder that will contain the source code for our project. First, let's open a terminal and navigate to any directory of our choice, and run the commands below:

mkdir strapi-blog-tutorial
cd strapi-blog-tutorial
Enter fullscreen mode Exit fullscreen mode

This folder will contain both our frontend and backend code. Now, let's create our Strapi API with the command below:

npx create-strapi-app@latest strapi-blog-api
Enter fullscreen mode Exit fullscreen mode

Once this is complete and it finishes installing the dependencies, we should receive a confirmation like the one below!

It should automatically direct us to the Strapi dashboard as it starts our project. We have to create our administrator here, so let's fill out the form and click the "Let's start" button.

001-welcome-to-strapi.png

We should now see the dashboard which looks like this:

002-strapi-dashboard.png

Building the Backend API with Strapi

Now let's build the backend API that will serve the articles for our blog.

Create Collection Types

First,  we need to create some collection types, so in the left side menu, click on Content-Type Builder, and on the left, click on Create new collection type. Now we should see the below screen. Enter author in the "Display name" field and click continue.

003-create-collection-type.png

That should take us to the next page, where we can enter the different fields for our author content. Below is a list of each field our author will need. Go ahead and add these:

  • name - Text - Short text
  • bio - Text - Long text
  • profileImage - Media - Single media

Click save in the top right. This will trigger the server to restart, so just wait for that. If it takes longer than expected, then just refresh the page.

Now that we have an author, let's create the article content type by clicking on Create new collection,  entering a display name of article, and this time entering the information below.

  • title - Text - Short text
  • description - Text - Long text
  • dateAdded - Date - Date (ex: 01/01/2024)
  • coverImage - Media - Single media
  • articleMarkdown - Rich text (Markdown)

We will want to create a one-to-one relationship between our author and an article collection types, so the next field will be Relation. Find and click on that, and it should bring up the below screen:

004-add-relation.png

Then select for the relationship to be between the article and the author collection type and click finish.

Now our article collection type should look like the one below:

005-article-collection-fields.png

Click save in the top right to finish everything and restart the server.

Create Entries

Alright, now that we have our collections ready, we can start to create authors and articles using the admin panel. On the left side panel, navigate to Content Manager, and under Collection Types, we should see the two collections we just created.

006-empty-article-collection-type.png

First, let's create an author so we can assign them when it comes to creating an article, so click author from the left side menu and then click on Create new entry in the top right.

Enter the name and bio, and upload a profile image. I just generated a bio with chatGPT and got a random profile picture from unsplash. Click save and then publish in the top right, and then back to see the table with our newly created author as below:

007-author-collection.png

Let's create an article to assign to this author. Click on article from the left side menu and then create new entry in the top right. Again, we can enter what we want here; I just generated a bunch of random content and then got the image from unsplashed. Once we have all inputs filled out, choose the author to associate this article with from the dropdown and then click save and publish in the top right.

008-create-author-entry.png

Enable API Public Access

By default, Strapi requires authentication to query our API and receive information, but that is outside the scope of this tutorial. Instead, we will make our API publicly accessible. We can find more about authentication and REST API in this blog post.

From the left sidebar, click on Settings. Again,  on the left panel under USERS & PERMISSIONS PLUGIN, click on Roles, then click on Public from the table on the right. Now scroll down, click on Article, and tick Select all then click on Author and do the same then save in the top right to allow the user to access information without authentication.

Now paste the below url into our browser to access the article information with the author information populated:

http://localhost:1337/api/articles?populate=author
Enter fullscreen mode Exit fullscreen mode

009-api-result.png

Now that we have our collection types set up, have added some content and can see that we have access to the API, let's see how we can add a custom API endpoint which will connect to openAI.

Integrating OpenAI with Strapi

First navigate to the terminal and run the below command in the root directory:

yarn strapi generate
Enter fullscreen mode Exit fullscreen mode

This will begin the process of generating our own custom API. Choose the API option, give it the name article-summary-gpt, and select "no" when it asks us if this is for a plugin.

Inside the src directory, If we check the api directory in our code editor, we should see the newly created API for article-summary-gpt with it's route, controller, and service.

Let's check it works by uncommenting the code in each file, restarting the project in the terminal, and navigating to the admin dashboard. Now, once again, click Settings > Roles > Public, then scroll down to Select all on the article-summary-gpt API to make the permissions public, and click save in the top right.

Now if we enter the following into our browser and click enter, we should get an "ok" message.

http://localhost:1337/api/article-summary-gpt
Enter fullscreen mode Exit fullscreen mode

Okay, now we've confirmed the API endpoint is working, let's connect it to OpenAI first, install the OpenAI package, navigate to the route directory, and run the command below in our terminal

yarn add openai
Enter fullscreen mode Exit fullscreen mode

Then in the .env file add our API key to the OPENAI environment variable:

OPENAI=<OpenAI api key here>
Enter fullscreen mode Exit fullscreen mode

Now under the article-summary-gpt directory change the code in the routes directory to the following:

module.exports = {
  routes: [
    {
      method: "POST",
      path: "/article-summary-gpt/exampleAction",
      handler: "article-summary-gpt.exampleAction",
      config: {
        policies: [],
        middlewares: [],
      },
    },
  ],
};
Enter fullscreen mode Exit fullscreen mode

Change the code in the controller directory to the following:

"use strict";

module.exports = {
  exampleAction: async (ctx) => {
    try {
      const response = await strapi
        .service("api::article-summary-gpt.article-summary-gpt")
        .articleService(ctx);

      ctx.body = { data: response };
    } catch (err) {
      console.log(err.message);
      throw new Error(err.message);
    }
  },
};
Enter fullscreen mode Exit fullscreen mode

And the code in the services directory to the following:

"use strict";
const { OpenAI } = require("openai");
const openai = new OpenAI({
  apiKey: process.env.OPENAI,
});

/**
 * article-summary-gpt service
 */

module.exports = ({ strapi }) => ({
  articleService: async (ctx) => {
    try {
      const input = ctx.request.body.data?.input;
      const completion = await openai.chat.completions.create({
        messages: [{ role: "user", content: input }],
        model: "gpt-3.5-turbo",
      });

      const answer = completion.choices[0].message.content;

      return {
        message: answer,
      };
    } catch (err) {
      ctx.body = err;
    }
  },
});
Enter fullscreen mode Exit fullscreen mode

Now we can make a post request which contains input (which will be from our frontend) and return answers from chatGPT.

We can check the connection to our post route by pasting the below code in our terminal:

curl -X POST \
  http://localhost:1337/api/article-summary-gpt/exampleAction \
  -H 'Content-Type: application/json' \
  -d '{
    "data": {
        "input": "Can you provide a concise summary of the following article with key takeaways? - Strapi is an open-source headless Content Management System (CMS) that empowers developers to build, deploy, and manage APIs quickly and efficiently. Unlike traditional CMS platforms, Strapi decouples the frontend presentation layer from the backend content management, offering unparalleled flexibility and customization options."
    }
}'

Enter fullscreen mode Exit fullscreen mode

Implementing Frontend Components with Astro.js and React

Astro.js is a web framework for content-driven websites; it automatically removes unused JavaScript and renders to HTML for better core web vitals, conversion rates, and SEO. It has also integrated "island architecture", which means we can use our favourite frontend framework or library when we need it. For instance, we can code our website using astro components, but if we come across a scenario where we want to build a form, it would be totally possible to build this feature with React (perhaps there's a specific npm package we want to use) and then integrate it seamlessly with our other components.

This gives us great performance and SEO while also allowing us to leverage the power of UI libraries like React or Vue when we need them.

Astro does have built-in Markdown support, but for the sake of this tutorial, I want to show how we can integrate React and use the technologies side by side, so we will be using React to render the articles.

First open the terminal and navigate back to the main directory - "strapi-blog-tutorial", Then run the below command:

npm create astro@latest
Enter fullscreen mode Exit fullscreen mode

In the terminal go through the set-up wizard first create our new project at "./strapi-blog-frontend", Include sample files, no to typescript, Yes to install dependencies and don't init a new git repo.

Once that's finished change into the new directory and run the below command:

npm run dev
Enter fullscreen mode Exit fullscreen mode

Navigate to http://localhost:4321/ in our browser and we should now be able to see the below:

010-welcom-to-astro.png

Now, for the sake of simplicity, we will incorporate two views into our application: the main blog section, which will render out all of our articles and show them as cards, and the article view, which will render our Markdown and show extra information such as the author.

Since we will be integrating React into this project, let's go ahead and add that. Run the below command in our terminal under the root directory of strapi-blog-frontend

npx astro add react
Enter fullscreen mode Exit fullscreen mode

Confirm yes to the changes it will automatically try to make, and wait for it to finish installing.

Before we start, let's add some core styles to our project that our components will inherit. Open the code editor, and under layouts there should be a component named Layout.astro. This is a component that wraps all of our other components and where we can add header information and meta tags, delete everything in the style tags, and paste in the following CSS.

:root {
    --primary: #ff6a3e;
    --primaryLight: #ffba43;
    --secondary: #ffba43;
    --secondaryLight: #ffba43;
    --headerColor: #1a1a1a;
    --bodyTextColor: #4e4b66;
    --bodyTextColorWhite: #fafbfc;
    /* 13px - 16px */
    --topperFontSize: clamp(0.8125rem, 1.6vw, 1rem);
    /* 31px - 49px */
    --headerFontSize: clamp(1.9375rem, 3.9vw, 3.0625rem);
    --bodyFontSize: 1rem;
    /* 60px - 100px top and bottom */
    --sectionPadding: clamp(3.75rem, 7.82vw, 6.25rem) 1rem;
}

body {
    margin: 0;
    padding: 0;
}

*, *:before, *:after {
    box-sizing: border-box;
}
.cs-topper {
    font-size: var(--topperFontSize);
    line-height: 1.2em;
    text-transform: uppercase;
    text-align: inherit;
    letter-spacing: .1em;
    font-weight: 700;
    color: var(--primary);
    margin-bottom: 0.25rem;
    display: block;
}

.cs-title {
    font-size: var(--headerFontSize);
    font-weight: 900;
    line-height: 1.2em;
    text-align: inherit;
    max-width: 43.75rem;
    margin: 0 0 1rem 0;
    color: var(--headerColor);
    position: relative;
}

.cs-text {
    font-size: var(--bodyFontSize);
    line-height: 1.5em;
    text-align: inherit;
    width: 100%;
    max-width: 40.625rem;
    margin: 0;
    color: var(--bodyTextColor);
}
Enter fullscreen mode Exit fullscreen mode

Now under the components directory, create a folder named blog and a file named blog.css and paste the following css there:


/* Mobile - 360px */
@media only screen and (min-width: 0rem) {
  #blog-1540 {
    padding: var(--sectionPadding);
    position: relative;
    z-index: 1;
    overflow: hidden;
  }
  #blog-1540:before {
    content: '';
    width: 100%;
    height: 100%;
    background: var(--primary);
    opacity: 0.05;
    position: absolute;
    display: block;
    top: 0;
    left: 0;
    z-index: -1;
  }
  #blog-1540 .cs-container {
    width: 100%;
    max-width: 36.5rem;
    margin: auto;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: clamp(3rem, 6vw, 4rem);
    position: relative;
  }
  #blog-1540 .cs-content {
    text-align: center;
    width: 100%;
    display: flex;
    flex-direction: column;
    align-items: center;
  }
  #blog-1540 .cs-title {
    max-width: 23ch;
  }
  #blog-1540 .cs-card-group {
    width: 100%;
    margin: 0;
    padding: 0;
    display: grid;
    justify-items: center;
    grid-template-columns: repeat(12, 1fr);
    gap: 1.25rem;
  }
  #blog-1540 .cs-item {
    text-align: left;
    list-style: none;
    padding: clamp(1rem, 3vw, 1.5rem);
    box-sizing: border-box;
    background-color: #fff;
    border: 1px solid #e8e8e8;
    border-radius: 2.5rem;
    grid-column: span 12;
    position: relative;
    z-index: 1;
    overflow: hidden;
    transition: border-color 0.3s;
  }
  #blog-1540 .cs-item:hover {
    border-color: var(--primary);
  }
  #blog-1540 .cs-link {
    text-decoration: none;
    font-weight: 400;
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    position: relative;
    z-index: 1;
  }
  #blog-1540 .cs-picture-group {
    width: 100%;
    margin-bottom: 1.5rem;
    position: relative;
  }
  #blog-1540 .cs-picture {
    width: 100%;
    height: clamp(12.5rem, 45vw, 21.25rem);
    background-color: #1a1a1a;
    display: block;
    position: relative;
    z-index: 1;
    overflow: hidden;
    flex: none;
  }
  #blog-1540 .cs-picture img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    position: absolute;
    top: 0;
    left: 0;
    transition: transform 0.6s, opacity 0.3s;
  }
  #blog-1540 .cs-mask {
    --maskBG: #fff;
    --maskBorder: #e8e8e8;
    width: 101%;
    height: 101%;
    position: absolute;
    top: -1px;
    right: -1px;
    bottom: -1px;
    left: -1px;
    z-index: 1;
  }
  #blog-1540 .cs-flex {
    margin: 0 0 1.5rem 0;
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 1.5rem;
  }
  #blog-1540 .cs-tag {
    font-size: 1rem;
    font-weight: 700;
    line-height: 1.2em;
    text-align: center;
    width: fit-content;
    margin-right: 0;
    padding: 0.5rem 1rem;
    color: var(--primary);
    border-radius: 6.25rem;
    display: block;
    position: relative;
    overflow: hidden;
    cursor: pointer;
  }
  #blog-1540 .cs-tag::before {
    content: '';
    width: 100%;
    height: 100%;
    background: var(--primary);
    opacity: 0.1;
    position: absolute;
    top: 0;
    left: 0;
  }
  #blog-1540 .cs-date {
    font-size: 1rem;
    line-height: 1.5em;
    margin: 0;
    color: var(--bodyTextColor);
    display: flex;
    align-items: center;
    justify-content: flex-start;
    gap: 0.5rem;
  }
  #blog-1540 .cs-item-text {
    font-size: 1rem;
    line-height: 1.5em;
    font-weight: 400;
    margin: 0;
    color: var(--bodyTextColor);
    display: flex;
    justify-content: flex-start;
    align-items: center;
    gap: 0.5rem;
  }
  #blog-1540 .cs-h3 {
    font-size: clamp(1.25rem, 2vw, 1.5625rem);
    font-weight: 700;
    line-height: 1.2em;
    text-align: inherit;
    margin: 0 0 0.5rem 0;
    color: var(--headerColor);
    transition: color 0.3s;
  }
  #blog-1540 .cs-bottom {
    margin: 1.5rem 0 0 0;
    padding: 1.5rem 0 0 0;
    border-top: 1px solid #e8e8e8;
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 0.75rem;
  }
  #blog-1540 .cs-author-group {
    display: flex;
    justify-content: flex-start;
    align-items: center;
    gap: 0.5rem;
  }
  #blog-1540 .cs-profile {
    width: 3.125rem;
    height: 3.125rem;
    border: 2px solid #bababa;
    background-color: #bababa;
    border-radius: 50%;
    overflow: hidden;
    position: relative;
    display: block;
  }
  #blog-1540 .cs-profile img {
    position: absolute;
    top: 0;
    left: 0;
    height: 100%;
    width: 100%;
    object-fit: cover;
  }
  #blog-1540 .cs-name {
    font-size: 1rem;
    line-height: 1.2em;
    font-weight: 700;
    margin: 0;
    color: var(--headerColor);
    display: block;
  }
  #blog-1540 .cs-job {
    font-size: 1rem;
    line-height: 1.5em;
    font-weight: 700;
    margin: 0;
    color: var(--secondary);
    display: block;
  }
  #blog-1540 .cs-wrapper {
    width: 3rem;
    height: 3rem;
    border: 1px solid #bababa;
    border-radius: 50%;
    display: flex;
    justify-content: center;
    align-items: center;
  }
  .summary-container {
    border: 1px solid black;
    padding: 10px;
    border-radius: 10px;
    line-height: 1.5em;
  }
}

/* Desktop - 1024px */
@media only screen and (min-width: 64rem) {
  #blog-1540 .cs-container {
    max-width: 80rem;
  }
  #blog-1540 .cs-picture {
    height: 12.5rem;
  }
  #blog-1540 .cs-item {
    grid-column: span 4;
  }
}

Enter fullscreen mode Exit fullscreen mode

Now create BlogContainer.jsx inside the same blog folder. This is where we will render out the cards for our blog:

import './blog.css';

export default function BlogContainer() {
  return (
    <section id="blog-1540">
      <div class="cs-container">
        <div class="cs-content">
          <span class="cs-topper">Strapi-Blog</span>
          <h2 class="cs-title">Feast your eyes on our blog section!</h2>
        </div>
        <ul class="cs-card-group">{/*Render out cards here*/}</ul>
      </div>
    </section>
  );
}
Enter fullscreen mode Exit fullscreen mode

Then under pages/index.astro replace the code in there with the following

---
import Layout from '../layouts/Layout.astro';
import BlogContainer from '../components/blog/BlogContainer';
---

<Layout title="Welcome to Astro.">
    <main>
        <BlogContainer client:load/>
    </main>
</Layout>
Enter fullscreen mode Exit fullscreen mode

Create another file named BlogCard.jsx inside the blog folder and paste the following code:

import './blog.css';

export default function BlogCard({
  date,
  title,
  authorName,
  authorImage,
  articleId,
  coverImage,
  getArticleSummary
}) {
  return (
    <li className="cs-item">
      <a href={`/?id=${articleId}`} className="cs-link">
        <div className="cs-picture-group">
          <picture className="cs-picture" aria-hidden="true">
            <img
              loading="lazy"
              decoding="async"
              src={coverImage}
              width="365"
              height="201"
            />
          </picture>
          <svg
            className="cs-mask"
            width="369"
            height="249"
            viewBox="0 0 369 249"
            fill="none"
            xmlns="http://www.w3.org/2000/svg"
            preserveAspectRatio="none"
          >
            <g clip-path="url(#clip0_3335_6487)">
              <path
                d="M369 249V105.57H364.72L360.02 177.28L350.1 221.22L338.48 233.69L294.54 231.71L227.65 231.14L159.67 232.48L102.92 238.23L43.73 243.79L31 238.99L24.33 229L8.32 177.28L5.69 111.52L0 110.67V249H369Z"
                fill="var(--maskBG)"
              />
              <path
                d="M0 0H369V114.64L364.64 113.67L364.72 77.5L356.91 50.01L348.69 27.11L329.08 10.47L296.81 4.93L28.9 9.69L21.28 14.57L15.61 25.63L4 124.51H0V0Z"
                fill="var(--maskBG)"
              />
              <path
                fill-rule="evenodd"
                clip-rule="evenodd"
                d="M366 4H4V245H366V4ZM31 239C-4.86 193.71 3.34 85.57 14 31C17.78 11.59 25.37 8.61 42 8C107.73 5.59 193.2 4.66 300 5C325.79 5.1 347.16 14.21 356 43C370.28 89.64 364.08 137.32 358.09 183.32L358 184C356.03 199.42 352.41 212.38 347 224C343.96 230.49 339.5 233.58 333 233C234.49 224.39 139.41 232.28 42 244C39.88 244.27 37.99 243.86 36 243C34.01 242.14 32.41 240.79 31 239Z"
                fill="var(--maskBG)"
              />
              <path
                d="M13.9996 30.9899C9.37956 54.6199 5.22956 88.2999 4.90956 122.57C4.49956 167.43 10.6696 213.31 30.9996 238.99C32.4096 240.78 34.0096 242.13 35.9996 242.99C37.9896 243.85 39.8796 244.26 41.9996 243.99C139.42 232.27 234.49 224.38 333 232.99C339.5 233.57 343.96 230.48 347 223.99C352.41 212.37 356.02 199.41 358 183.99C364.01 137.78 370.35 89.8599 356 42.9899C347.16 14.1999 325.79 5.08989 300 4.98989C193.2 4.64989 107.73 5.57989 41.9996 7.98989C25.3696 8.59989 17.7796 11.5799 13.9996 30.9899Z"
                stroke="var(--maskBorder)"
                stroke-width="8"
              />
            </g>
            <defs>
              <clipPath id="clip0_3335_6487-1516-1540">
                <rect width="369" height="249" fill="var(--maskBG)" />
              </clipPath>
            </defs>
          </svg>
        </div>
        <div className="cs-info">
          <div className="cs-flex">
            <span onClick={getArticleSummary} className="cs-tag">Get article summary</span>
            <span className="cs-date">
              <img
                className="cs-icon"
                loading="lazy"
                decoding="async"
                src="https://csimg.nyc3.cdn.digitaloceanspaces.com/Images/Icons/calander.svg"
                alt="icon"
                width="24"
                height="24"
                aria-hidden="true"
              />
              {date}
            </span>
          </div>
          <h3 className="cs-h3">{title}</h3>
          <div className="cs-bottom">
            <div className="cs-author-group">
              <picture className="cs-profile">
                <img
                  src={authorImage}
                  decoding="async"
                  alt="profile"
                  width="50"
                  height="50"
                  aria-hidden="true"
                />
              </picture>
              <span className="cs-name">
                {authorName}
                <span className="cs-job">Author</span>
              </span>
            </div>
            <picture className="cs-wrapper">
              <img
                className="cs-arrow"
                loading="lazy"
                decoding="async"
                src="https://csimg.nyc3.cdn.digitaloceanspaces.com/Images/Icons/grey-right-chevron.svg"
                alt="icon"
                width="24"
                height="24"
                aria-hidden="true"
              />
            </picture>
          </div>
        </div>
      </a>
    </li>
  );
}
Enter fullscreen mode Exit fullscreen mode

Now to show our articles, we will need to be able to render Markdown in JSX. Let's install an npm package to help us do this. Navigate to the root of our project in the terminal and run the following command:

npm i markdown-to-jsx
Enter fullscreen mode Exit fullscreen mode

Now create an ArticleView.jsx file under the blog folder or directory with the following code:

import Markdown from 'markdown-to-jsx';

export default function ArticleView({ articleMarkdown, title }) {
  return (
    <div style={{ marginTop: '150px', padding: '40px' }}>
      <a href="/">Back</a>
      <Markdown>{articleMarkdown}</Markdown>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Okay, so now we have our views ready. We have the main container, which will fetch the blog data and render out our cards and we have the view for the articles themselves, which will render out the markdown. Let's move on to fetching the data from Strapi and displaying it in our components.

Fetching and Displaying Article Data

So let's use fetch to get the articles from the API, and we can utilise React hooks to create a reusable piece of logic that will fetch the articles and save them in state. This way, if the application evolves and we need to fetch articles in other parts of the application, we won't have to repeat the code.

Under the src directory create a folder named api and a file named articleRoutes.js with the following code:

const baseUrl = 'http://localhost:1337';
const url = `${baseUrl}/api/articles?populate[coverImage][populate][0]=data&populate[author][populate][0]=profileImage`;

export async function fetchArticles() {
  try {
    const res = await fetch(url);
    return await res.json();
  } catch (e) {
    console.error('Error fetching data:', error);
    throw error;
  }
}
Enter fullscreen mode Exit fullscreen mode

The URL is quite complex here because we have several levels of nested relations we need to populate, I made this URL using strapis interactive tool which we can find here.

In the src directory, create a hooks folder, and inside that, create a file named useGetArticle.js and add the following code:

import { useState, useEffect } from 'react';
import { fetchArticles } from '../api/articleRoutes';

function useGetArticle() {
  const [articles, setArticles] = useState(null);
  const [error, setError] = useState(null);

  useEffect(() => {
    const getArticles = async () => {
      try {
        const response = await fetchArticles();
        const data = await response;
        setArticles(data.data);
      } catch (error) {
        setError(error);
      }
    };

    getArticles();
  }, []);

  return { articles, error };
}

export default useGetArticle;
Enter fullscreen mode Exit fullscreen mode

Now with that set up, all we need to do is import our hook into the BlogContainer component and then use a map function to render out the BlogCard component. We will also add some logic to read the parameters of the current URL. This way, when the user clicks on a card, we can route to that article ID and have some rendering logic to show it.

Modify the code inside the BlogContainer.jsx with the following code:

import './blog.css';
import useGetArticle from '../../hooks/useGetArticle';
import BlogCard from './BlogCard';
import ArticleView from './ArticleView';

const baseUrl = 'http://localhost:1337';

export default function BlogContainer() {
  const { articles, error } = useGetArticle();

  if (error) {
    return <div>Error: {error.message}</div>;
  }

  if (!articles) {
    return (
      <div>
        <p>Loading...</p>
      </div>
    );
  }

  const urlSearchParams = new URLSearchParams(window.location.search);
  const params = Object.fromEntries(urlSearchParams.entries());
  const articleId = Number(params.id);
  const articleToShow = articles.find((article) => article.id === articleId);

  if (articleToShow) {
    return (
      <ArticleView
        articleMarkdown={articleToShow.attributes.articleMarkdown}
        title={articleToShow.attributes.title}
      />
    );
  }

  return (
    <section id="blog-1540">
      <div className="cs-container">
        <div className="cs-content">
          <span className="cs-topper">Strapi-Blog</span>
          <h2 className="cs-title">Feast your eyes on our blog section!</h2>
        </div>
        <ul className="cs-card-group">
          {articles.map((article, i) => {
            return (
              <BlogCard
                title={article.attributes.title}
                authorName={article.attributes.author.data.attributes.name}
                authorImage={`${baseUrl}${article.attributes.author.data.attributes.profileImage.data.attributes.url}`}
                articleId={article.id}
                coverImage={`${baseUrl}${article.attributes.coverImage.data.attributes.url}`}
                date={article.attributes.dateAdded}
              />
            );
          })}
        </ul>
      </div>
    </section>
  );
}
Enter fullscreen mode Exit fullscreen mode

Navigate back to localhost, and we should now be able to see the article we created earlier, and we should be able to click through and view the article details as shown below.

011-article-detials.gif

Enhancing User experience with AI-Generated Article Summaries

Now let's see how we can integrate the API we created earlier to get article summaries from chatGPT.

First create the below function in the api directory:

export async function fetchArticleSummary(article) {
  const gptUrl = `${baseUrl}/api/article-summary-gpt/exampleAction`;
  const data = {
    data: {
      input: `Provide a concise summary of the following article with key takeaways listed - ${article}`,
    },
  };

  try {
    const response = await fetch(gptUrl, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(data),
    });

    if (!response.ok) {
      throw new Error('Network response was not ok');
    }

    return await response.json();
  } catch (error) {
    console.error('There was a problem with the fetch operation:', error);
  }
}
Enter fullscreen mode Exit fullscreen mode

Then in the hooks directory create a file called useArticleSummary.js which will contain the logic to handle this functionality:

import { useState } from 'react';
import { fetchArticleSummary } from '../api/articleRoutes';

function useArticleSummary() {
  const [summary, setSummary] = useState(null);
  const [error, setError] = useState(null);
  const [isLoading, setIsLoading] = useState(false);

  const fetchSummary = async (article) => {
    setIsLoading(true);
    try {
      const response = await fetchArticleSummary(article);
      setSummary(response.data.message);
    } catch (error) {
      setError(error);
    } finally {
      setIsLoading(false);
    }
  };

  return { summary, error, isLoading, fetchSummary };
}

export default useArticleSummary;
Enter fullscreen mode Exit fullscreen mode

Now that's in place, let's tweak the BlogContainer to use this hook and add a section to show the article summary. Modify the BlogContainer.jsx file with the following code:

import './blog.css';
import useGetArticle from '../../hooks/useGetArticle';
import useArticleSummary from '../../hooks/useArticleSummary';
import BlogCard from './BlogCard';
import ArticleView from './ArticleView';

const baseUrl = 'http://localhost:1337';

export default function BlogContainer() {
  const { articles, error } = useGetArticle();
  const { summary, isLoading, fetchSummary } = useArticleSummary();

  if (error) {
    return <div>Error: {error.message}</div>;
  }

  if (!articles) {
    return (
      <div>
        <p>Loading...</p>
      </div>
    );
  }

  const urlSearchParams = new URLSearchParams(window.location.search);
  const params = Object.fromEntries(urlSearchParams.entries());
  const articleId = Number(params.id);
  const articleToShow = articles.find((article) => article.id === articleId);

  if (articleToShow) {
    return (
      <ArticleView
        articleMarkdown={articleToShow.attributes.articleMarkdown}
        title={articleToShow.attributes.title}
      />
    );
  }

  return (
    <section id="blog-1540">
      <div className="cs-container">
        <div className="cs-content">
          <span className="cs-topper">Strapi-Blog</span>
          <h2 className="cs-title">Feast your eyes on our blog section!</h2>
        </div>

        {isLoading && (
          <div className="summary-container">
            <p>Loading your article summary</p>
          </div>
        )}
        {summary && (
          <div className="summary-container">
            <p>{summary}</p>
          </div>
        )}
        <ul className="cs-card-group">
          {articles.map((article, i) => {
            return (
              <BlogCard
                title={article.attributes.title}
                authorName={article.attributes.author.data.attributes.name}
                authorImage={`${baseUrl}${article.attributes.author.data.attributes.profileImage.data.attributes.url}`}
                articleId={article.id}
                coverImage={`${baseUrl}${article.attributes.coverImage.data.attributes.url}`}
                date={article.attributes.dateAdded}
                getArticleSummary={() =>
                  fetchSummary(article.attributes.articleMarkdown)
                }
              />
            );
          })}
        </ul>
      </div>
    </section>
  );
}
Enter fullscreen mode Exit fullscreen mode

Demo Time

The GIF below demonstrates how we can get the summary of an article uing AI. The image below it shows the result when we click the "Get article summary".
012-demo-get-summary.gif

013-get-summary-static.png

Conclusion

That's it. Now we have a blog that provides our users with summaries and key takeaways before they decide to invest time in reading. Think of some more use cases. You could even ask the user to fill out certain forms or analyse their reading history to create summaries that they might find interesting, which will then lead them to read those articles and improve website optimization, or you could use this information to ask ChatGPT to provide a summary and a percentage of how interesting the user will find the article.

Additional Resources

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .