Website Optimization Using Strapi, Astro.js and OpenAI
Tags: webdev, astro, openai
Published: 2024-05-08 09:18:03

## 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](https://astro.build) which is a great new technology for quickly creating blazing fast frontend apps, and [ChatGPT](https://chat.openai.com/) to provide the article summaries.

## Prerequisites
To follow this tutorial, we will need the following:

- An [OpenAI](https://platform.openai.com/) (ChatGPT) account and API key.
- Package manager (yarn or npm).
- [Node.js](https://nodejs.org/en/download) (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:

```bash
mkdir strapi-blog-tutorial
cd strapi-blog-tutorial
```

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

```bash
npx create-strapi-app@latest strapi-blog-api
```

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](https://api-prod.strapi.io/uploads/001_welcome_to_strapi_85de6ddde5.png)

We should now see the dashboard which looks like this:

![002-strapi-dashboard.png](https://api-prod.strapi.io/uploads/002_strapi_dashboard_388c2938f4.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](https://api-prod.strapi.io/uploads/003_create_collection_type_9d709da429.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](https://api-prod.strapi.io/uploads/004_add_relation_844c53d3ae.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](https://api-prod.strapi.io/uploads/005_article_collection_fields_62257291cd.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](https://api-prod.strapi.io/uploads/006_empty_article_collection_type_03fbd12f42.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](https://api-prod.strapi.io/uploads/007_author_collection_6797dd3279.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](https://api-prod.strapi.io/uploads/008_create_author_entry_fe0ca228c7.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](https://strapi.io/blog/guide-on-authenticating-requests-with-the-rest-api).

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:

```bash
http://localhost:1337/api/articles?populate=author
```

![009-api-result.png](https://api-prod.strapi.io/uploads/009_api_result_7e16c46330.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:

```bash
yarn strapi generate
```

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.

```bash
http://localhost:1337/api/article-summary-gpt
```

Okay, now we've confirmed the API endpoint is working, let's connect it to [OpenAI](https://openai.com/) first, install the [OpenAI package](https://www.npmjs.com/package/openai), navigate to the route directory, and run the command below in our terminal

```bash
yarn add openai
```

Then in the `.env` file add our [API key](https://platform.openai.com/api-keys) to the `OPENAI` environment variable:

```bash
OPENAI=<OpenAI api key here>
```

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

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

Change the code in the `controller` directory to the following:

```javascript
"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);
    }
  },
};
```

And the code in the `services` directory to the following:

```javascript
"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;
    }
  },
});
```

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:

```bash
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."
    }
}'

```

## 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:

```bash
npm create astro@latest
```

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:

```bash
npm run dev
```

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

![010-welcom-to-astro.png](https://api-prod.strapi.io/uploads/010_welcom_to_astro_cdf7e36b05.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`

```bash
npx astro add react
```

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.

```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);
}
```
Now under the `components` directory, create a folder named `blog` and a file named `blog.css` and paste the following css there:

```css

/* 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;
  }
}

```

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

```javascript
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>
  );
}
```

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

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

<Layout title="Welcome to Astro.">
	<main>
	    <BlogContainer client:load/>
	</main>
</Layout>
```

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

```javascript
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>
  );
}
```

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:

```bash
npm i markdown-to-jsx
```

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

```javascript
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>
  );
}
```

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:

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

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](https://docs.strapi.io/dev-docs/api/rest/interactive-query-builder).

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

```javascript
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;
```

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:

```javascript
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>
  );
}
```

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](https://api-prod.strapi.io/uploads/011_article_detials_9e3eb0f48a.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:

```javascript
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);
  }
}
```

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

```javascript
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;
```

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:

```javascript
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>
  );
}
```

## 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](https://api-prod.strapi.io/uploads/012_demo_get_summary_574bc83f8e.gif)

![013-get-summary-static.png](https://api-prod.strapi.io/uploads/013_get_summary_static_82536912a3.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
* [Github link](https://github.com/gitChimp88/strapi-blog-tutorial) to the complete code.