Website Optimization Using Strapi, Astro.js, and OpenAI
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
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
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.
We should now see the dashboard which looks like this:
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.
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 textbio
- Text - Long textprofileImage
- 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 textdescription
- Text - Long textdateAdded
- Date - Date (ex: 01/01/2024)coverImage
- Media - Single mediaarticleMarkdown
- 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:
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:
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.
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:
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.
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
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
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
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
Then in the .env
file add our API key to the OPENAI
environment variable:
OPENAI=<OpenAI api key here>
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: [],
},
},
],
};
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);
}
},
};
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;
}
},
});
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."
}
}'
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
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
Navigate to http://localhost:4321/
in our browser and we should now be able to see the below:
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
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);
}
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;
}
}
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>
);
}
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>
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>
);
}
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
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>
);
}
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;
}
}
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;
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>
);
}
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.
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);
}
}
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;
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>
);
}
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”.
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 to the complete code.