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.
To follow this tutorial, we will need the following:
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:
Now let's build the backend API that will serve the articles for our blog.
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 mediaClick 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.
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.
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.
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:
1module.exports = {
2 routes: [
3 {
4 method: "POST",
5 path: "/article-summary-gpt/exampleAction",
6 handler: "article-summary-gpt.exampleAction",
7 config: {
8 policies: [],
9 middlewares: [],
10 },
11 },
12 ],
13};
Change the code in the controller
directory to the following:
1"use strict";
2
3module.exports = {
4 exampleAction: async (ctx) => {
5 try {
6 const response = await strapi
7 .service("api::article-summary-gpt.article-summary-gpt")
8 .articleService(ctx);
9
10 ctx.body = { data: response };
11 } catch (err) {
12 console.log(err.message);
13 throw new Error(err.message);
14 }
15 },
16};
And the code in the services
directory to the following:
1"use strict";
2const { OpenAI } = require("openai");
3const openai = new OpenAI({
4 apiKey: process.env.OPENAI,
5});
6
7/**
8 * article-summary-gpt service
9 */
10
11module.exports = ({ strapi }) => ({
12 articleService: async (ctx) => {
13 try {
14 const input = ctx.request.body.data?.input;
15 const completion = await openai.chat.completions.create({
16 messages: [{ role: "user", content: input }],
17 model: "gpt-3.5-turbo",
18 });
19
20 const answer = completion.choices[0].message.content;
21
22 return {
23 message: answer,
24 };
25 } catch (err) {
26 ctx.body = err;
27 }
28 },
29});
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."
}
}'
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.
1:root {
2 --primary: #ff6a3e;
3 --primaryLight: #ffba43;
4 --secondary: #ffba43;
5 --secondaryLight: #ffba43;
6 --headerColor: #1a1a1a;
7 --bodyTextColor: #4e4b66;
8 --bodyTextColorWhite: #fafbfc;
9 /* 13px - 16px */
10 --topperFontSize: clamp(0.8125rem, 1.6vw, 1rem);
11 /* 31px - 49px */
12 --headerFontSize: clamp(1.9375rem, 3.9vw, 3.0625rem);
13 --bodyFontSize: 1rem;
14 /* 60px - 100px top and bottom */
15 --sectionPadding: clamp(3.75rem, 7.82vw, 6.25rem) 1rem;
16}
17
18body {
19 margin: 0;
20 padding: 0;
21}
22
23*, *:before, *:after {
24 box-sizing: border-box;
25}
26.cs-topper {
27 font-size: var(--topperFontSize);
28 line-height: 1.2em;
29 text-transform: uppercase;
30 text-align: inherit;
31 letter-spacing: .1em;
32 font-weight: 700;
33 color: var(--primary);
34 margin-bottom: 0.25rem;
35 display: block;
36}
37
38.cs-title {
39 font-size: var(--headerFontSize);
40 font-weight: 900;
41 line-height: 1.2em;
42 text-align: inherit;
43 max-width: 43.75rem;
44 margin: 0 0 1rem 0;
45 color: var(--headerColor);
46 position: relative;
47}
48
49.cs-text {
50 font-size: var(--bodyFontSize);
51 line-height: 1.5em;
52 text-align: inherit;
53 width: 100%;
54 max-width: 40.625rem;
55 margin: 0;
56 color: var(--bodyTextColor);
57}
Now under the components
directory, create a folder named blog
and a file named blog.css
and paste the following css there:
1/* Mobile - 360px */
2@media only screen and (min-width: 0rem) {
3 #blog-1540 {
4 padding: var(--sectionPadding);
5 position: relative;
6 z-index: 1;
7 overflow: hidden;
8 }
9 #blog-1540:before {
10 content: '';
11 width: 100%;
12 height: 100%;
13 background: var(--primary);
14 opacity: 0.05;
15 position: absolute;
16 display: block;
17 top: 0;
18 left: 0;
19 z-index: -1;
20 }
21 #blog-1540 .cs-container {
22 width: 100%;
23 max-width: 36.5rem;
24 margin: auto;
25 display: flex;
26 flex-direction: column;
27 align-items: center;
28 gap: clamp(3rem, 6vw, 4rem);
29 position: relative;
30 }
31 #blog-1540 .cs-content {
32 text-align: center;
33 width: 100%;
34 display: flex;
35 flex-direction: column;
36 align-items: center;
37 }
38 #blog-1540 .cs-title {
39 max-width: 23ch;
40 }
41 #blog-1540 .cs-card-group {
42 width: 100%;
43 margin: 0;
44 padding: 0;
45 display: grid;
46 justify-items: center;
47 grid-template-columns: repeat(12, 1fr);
48 gap: 1.25rem;
49 }
50 #blog-1540 .cs-item {
51 text-align: left;
52 list-style: none;
53 padding: clamp(1rem, 3vw, 1.5rem);
54 box-sizing: border-box;
55 background-color: #fff;
56 border: 1px solid #e8e8e8;
57 border-radius: 2.5rem;
58 grid-column: span 12;
59 position: relative;
60 z-index: 1;
61 overflow: hidden;
62 transition: border-color 0.3s;
63 }
64 #blog-1540 .cs-item:hover {
65 border-color: var(--primary);
66 }
67 #blog-1540 .cs-link {
68 text-decoration: none;
69 font-weight: 400;
70 display: flex;
71 flex-direction: column;
72 justify-content: space-between;
73 position: relative;
74 z-index: 1;
75 }
76 #blog-1540 .cs-picture-group {
77 width: 100%;
78 margin-bottom: 1.5rem;
79 position: relative;
80 }
81 #blog-1540 .cs-picture {
82 width: 100%;
83 height: clamp(12.5rem, 45vw, 21.25rem);
84 background-color: #1a1a1a;
85 display: block;
86 position: relative;
87 z-index: 1;
88 overflow: hidden;
89 flex: none;
90 }
91 #blog-1540 .cs-picture img {
92 width: 100%;
93 height: 100%;
94 object-fit: cover;
95 position: absolute;
96 top: 0;
97 left: 0;
98 transition: transform 0.6s, opacity 0.3s;
99 }
100 #blog-1540 .cs-mask {
101 --maskBG: #fff;
102 --maskBorder: #e8e8e8;
103 width: 101%;
104 height: 101%;
105 position: absolute;
106 top: -1px;
107 right: -1px;
108 bottom: -1px;
109 left: -1px;
110 z-index: 1;
111 }
112 #blog-1540 .cs-flex {
113 margin: 0 0 1.5rem 0;
114 display: flex;
115 justify-content: space-between;
116 align-items: center;
117 gap: 1.5rem;
118 }
119 #blog-1540 .cs-tag {
120 font-size: 1rem;
121 font-weight: 700;
122 line-height: 1.2em;
123 text-align: center;
124 width: fit-content;
125 margin-right: 0;
126 padding: 0.5rem 1rem;
127 color: var(--primary);
128 border-radius: 6.25rem;
129 display: block;
130 position: relative;
131 overflow: hidden;
132 cursor: pointer;
133 }
134 #blog-1540 .cs-tag::before {
135 content: '';
136 width: 100%;
137 height: 100%;
138 background: var(--primary);
139 opacity: 0.1;
140 position: absolute;
141 top: 0;
142 left: 0;
143 }
144 #blog-1540 .cs-date {
145 font-size: 1rem;
146 line-height: 1.5em;
147 margin: 0;
148 color: var(--bodyTextColor);
149 display: flex;
150 align-items: center;
151 justify-content: flex-start;
152 gap: 0.5rem;
153 }
154 #blog-1540 .cs-item-text {
155 font-size: 1rem;
156 line-height: 1.5em;
157 font-weight: 400;
158 margin: 0;
159 color: var(--bodyTextColor);
160 display: flex;
161 justify-content: flex-start;
162 align-items: center;
163 gap: 0.5rem;
164 }
165 #blog-1540 .cs-h3 {
166 font-size: clamp(1.25rem, 2vw, 1.5625rem);
167 font-weight: 700;
168 line-height: 1.2em;
169 text-align: inherit;
170 margin: 0 0 0.5rem 0;
171 color: var(--headerColor);
172 transition: color 0.3s;
173 }
174 #blog-1540 .cs-bottom {
175 margin: 1.5rem 0 0 0;
176 padding: 1.5rem 0 0 0;
177 border-top: 1px solid #e8e8e8;
178 display: flex;
179 justify-content: space-between;
180 align-items: center;
181 gap: 0.75rem;
182 }
183 #blog-1540 .cs-author-group {
184 display: flex;
185 justify-content: flex-start;
186 align-items: center;
187 gap: 0.5rem;
188 }
189 #blog-1540 .cs-profile {
190 width: 3.125rem;
191 height: 3.125rem;
192 border: 2px solid #bababa;
193 background-color: #bababa;
194 border-radius: 50%;
195 overflow: hidden;
196 position: relative;
197 display: block;
198 }
199 #blog-1540 .cs-profile img {
200 position: absolute;
201 top: 0;
202 left: 0;
203 height: 100%;
204 width: 100%;
205 object-fit: cover;
206 }
207 #blog-1540 .cs-name {
208 font-size: 1rem;
209 line-height: 1.2em;
210 font-weight: 700;
211 margin: 0;
212 color: var(--headerColor);
213 display: block;
214 }
215 #blog-1540 .cs-job {
216 font-size: 1rem;
217 line-height: 1.5em;
218 font-weight: 700;
219 margin: 0;
220 color: var(--secondary);
221 display: block;
222 }
223 #blog-1540 .cs-wrapper {
224 width: 3rem;
225 height: 3rem;
226 border: 1px solid #bababa;
227 border-radius: 50%;
228 display: flex;
229 justify-content: center;
230 align-items: center;
231 }
232 .summary-container {
233 border: 1px solid black;
234 padding: 10px;
235 border-radius: 10px;
236 line-height: 1.5em;
237 }
238}
239
240/* Desktop - 1024px */
241@media only screen and (min-width: 64rem) {
242 #blog-1540 .cs-container {
243 max-width: 80rem;
244 }
245 #blog-1540 .cs-picture {
246 height: 12.5rem;
247 }
248 #blog-1540 .cs-item {
249 grid-column: span 4;
250 }
251}
Now create BlogContainer.jsx
inside the same blog
folder. This is where we will render out the cards for our blog:
1import './blog.css';
2
3export default function BlogContainer() {
4 return (
5 <section id="blog-1540">
6 <div class="cs-container">
7 <div class="cs-content">
8 <span class="cs-topper">Strapi-Blog</span>
9 <h2 class="cs-title">Feast your eyes on our blog section!</h2>
10 </div>
11 <ul class="cs-card-group">{/*Render out cards here*/}</ul>
12 </div>
13 </section>
14 );
15}
Then under pages/index.astro
replace the code in there with the following
1---
2import Layout from '../layouts/Layout.astro';
3import BlogContainer from '../components/blog/BlogContainer';
4---
5
6<Layout title="Welcome to Astro.">
7 <main>
8 <BlogContainer client:load/>
9 </main>
10</Layout>
Create another file named BlogCard.jsx
inside the blog
folder and paste the following code:
1import './blog.css';
2
3export default function BlogCard({
4 date,
5 title,
6 authorName,
7 authorImage,
8 articleId,
9 coverImage,
10 getArticleSummary
11}) {
12 return (
13 <li className="cs-item">
14 <a href={`/?id=${articleId}`} className="cs-link">
15 <div className="cs-picture-group">
16 <picture className="cs-picture" aria-hidden="true">
17 <img
18 loading="lazy"
19 decoding="async"
20 src={coverImage}
21 width="365"
22 height="201"
23 />
24 </picture>
25 <svg
26 className="cs-mask"
27 width="369"
28 height="249"
29 viewBox="0 0 369 249"
30 fill="none"
31 xmlns="http://www.w3.org/2000/svg"
32 preserveAspectRatio="none"
33 >
34 <g clip-path="url(#clip0_3335_6487)">
35 <path
36 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"
37 fill="var(--maskBG)"
38 />
39 <path
40 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"
41 fill="var(--maskBG)"
42 />
43 <path
44 fill-rule="evenodd"
45 clip-rule="evenodd"
46 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"
47 fill="var(--maskBG)"
48 />
49 <path
50 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"
51 stroke="var(--maskBorder)"
52 stroke-width="8"
53 />
54 </g>
55 <defs>
56 <clipPath id="clip0_3335_6487-1516-1540">
57 <rect width="369" height="249" fill="var(--maskBG)" />
58 </clipPath>
59 </defs>
60 </svg>
61 </div>
62 <div className="cs-info">
63 <div className="cs-flex">
64 <span onClick={getArticleSummary} className="cs-tag">Get article summary</span>
65 <span className="cs-date">
66 <img
67 className="cs-icon"
68 loading="lazy"
69 decoding="async"
70 src="https://csimg.nyc3.cdn.digitaloceanspaces.com/Images/Icons/calander.svg"
71 alt="icon"
72 width="24"
73 height="24"
74 aria-hidden="true"
75 />
76 {date}
77 </span>
78 </div>
79 <h3 className="cs-h3">{title}</h3>
80 <div className="cs-bottom">
81 <div className="cs-author-group">
82 <picture className="cs-profile">
83 <img
84 src={authorImage}
85 decoding="async"
86 alt="profile"
87 width="50"
88 height="50"
89 aria-hidden="true"
90 />
91 </picture>
92 <span className="cs-name">
93 {authorName}
94 <span className="cs-job">Author</span>
95 </span>
96 </div>
97 <picture className="cs-wrapper">
98 <img
99 className="cs-arrow"
100 loading="lazy"
101 decoding="async"
102 src="https://csimg.nyc3.cdn.digitaloceanspaces.com/Images/Icons/grey-right-chevron.svg"
103 alt="icon"
104 width="24"
105 height="24"
106 aria-hidden="true"
107 />
108 </picture>
109 </div>
110 </div>
111 </a>
112 </li>
113 );
114}
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:
1import Markdown from 'markdown-to-jsx';
2
3export default function ArticleView({ articleMarkdown, title }) {
4 return (
5 <div style={{ marginTop: '150px', padding: '40px' }}>
6 <a href="/">Back</a>
7 <Markdown>{articleMarkdown}</Markdown>
8 </div>
9 );
10}
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.
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:
1const baseUrl = 'http://localhost:1337';
2const url = `${baseUrl}/api/articles?populate[coverImage][populate][0]=data&populate[author][populate][0]=profileImage`;
3
4export async function fetchArticles() {
5 try {
6 const res = await fetch(url);
7 return await res.json();
8 } catch (e) {
9 console.error('Error fetching data:', error);
10 throw error;
11 }
12}
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:
1import { useState, useEffect } from 'react';
2import { fetchArticles } from '../api/articleRoutes';
3
4function useGetArticle() {
5 const [articles, setArticles] = useState(null);
6 const [error, setError] = useState(null);
7
8 useEffect(() => {
9 const getArticles = async () => {
10 try {
11 const response = await fetchArticles();
12 const data = await response;
13 setArticles(data.data);
14 } catch (error) {
15 setError(error);
16 }
17 };
18
19 getArticles();
20 }, []);
21
22 return { articles, error };
23}
24
25export 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:
1import './blog.css';
2import useGetArticle from '../../hooks/useGetArticle';
3import BlogCard from './BlogCard';
4import ArticleView from './ArticleView';
5
6const baseUrl = 'http://localhost:1337';
7
8export default function BlogContainer() {
9 const { articles, error } = useGetArticle();
10
11 if (error) {
12 return <div>Error: {error.message}</div>;
13 }
14
15 if (!articles) {
16 return (
17 <div>
18 <p>Loading...</p>
19 </div>
20 );
21 }
22
23 const urlSearchParams = new URLSearchParams(window.location.search);
24 const params = Object.fromEntries(urlSearchParams.entries());
25 const articleId = Number(params.id);
26 const articleToShow = articles.find((article) => article.id === articleId);
27
28 if (articleToShow) {
29 return (
30 <ArticleView
31 articleMarkdown={articleToShow.attributes.articleMarkdown}
32 title={articleToShow.attributes.title}
33 />
34 );
35 }
36
37 return (
38 <section id="blog-1540">
39 <div className="cs-container">
40 <div className="cs-content">
41 <span className="cs-topper">Strapi-Blog</span>
42 <h2 className="cs-title">Feast your eyes on our blog section!</h2>
43 </div>
44 <ul className="cs-card-group">
45 {articles.map((article, i) => {
46 return (
47 <BlogCard
48 title={article.attributes.title}
49 authorName={article.attributes.author.data.attributes.name}
50 authorImage={`${baseUrl}${article.attributes.author.data.attributes.profileImage.data.attributes.url}`}
51 articleId={article.id}
52 coverImage={`${baseUrl}${article.attributes.coverImage.data.attributes.url}`}
53 date={article.attributes.dateAdded}
54 />
55 );
56 })}
57 </ul>
58 </div>
59 </section>
60 );
61}
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.
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:
1export async function fetchArticleSummary(article) {
2 const gptUrl = `${baseUrl}/api/article-summary-gpt/exampleAction`;
3 const data = {
4 data: {
5 input: `Provide a concise summary of the following article with key takeaways listed - ${article}`,
6 },
7 };
8
9 try {
10 const response = await fetch(gptUrl, {
11 method: 'POST',
12 headers: {
13 'Content-Type': 'application/json',
14 },
15 body: JSON.stringify(data),
16 });
17
18 if (!response.ok) {
19 throw new Error('Network response was not ok');
20 }
21
22 return await response.json();
23 } catch (error) {
24 console.error('There was a problem with the fetch operation:', error);
25 }
26}
Then in the hooks
directory create a file called useArticleSummary.js
which will contain the logic to handle this functionality:
1import { useState } from 'react';
2import { fetchArticleSummary } from '../api/articleRoutes';
3
4function useArticleSummary() {
5 const [summary, setSummary] = useState(null);
6 const [error, setError] = useState(null);
7 const [isLoading, setIsLoading] = useState(false);
8
9 const fetchSummary = async (article) => {
10 setIsLoading(true);
11 try {
12 const response = await fetchArticleSummary(article);
13 setSummary(response.data.message);
14 } catch (error) {
15 setError(error);
16 } finally {
17 setIsLoading(false);
18 }
19 };
20
21 return { summary, error, isLoading, fetchSummary };
22}
23
24export 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:
1import './blog.css';
2import useGetArticle from '../../hooks/useGetArticle';
3import useArticleSummary from '../../hooks/useArticleSummary';
4import BlogCard from './BlogCard';
5import ArticleView from './ArticleView';
6
7const baseUrl = 'http://localhost:1337';
8
9export default function BlogContainer() {
10 const { articles, error } = useGetArticle();
11 const { summary, isLoading, fetchSummary } = useArticleSummary();
12
13 if (error) {
14 return <div>Error: {error.message}</div>;
15 }
16
17 if (!articles) {
18 return (
19 <div>
20 <p>Loading...</p>
21 </div>
22 );
23 }
24
25 const urlSearchParams = new URLSearchParams(window.location.search);
26 const params = Object.fromEntries(urlSearchParams.entries());
27 const articleId = Number(params.id);
28 const articleToShow = articles.find((article) => article.id === articleId);
29
30 if (articleToShow) {
31 return (
32 <ArticleView
33 articleMarkdown={articleToShow.attributes.articleMarkdown}
34 title={articleToShow.attributes.title}
35 />
36 );
37 }
38
39 return (
40 <section id="blog-1540">
41 <div className="cs-container">
42 <div className="cs-content">
43 <span className="cs-topper">Strapi-Blog</span>
44 <h2 className="cs-title">Feast your eyes on our blog section!</h2>
45 </div>
46
47 {isLoading && (
48 <div className="summary-container">
49 <p>Loading your article summary</p>
50 </div>
51 )}
52 {summary && (
53 <div className="summary-container">
54 <p>{summary}</p>
55 </div>
56 )}
57 <ul className="cs-card-group">
58 {articles.map((article, i) => {
59 return (
60 <BlogCard
61 title={article.attributes.title}
62 authorName={article.attributes.author.data.attributes.name}
63 authorImage={`${baseUrl}${article.attributes.author.data.attributes.profileImage.data.attributes.url}`}
64 articleId={article.id}
65 coverImage={`${baseUrl}${article.attributes.coverImage.data.attributes.url}`}
66 date={article.attributes.dateAdded}
67 getArticleSummary={() =>
68 fetchSummary(article.attributes.articleMarkdown)
69 }
70 />
71 );
72 })}
73 </ul>
74 </div>
75 </section>
76 );
77}
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".
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.
Hey! 👋 I'm Mike, a seasoned web developer with 5 years of full-stack expertise. Passionate about tech's impact on the world, I'm on a journey to blend code with compelling stories. Let's explore the tech landscape together! 🚀✍️