In the previous tutorial, we completed our Dashboard and Account pages. In this section, we will work on generating our video summary using Open AI and LangChain.
We will kick off the tutorial by working on our SummaryForm
component. This time around, instead of using a server action,
we will explore how to create an api route in Next.js.
You can learn more about Next.js route handlers here
But first, let's create our summary form, which we can use to submit the request.
Navigate to src/components/forms
and create a new file called summary-form.tsx
and paste in the following code as the starting point.
1"use client";
2
3import { useState } from "react";
4import { toast } from "sonner";
5import { cn } from "@/lib/utils";
6
7import { Input } from "@/components/ui/input";
8import { SubmitButton } from "@/components/custom/submit-button";
9
10interface StrapiErrorsProps {
11 message: string | null;
12 name: string;
13}
14
15const INITIAL_STATE = {
16 message: null,
17 name: "",
18};
19
20export function SummaryForm() {
21 const [loading, setLoading] = useState(false);
22 const [error, setError] = useState<StrapiErrorsProps>(INITIAL_STATE);
23 const [value, setValue] = useState<string>("");
24
25 async function handleFormSubmit(event: React.FormEvent<HTMLFormElement>) {
26 event.preventDefault();
27 setLoading(true);
28 toast.success("Summary Created");
29 setLoading(false);
30 }
31
32 function clearError() {
33 setError(INITIAL_STATE);
34 if (error.message) setValue("");
35 }
36
37 const errorStyles = error.message
38 ? "outline-1 outline outline-red-500 placeholder:text-red-700"
39 : "";
40
41 return (
42 <div className="w-full max-w-[960px]">
43 <form
44 onSubmit={handleFormSubmit}
45 className="flex gap-2 items-center justify-center"
46 >
47 <Input
48 name="videoId"
49 placeholder={
50 error.message ? error.message : "Youtube Video ID or URL"
51 }
52 value={value}
53 onChange={(e) => setValue(e.target.value)}
54 onMouseDown={clearError}
55 className={cn(
56 "w-full focus:text-black focus-visible:ring-pink-500",
57 errorStyles
58 )}
59 required
60 />
61
62 <SubmitButton
63 text="Create Summary"
64 loadingText="Creating Summary"
65 loading={loading}
66 />
67 </form>
68 </div>
69 );
70}
The above code contains a basic form UI and a handleFormSubmit
function, which does not include any of our logic to get the summary yet.
We also use Sonner, one of my favorite toast libraries. You can learn more about it here.
But we are not using it directly; instead, we are using the Chadcn UI component, which you can find here.
npx shadcn@latest add sonner
Once Sonner is installed, implement it in our main layout.tsx
file by adding the following import.
1import { Toaster } from "@/components/ui/sonner";
And adding the code below above our TopNav
.
1<body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
2 <Toaster position="bottom-center" />
3 <Header data={globalData.data.header} />
4 {children}
5 <Footer data={globalData.data.footer} />
6</body>
Let's add this form to our top navigation by navigating to the src/components/custom/header.tsx
file and making the following changes.
1// import the form
2import { SummaryForm } from "@/components/forms/summary-form";
3
4// rest of the code
5
6export async function Header({ data }: Readonly<HeaderProps>) {
7 const { logoText, ctaButton } = data;
8 const user = await getUserMeLoader();
9
10 return (
11 <div className="flex items-center justify-between px-4 py-3 bg-white shadow-md dark:bg-gray-800">
12 <Logo text={logoText.text} />
13 {user.ok && <SummaryForm />}
14 <div className="flex items-center gap-4">
15 {user.ok ? (
16 <LoggedInUser userData={user.data} />
17 ) : (
18 <Link href={ctaButton.url}>
19 <Button>{ctaButton.text}</Button>
20 </Link>
21 )}
22 </div>
23 </div>
24 );
25}
Let's restart our frontend project and see if it shows up.
Now that our basic form is working let's examine how to set up our first API Handler Route in Next.js 15.
We will have the Next.js docs open as a reference.
Let's start by creating a new folder inside our app
directory called api
, a folder called summarize
, and a file called route.ts
. Then, paste in the following code.
1import { NextRequest } from "next/server";
2
3export async function POST(req: NextRequest) {
4 console.log("FROM OUR ROUTE HANDLER:", req.body);
5 try {
6 return new Response(
7 JSON.stringify({ data: "return from our handler", error: null }),
8 {
9 status: 200,
10 }
11 );
12 } catch (error) {
13 console.error("Error processing request:", error);
14 if (error instanceof Error)
15 return new Response(JSON.stringify({ error: error }));
16 return new Response(JSON.stringify({ error: "Unknown error" }));
17 }
18}
Next, let's create a service to call our new route handler. Navigate to src/data/services
and create a new file called summary-service.ts
.
Create a new async function called generateSummaryService
with the following code.
1export async function generateSummaryService(videoId: string) {
2 const url = "/api/summarize";
3 try {
4 const response = await fetch(url, {
5 method: "POST",
6 body: JSON.stringify({ videoId: videoId }),
7 });
8 return await response.json();
9 } catch (error) {
10 console.error("Failed to generate summary:", error);
11 if (error instanceof Error) return { error: { message: error.message } };
12 return { data: null, error: { message: "Unknown error" } };
13 }
14}
The following service allows us to call our newly created route handler located at api/summarize
endpoint. It expects us to pass a videoId
for the video we want to summarize.
Now that we have our basic route handler let's return to our summary-form.tsx
file and see if we can request this endpoint.
Let's modify our handleFormSubmit
with the following code to use our newly created service. Don't forget to import the generateSummaryService
service.
1import { generateSummaryService } from "@/data/services/summary-service";
1async function handleFormSubmit(event: React.FormEvent<HTMLFormElement>) {
2 event.preventDefault();
3 setLoading(true);
4
5 const formData = new FormData(event.currentTarget);
6 const videoId = formData.get("videoId") as string;
7
8 toast.success("Generating Summary");
9
10 const summaryResponseData = await generateSummaryService(videoId);
11 console.log(summaryResponseData, "Response from route handler");
12
13 toast.success("Testing Toast");
14 setLoading(false);
15}
When you submit your form, you should see the message returned from our route handler in our console log.
Now that we know that our Summary Form and Route Handler are connected, we can work on the logic responsible for summarizing our video.
This section will examine how to create a video summary based on the video transcript.
We will be using a couple of services to help us accomplish this.
You must have an account with Open AI. If you don't have one, go here and create one.
In the past, I have used the youtube-transcript library; interestingly, it broke just recently. You can read through the issues here.
NOTE: Using other libraries, especially ones not officially supported, can lead to breaking changes, so keep this in mind.
I ended up creating my own implementation based on this library and created it as Strapi Plugin. You can learn about it here.
Which I have already deployed and we will just call that endpoint in our code to get the summary.
But I suggest checking out the tutorial above to learn how to build your own plugin and add it to your Strapi backend.
Let's navigate to src/app/api/summarize/route.ts
and implement the logic to get the transcript.
Let's make the following changes.
1import { NextRequest } from "next/server";
2
3export async function POST(req: NextRequest) {
4 console.log("FROM OUR ROUTE HANDLER:", req.body);
5
6 const body = await req.json();
7 const videoId = body.videoId;
8 const url = `https://deserving-harmony-9f5ca04daf.strapiapp.com/utilai/yt-transcript/${videoId}`;
9
10 let transcriptData;
11
12 try {
13 const transcript = await fetch(url);
14 transcriptData = await transcript.text();
15 } catch (error) {
16 console.error("Error processing request:", error);
17 if (error instanceof Error)
18 return new Response(JSON.stringify({ error: error.message }));
19 return new Response(JSON.stringify({ error: "Unknown error" }));
20 }
21
22 return new Response(JSON.stringify({ transcript: transcriptData }));
23}
Before we test our front end, let's add a check to our form to ensure we have a valid video ID or URL.
In the summary-form.tsx
update the handleFormSubmit
function with the following.
1async function handleFormSubmit(event: React.FormEvent<HTMLFormElement>) {
2 event.preventDefault();
3 setLoading(true);
4
5 const formData = new FormData(event.currentTarget);
6 const videoId = formData.get("videoId") as string;
7
8 const processedVideoId = extractYouTubeID(videoId);
9
10 if (!processedVideoId) {
11 toast.error("Invalid Youtube Video ID");
12 setLoading(false);
13 setValue("");
14 setError({
15 ...INITIAL_STATE,
16 message: "Invalid Youtube Video ID",
17 name: "Invalid Id",
18 });
19 return;
20 }
21
22 toast.success("Generating Summary");
23
24 const summaryResponseData = await generateSummaryService(processedVideoId);
25 console.log(summaryResponseData, "Response from route handler");
26
27 toast.success("Testing Toast");
28 setLoading(false);
29}
Notice that we are using the extractYouTubeID
function to ensure we have a valid YouTube video ID.
If we don't have a valid video ID or URL, we will show an error message and stop the function from continuing.
Now before we test our front end, let's add the extractYouTubeID
function to our utils.ts
file.
Here is the code for the extractYouTubeID
function.
1export function extractYouTubeID(urlOrID: string): string | null {
2 // Regular expression for YouTube ID format
3 const regExpID = /^[a-zA-Z0-9_-]{11}$/;
4
5 // Check if the input is a YouTube ID
6 if (regExpID.test(urlOrID)) {
7 return urlOrID;
8 }
9
10 // Regular expression for standard YouTube links
11 const regExpStandard = /youtube\.com\/watch\?v=([a-zA-Z0-9_-]+)/;
12
13 // Regular expression for YouTube Shorts links
14 const regExpShorts = /youtube\.com\/shorts\/([a-zA-Z0-9_-]+)/;
15
16 // Check for standard YouTube link
17 const matchStandard = urlOrID.match(regExpStandard);
18 if (matchStandard) {
19 return matchStandard[1];
20 }
21
22 // Check for YouTube Shorts link
23 const matchShorts = urlOrID.match(regExpShorts);
24 if (matchShorts) {
25 return matchShorts[1];
26 }
27
28 // Return null if no match is found
29 return null;
30}
Just make sure to import it in our summary-form.tsx
file.
1import { extractYouTubeID } from "@/lib/utils";
The complete summary-form.tsx
file should look like the following.
1"use client";
2
3import { useState } from "react";
4import { toast } from "sonner";
5import { cn, extractYouTubeID } from "@/lib/utils";
6
7import { Input } from "@/components/ui/input";
8import { SubmitButton } from "@/components/custom/submit-button";
9
10import { generateSummaryService } from "@/data/services/summary-service";
11
12interface StrapiErrorsProps {
13 message: string | null;
14 name: string;
15}
16
17const INITIAL_STATE = {
18 message: null,
19 name: "",
20};
21
22export function SummaryForm() {
23 const [loading, setLoading] = useState(false);
24 const [error, setError] = useState<StrapiErrorsProps>(INITIAL_STATE);
25 const [value, setValue] = useState<string>("");
26
27 async function handleFormSubmit(event: React.FormEvent<HTMLFormElement>) {
28 event.preventDefault();
29 setLoading(true);
30
31 const formData = new FormData(event.currentTarget);
32 const videoId = formData.get("videoId") as string;
33
34 const processedVideoId = extractYouTubeID(videoId);
35
36 if (!processedVideoId) {
37 toast.error("Invalid Youtube Video ID");
38 setLoading(false);
39 setValue("");
40 setError({
41 ...INITIAL_STATE,
42 message: "Invalid Youtube Video ID",
43 name: "Invalid Id",
44 });
45 return;
46 }
47
48 toast.success("Generating Summary");
49
50 const summaryResponseData = await generateSummaryService(processedVideoId);
51 console.log(summaryResponseData, "Response from route handler");
52
53 toast.success("Testing Toast");
54 setLoading(false);
55 }
56
57 function clearError() {
58 setError(INITIAL_STATE);
59 if (error.message) setValue("");
60 }
61
62 const errorStyles = error.message
63 ? "outline-1 outline outline-red-500 placeholder:text-red-700"
64 : "";
65
66 return (
67 <div className="w-full max-w-[960px]">
68 <form
69 onSubmit={handleFormSubmit}
70 className="flex gap-2 items-center justify-center"
71 >
72 <Input
73 name="videoId"
74 placeholder={
75 error.message ? error.message : "Youtube Video ID or URL"
76 }
77 value={value}
78 onChange={(e) => setValue(e.target.value)}
79 onMouseDown={clearError}
80 className={cn(
81 "w-full focus:text-black focus-visible:ring-pink-500",
82 errorStyles
83 )}
84 required
85 />
86
87 <SubmitButton
88 text="Create Summary"
89 loadingText="Creating Summary"
90 loading={loading}
91 />
92 </form>
93 </div>
94 );
95}
Now, let's test our front end. Make sure to provide a valid YouTube video ID or URL.
Once we submit our form, you should see the response in the console.
Excellent. Now that we have our transcript, we can use it to prepare our summary.
Now, let's navigate to our route handler at src/app/api/summarize/route.ts
and add a check to check if a user is logged in and has available credits.
First, import the following helper methods.
1import { getUserMeLoader } from "@/data/services/get-user-me-loader";
2import { getAuthToken } from "@/data/services/get-token";
And the following lines are inside the POST
function.
1export async function POST(req: NextRequest) {
2 const user = await getUserMeLoader();
3 const token = await getAuthToken();
4
5 if (!user.ok || !token) {
6 return new Response(
7 JSON.stringify({ data: null, error: "Not authenticated" }),
8 { status: 401 }
9 );
10 }
11
12 if (user.data.credits < 1) {
13 return new Response(
14 JSON.stringify({
15 data: null,
16 error: "Insufficient credits",
17 }),
18 { status: 402 }
19 );
20 }
21
22 // rest of code
23}
The final code should look like the following.
1import { NextRequest } from "next/server";
2import { getUserMeLoader } from "@/data/services/get-user-me-loader";
3import { getAuthToken } from "@/data/services/get-token";
4
5export async function POST(req: NextRequest) {
6 const user = await getUserMeLoader();
7 const token = await getAuthToken();
8
9 if (!user.ok || !token) {
10 return new Response(
11 JSON.stringify({ data: null, error: "Not authenticated" }),
12 { status: 401 }
13 );
14 }
15
16 if (user.data.credits < 1) {
17 return new Response(
18 JSON.stringify({
19 data: null,
20 error: "Insufficient credits",
21 }),
22 { status: 402 }
23 );
24 }
25
26 const body = await req.json();
27 const videoId = body.videoId;
28 const url = `https://deserving-harmony-9f5ca04daf.strapiapp.com/utilai/yt-transcript/${videoId}`;
29
30 let transcriptData;
31
32 try {
33 const transcript = await fetch(url);
34 transcriptData = await transcript.text();
35 } catch (error) {
36 console.error("Error processing request:", error);
37 if (error instanceof Error)
38 return new Response(JSON.stringify({ error: error.message }));
39 return new Response(JSON.stringify({ error: "Unknown error" }));
40 }
41
42 return new Response(JSON.stringify({ transcript: transcriptData }));
43}
We must add a check in our summary-form.tsx
file to handle the errors inside our handleFormSubmit
function.
Let's add the following code after this line.
1const summaryResponseData = await generateSummaryService(videoId);
2console.log(summaryResponseData, "Response from route handler");
3
4// add the following
5
6if (summaryResponseData.error) {
7 setValue("");
8 toast.error(summaryResponseData.error);
9 setError({
10 ...INITIAL_STATE,
11 message: summaryResponseData.error,
12 name: "Summary Error",
13 });
14 setLoading(false);
15 return;
16}
17
18// rest of the code
The completed code in the handleFormSubmit
function should look like the following.
1async function handleFormSubmit(event: React.FormEvent<HTMLFormElement>) {
2 event.preventDefault();
3 setLoading(true);
4
5 const formData = new FormData(event.currentTarget);
6 const videoId = formData.get("videoId") as string;
7
8 const processedVideoId = extractYouTubeID(videoId);
9
10 if (!processedVideoId) {
11 toast.error("Invalid Youtube Video ID");
12 setLoading(false);
13 setValue("");
14 setError({
15 ...INITIAL_STATE,
16 message: "Invalid Youtube Video ID",
17 name: "Invalid Id",
18 });
19 return;
20 }
21
22 toast.success("Generating Summary");
23
24 const summaryResponseData = await generateSummaryService(processedVideoId);
25 console.log(summaryResponseData, "Response from route handler");
26
27 if (summaryResponseData.error) {
28 setValue("");
29 toast.error(summaryResponseData.error);
30 setError({
31 ...INITIAL_STATE,
32 message: summaryResponseData.error,
33 name: "Summary Error",
34 });
35 setLoading(false);
36 return;
37 }
38
39 toast.success("Testing Toast");
40 setLoading(false);
41}
Now, let's test our form. Make sure to provide a valid YouTube video ID or URL and set credits to 0.
Excellent, it is working. Now, we are ready to implement our logic to get our summary. Let's do it.
Now, let's write our logic to handle the generation of our summary with OpenAi and LangChain.
If you never used LangChain before, it is a tool that helps you simplify building AI-powered apps. You can learn about it here.
Before starting, install the following packages @langchain/openai
and langchain
with the following command.
yarn add @langchain/core @langchain/openai
Nice. Now, let's make the following changes in our route handler: Navigate to src/app/api/summarize/route.ts
and make the following changes.
First, let's import all the required dependencies.
1import { ChatOpenAI } from "@langchain/openai";
2import { PromptTemplate } from "@langchain/core/prompts";
3import { StringOutputParser } from "@langchain/core/output_parsers";
Now, let's create our generateSummary
function.
1async function generateSummary(content: string, template: string) {
2 const prompt = PromptTemplate.fromTemplate(template);
3
4 const model = new ChatOpenAI({
5 openAIApiKey: process.env.OPENAI_API_KEY,
6 modelName: process.env.OPENAI_MODEL ?? "gpt-4o-mini",
7 temperature: process.env.OPENAI_TEMPERATURE
8 ? parseFloat(process.env.OPENAI_TEMPERATURE)
9 : 0.7,
10 maxTokens: process.env.OPENAI_MAX_TOKENS
11 ? parseInt(process.env.OPENAI_MAX_TOKENS)
12 : 4000,
13 });
14
15 const outputParser = new StringOutputParser();
16 const chain = prompt.pipe(model).pipe(outputParser);
17
18 try {
19 const summary = await chain.invoke({ text: content });
20 return summary;
21 } catch (error) {
22 if (error instanceof Error)
23 return new Response(JSON.stringify({ error: error.message }));
24 return new Response(
25 JSON.stringify({ error: "Failed to generate summary." })
26 );
27 }
28}
Now, let's create a prompt template.
1const TEMPLATE = `
2INSTRUCTIONS:
3 For the this {text} complete the following steps.
4 Generate the title for based on the content provided
5 Summarize the following content and include 5 key topics, writing in first person using normal tone of voice.
6
7 Write a youtube video description
8 - Include heading and sections.
9 - Incorporate keywords and key takeaways
10
11 Generate bulleted list of key points and benefits
12
13 Return possible and best recommended key words
14`;
You can change the template accordingly to suit your needs.
Let's use the generateSummary
function inside our POST
function. Update the code with the following.
The updated POST
function should look like the following.
1export async function POST(req: NextRequest) {
2 const user = await getUserMeLoader();
3 const token = await getAuthToken();
4
5 if (!user.ok || !token) {
6 return new Response(
7 JSON.stringify({ data: null, error: "Not authenticated" }),
8 { status: 401 }
9 );
10 }
11
12 if (user.data.credits < 1) {
13 return new Response(
14 JSON.stringify({
15 data: null,
16 error: "Insufficient credits",
17 }),
18 { status: 402 }
19 );
20 }
21
22 const body = await req.json();
23 const videoId = body.videoId;
24 const url = `https://deserving-harmony-9f5ca04daf.strapiapp.com/utilai/yt-transcript/${videoId}`;
25
26 let transcriptData;
27
28 try {
29 const transcript = await fetch(url);
30 transcriptData = await transcript.text();
31 } catch (error) {
32 console.error("Error processing request:", error);
33 if (error instanceof Error)
34 return new Response(JSON.stringify({ error: error.message }));
35 return new Response(JSON.stringify({ error: "Unknown error" }));
36 }
37
38 let summary: Awaited<ReturnType<typeof generateSummary>>;
39
40 try {
41 summary = await generateSummary(transcriptData, TEMPLATE);
42 return new Response(JSON.stringify({ data: summary, error: null }));
43 } catch (error) {
44 console.error("Error processing request:", error);
45 if (error instanceof Error)
46 return new Response(JSON.stringify({ error: error.message }));
47 return new Response(JSON.stringify({ error: "Error generating summary." }));
48 }
49}
In the code above, we implemented our generateSummary
function, which will generate our summary and send it back to you via our form. We will also create a server action responsible for saving our data into our Strapi backend.
The complete code in the route.ts
file should look like the following.
1import { NextRequest } from "next/server";
2import { getUserMeLoader } from "@/data/services/get-user-me-loader";
3import { getAuthToken } from "@/data/services/get-token";
4
5import { ChatOpenAI } from "@langchain/openai";
6import { PromptTemplate } from "@langchain/core/prompts";
7import { StringOutputParser } from "@langchain/core/output_parsers";
8
9const TEMPLATE = `
10INSTRUCTIONS:
11 For the this {text} complete the following steps.
12 Generate the title for based on the content provided
13 Summarize the following content and include 5 key topics, writing in first person using normal tone of voice.
14
15 Write a youtube video description
16 - Include heading and sections.
17 - Incorporate keywords and key takeaways
18
19 Generate bulleted list of key points and benefits
20
21 Return possible and best recommended key words
22`;
23
24async function generateSummary(content: string, template: string) {
25 const prompt = PromptTemplate.fromTemplate(template);
26
27 const model = new ChatOpenAI({
28 openAIApiKey: process.env.OPENAI_API_KEY,
29 modelName: process.env.OPENAI_MODEL ?? "gpt-4o-mini",
30 temperature: process.env.OPENAI_TEMPERATURE
31 ? parseFloat(process.env.OPENAI_TEMPERATURE)
32 : 0.7,
33 maxTokens: process.env.OPENAI_MAX_TOKENS
34 ? parseInt(process.env.OPENAI_MAX_TOKENS)
35 : 4000,
36 });
37
38 const outputParser = new StringOutputParser();
39 const chain = prompt.pipe(model).pipe(outputParser);
40
41 try {
42 const summary = await chain.invoke({ text: content });
43 return summary;
44 } catch (error) {
45 if (error instanceof Error)
46 return new Response(JSON.stringify({ error: error.message }));
47 return new Response(
48 JSON.stringify({ error: "Failed to generate summary." })
49 );
50 }
51}
52
53export async function POST(req: NextRequest) {
54 const user = await getUserMeLoader();
55 const token = await getAuthToken();
56
57 if (!user.ok || !token) {
58 return new Response(
59 JSON.stringify({ data: null, error: "Not authenticated" }),
60 { status: 401 }
61 );
62 }
63
64 if (user.data.credits < 1) {
65 return new Response(
66 JSON.stringify({
67 data: null,
68 error: "Insufficient credits",
69 }),
70 { status: 402 }
71 );
72 }
73
74 const body = await req.json();
75 const videoId = body.videoId;
76 const url = `https://deserving-harmony-9f5ca04daf.strapiapp.com/utilai/yt-transcript/${videoId}`;
77
78 let transcriptData;
79
80 try {
81 const transcript = await fetch(url);
82 transcriptData = await transcript.text();
83 } catch (error) {
84 console.error("Error processing request:", error);
85 if (error instanceof Error)
86 return new Response(JSON.stringify({ error: error.message }));
87 return new Response(JSON.stringify({ error: "Unknown error" }));
88 }
89
90 let summary: Awaited<ReturnType<typeof generateSummary>>;
91
92 try {
93 summary = await generateSummary(transcriptData, TEMPLATE);
94 return new Response(JSON.stringify({ data: summary, error: null }));
95 } catch (error) {
96 console.error("Error processing request:", error);
97 if (error instanceof Error)
98 return new Response(JSON.stringify({ error: error.message }));
99 return new Response(JSON.stringify({ error: "Error generating summary." }));
100 }
101}
Before we test our form, let's add our Open AI API key to our .env.local
file and add your Open AI API key.
WARNING: Ensure your
.gitignore
file ignores the.env*.local
file from your commit so you don't leak your Open AI key.
1# local env files
2.env*.local
OPENAI_API_KEY=ADD_YOUR_KEY_HERE
Let's test our form and see if we can get our summary. Make sure to add some credits to your user.
Excellent. We can see our output on the console.
1**Title:** Quickstart Guide to Launching Your Project with Strapi in Just 3 Minutes
2
3**YouTube Video Description:**
4
5**Heading:** Fast Track Your Development with Strapi: A 3-Minute Quickstart Guide
6
7**Introduction:**
8Join me today as we explore how to get your project up and running with Strapi in just three minutes! Strapi is an open-source headless CMS that simplifies the process of building, managing, and deploying content. Whether you're a developer, content creator, or project manager, this guide is designed to help you kickstart your project effortlessly.
9
10**Sections:**
11
12- **Setting Up Your Strapi Project:**
13 - Learn how to create a new Strapi project using the quickstart command to leverage default configurations, including setting up an SQLite database.
14
15Rest of summary...
Now that we know we are getting our summary, the last step is to save it to Strapi and deduct 1 credit.
First, create a new collection-type
in Strapi admin to save our summary.
Navigate to the content builder page and create a new collection named Summary
with the following fields.
Let's add the following fields.
Name | Field | Type |
---|---|---|
videoId | Text | Short Text |
title | Text | Short Text |
summary | Rich Text | Markdown |
authorId | Text | Short Text |
You can make a relations between the Summary and User collection types. But in this case we will only use the authorId
field to store the documentId
of the user who created the summary.
This way we don't need to expose the user
api which is something we would have to do if we were to make a relations between the Summary and User collection types.
So whenever we want to find all summaries for a user, we can simply query the Summary collection type and filter by the authorId
field.
Here is what the final fields will look like.
Now, navigate to Setting
and add the following permissions.
Now that we have our Summary collection-type
, let's create a server action to save our data to Strapi.
Let's start by navigating to srs/data/actions
, creating a new file called summary-actions.ts
, and adding the following code.
1"use server";
2
3import { getAuthToken } from "@/data/services/get-token";
4import { mutateData } from "@/data/services/mutate-data";
5import { redirect } from "next/navigation";
6
7interface Payload {
8 data: {
9 title?: string;
10 videoId: string;
11 summary: string;
12 };
13}
14
15export async function createSummaryAction(payload: Payload) {
16 const authToken = await getAuthToken();
17 if (!authToken) throw new Error("No auth token found");
18
19 const data = await mutateData("POST", "/api/summaries", payload);
20 redirect("/dashboard/summaries/" + data.data.documentId);
21}
Now that we have our createSummaryAction
, let's use it in our handleFormSubmit,
found in our form named summary-form.tsx
.
First, let's import our newly created action.
1import { createSummaryAction } from "@/data/actions/summary-actions";
Update the handleFormSubmit
with the following code.
1async function handleFormSubmit(event: React.FormEvent<HTMLFormElement>) {
2 event.preventDefault();
3 setLoading(true);
4
5 const formData = new FormData(event.currentTarget);
6 const videoId = formData.get("videoId") as string;
7
8 const processedVideoId = extractYouTubeID(videoId);
9
10 if (!processedVideoId) {
11 toast.error("Invalid Youtube Video ID");
12 setLoading(false);
13 setValue("");
14 setError({
15 ...INITIAL_STATE,
16 message: "Invalid Youtube Video ID",
17 name: "Invalid Id",
18 });
19 return;
20 }
21
22 toast.success("Generating Summary");
23
24 const summaryResponseData = await generateSummaryService(processedVideoId);
25
26 if (summaryResponseData.error) {
27 setValue("");
28 toast.error(summaryResponseData.error);
29 setError({
30 ...INITIAL_STATE,
31 message: summaryResponseData.error,
32 name: "Summary Error",
33 });
34 setLoading(false);
35 return;
36 }
37
38 const payload = {
39 data: {
40 title: `Summary for video: ${processedVideoId}`,
41 videoId: processedVideoId,
42 summary: summaryResponseData.data,
43 },
44 };
45
46 try {
47 await createSummaryAction(payload);
48 toast.success("Summary Created");
49 // Reset form after successful creation
50 setValue("");
51 setError(INITIAL_STATE);
52 } catch (error) {
53 let errorMessage =
54 "An unexpected error occurred while creating the summary";
55
56 if (error instanceof Error) {
57 errorMessage = error.message;
58 } else if (typeof error === "string") {
59 errorMessage = error;
60 }
61
62 toast.error(errorMessage);
63 setError({
64 message: errorMessage,
65 name: "Summary Error",
66 });
67 setLoading(false);
68 return;
69 }
70 setLoading(false);
71}
The above code will be responsible for saving our data into Strapi.
Let's do a quick test and see if it works. We should be redirected to our summaries
route, which we have yet to create, so we will get our not found page. This is okay, and we will fix it soon.
But you should see your data in your Strapi Admin.
You will notice that we are not setting our user or deducting one credit on creation. We will do this in Strapi by creating custom middleware. But first, let's finish all of our Next.js UI.
Let's navigate to our dashboard
folder. Inside, create another folder named summaries
with a page.tsx
file and paste it into the following code.
1import Link from "next/link";
2import { getSummaries } from "@/data/loaders";
3
4import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
5
6interface LinkCardProps {
7 documentId: string;
8 title: string;
9 summary: string;
10}
11
12function LinkCard({ documentId, title, summary }: Readonly<LinkCardProps>) {
13 return (
14 <Link href={`/dashboard/summaries/${documentId}`}>
15 <Card className="relative">
16 <CardHeader>
17 <CardTitle className="leading-8 text-pink-500">
18 {title || "Video Summary"}
19 </CardTitle>
20 </CardHeader>
21 <CardContent>
22 <p className="w-full mb-4 leading-7">
23 {summary.slice(0, 164) + " [read more]"}
24 </p>
25 </CardContent>
26 </Card>
27 </Link>
28 );
29}
30
31export default async function SummariesRoute() {
32 const { data } = await getSummaries();
33 if (!data) return null;
34 return (
35 <div className="grid grid-cols-1 gap-4 p-4">
36 <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
37 {data.map((item: LinkCardProps) => (
38 <LinkCard key={item.documentId} {...item} />
39 ))}
40 </div>
41 </div>
42 );
43}
Before this component works, we must create the getSummaries
function to load our data.
Let's navigate to our loaders.ts
file and make the following changes.
First, import the following method to give us access to our auth token for authorized requests.
1import { getAuthToken } from "./services/get-token";
Next, update the fetchData
function with the following code.
1async function fetchData(url: string) {
2 const authToken = await getAuthToken();
3
4 const headers = {
5 method: "GET",
6 headers: {
7 "Content-Type": "application/json",
8 Authorization: `Bearer ${authToken}`,
9 },
10 };
11
12 try {
13 const response = await fetch(url, authToken ? headers : {});
14 const data = await response.json();
15 return flattenAttributes(data);
16 } catch (error) {
17 console.error("Error fetching data:", error);
18 throw error;
19 }
20}
Now, let's add this code at the end of the file to get our summaries.
1export async function getSummaries() {
2 const url = new URL("/api/summaries", baseUrl);
3 return await fetchData(url.href);
4}
Now, we need to update the following permissions in the Strapi dashboard.
Under Settings
=> User Permissions
=> Roles
=> Authenticated
, set the Global and Home-page checkboxes to checked
.
Restart your application, and you should now be able to view the list.
Before we create the Single Card view, let's create a component that will display our markdown in a readable format.
We will use a package called react-markdown
to display our markdown. You can find it here.
And install it with the following command.
yarn add react-markdown
After installing react-markdown
, let's update our summaries/page.tsx
file to use it.
We will make the following change here.
1<CardContent>
2 <p className="w-full mb-4 leading-7">
3 {summary.slice(0, 164) + " [read more]"}
4 </p>
5</CardContent>
And update to the following.
1<CardContent>
2 <ReactMarkdown
3 className="card-markdown prose prose-sm max-w-none
4 prose-headings:text-gray-900 prose-headings:font-semibold
5 prose-p:text-gray-600 prose-p:leading-relaxed
6 prose-a:text-pink-500 prose-a:no-underline hover:prose-a:underline
7 prose-strong:text-gray-900 prose-strong:font-semibold
8 prose-ul:list-disc prose-ul:pl-4
9 prose-ol:list-decimal prose-ol:pl-4"
10 >
11 {summary.slice(0, 164) + " [read more]"}
12 </ReactMarkdown>
13</CardContent>
We are styling it with Tailwind in line. For this to work,you will need to install the @tailwindcss/typography
package.
yarn add @tailwindcss/typography
And update your tailwind.config.ts
in the plugins array with the following code.
1 plugins: [
2 require("tailwindcss-animate"),
3 require("@tailwindcss/typography"),
4 ],
It should now look a bit nicer, feel free to style it more. I will show you another example of this soon.
But first, let's create the Single Card view.
First, we will create a dynamic route; you can learn more about dynamic routes in Next.Js docs here.
"Dynamic Routes are pages that allow you to add custom params to your URLs."
Create a new folder called [videoId]
inside the summaries
folder.
Inside our newly created dynamic route, create a file named layout.tsx
with the following code.
1import { extractYouTubeID } from "@/lib/utils";
2
3export default async function SummarySingleRoute({
4 params,
5 children,
6}: {
7 readonly params: any;
8 readonly children: React.ReactNode;
9}) {
10 return (
11 <div>
12 <div className="h-full grid gap-4 grid-cols-5 p-4">
13 <div className="col-span-3">{children}</div>
14 <div className="col-span-2">
15 <div>
16 <p>Video will go here</p>
17 </div>
18 </div>
19 </div>
20 </div>
21 );
22}
Create another file called page.tsx
file with the following.
1interface ParamsProps {
2 params: {
3 videoId: string;
4 };
5}
6
7export default async function SummaryCardRoute({
8 params,
9}: Readonly<ParamsProps>) {
10 return <p>Summary card with go here: {params.videoId}</p>;
11}
The above code will give us a warning in Next 15.
Error: Route "/dashboard/summaries/[videoId]" used `params.videoId`. `params` should be awaited before using its properties. Learn more: https://nextjs.org/docs/messages/sync-dynamic-apis
You can read more about it here
Let's run the following command to fix it. Just make sure you save or stash all your changes.
npx @next/codemod@canary next-async-request-api
➜ frontend git:(main) npx @next/codemod@canary next-async-request-api
✔ On which files or directory should the codemods be applied? … .
Executing command: jscodeshift --no-babel --ignore-pattern=**/node_modules/** --ignore-pattern=**/.next/** --extensions=tsx,ts,jsx,js --transform /Users/paulbratslavsky/.npm/_npx/6a090669e21b4303/node_modules/@next/codemod/transforms/next-async-request-api.js .
Processing 53 files...
Spawning 7 workers...
Sending 8 files to free worker...
Sending 8 files to free worker...
Sending 8 files to free worker...
Sending 8 files to free worker...
Sending 8 files to free worker...
Sending 8 files to free worker...
Sending 5 files to free worker...
All done.
Results:
0 errors
52 unmodified
0 skipped
1 ok
Time elapsed: 0.479seconds
and refactor the code to the following.
1export default async function SummaryCardRoute(props: Readonly<ParamsProps>) {
2 const params = await props?.params;
3 const { videoId } = params;
4 return <p>Summary card with go here: {videoId}</p>;
5}
Now the warning should be gone. Let's continue.
Now when you click on the summary card, you should be navigated to the single summary view and see our placeholder text.
Now that we know our pages work, let's create the loaders to get the appropriate data.
Let's start by navigating our loaders.ts
file and adding the following functions.
1export async function getSummaryById(summaryId: string) {
2 return fetchData(`${baseUrl}/api/summaries/${summaryId}`);
3}
Now, before using our getSummaryById
function, let's install our video player. We will use React Player that you can find here.
Let's start by installing it with the following command.
yarn add react-player
Let's create a wrapper component using our React Player inside the components/custom
folder. Create a new folder called client-youtube-player
and inside create two files youtube-player.tsx
and index.tsx
and add the following code.
youtube-player.tsx
1"use client";
2
3import ReactPlayer from "react-player/youtube";
4
5function generateYouTubeUrl(videoId: string) {
6 const baseUrl = new URL("https://www.youtube.com/watch");
7 baseUrl.searchParams.append("v", videoId);
8 return baseUrl.href;
9}
10
11interface YouTubePlayerProps {
12 videoId: string;
13}
14
15export default function YouTubePlayer({
16 videoId,
17}: Readonly<YouTubePlayerProps>) {
18 if (!videoId) return null;
19 const videoUrl = generateYouTubeUrl(videoId);
20
21 return (
22 <div className="relative aspect-video rounded-md overflow-hidden">
23 <ReactPlayer
24 url={videoUrl}
25 width="100%"
26 height="100%"
27 controls
28 className="absolute top-0 left-0"
29 />
30 </div>
31 );
32}
index.tsx
1"use client";
2
3import dynamic from "next/dynamic";
4
5const YouTubePlayer = dynamic(
6 () => import("@/components/custom/client-youtube-player/youtube-player"),
7 { ssr: false }
8);
9
10export default function ClientYouTubePlayer({ videoId }: { videoId: string }) {
11 return <YouTubePlayer videoId={videoId} />;
12}
In the code above, we use dynamic
to disable SSR, which helps avoid issues when using some client-side components. In this blog post, you can read more here.
Next.js docs reference on solving hydration issues here
Now that we have our React Player let's update the layout.tsx
file using the following code.
1import { extractYouTubeID } from "@/lib/utils";
2import { getSummaryById } from "@/data/loaders";
3import ClientYouTubePlayer from "@/components/custom/client-youtube-player";
4
5export default async function SummarySingleRoute({
6 params,
7 children,
8}: {
9 readonly params: any;
10 readonly children: React.ReactNode;
11}) {
12 const { videoId } = await params;
13 const data = await getSummaryById(videoId);
14 if (data?.error?.status === 404) return <p>No Items Found</p>;
15 const videoYTId = extractYouTubeID(data.data.videoId);
16
17 return (
18 <div>
19 <div className="h-full grid gap-4 grid-cols-5 p-4">
20 <div className="col-span-3">{children}</div>
21 <div className="col-span-2">
22 <ClientYouTubePlayer videoId={videoYTId as string} />
23 </div>
24 </div>
25 </div>
26 );
27}
Now, let's display our summary.
Let's first create a new file called summary-card-form.tsx
. We can add it to our src/components/forms
folder and paste it into the following code.
We are using Tabs component to switch between the preview and markdown editor. So make sure to install it by running the following command.
npx shadcn@latest add tabs
1/// import { updateSummaryAction, deleteSummaryAction } from "@/data/actions/summary-actions";
2import { cn } from "@/lib/utils";
3
4import { Input } from "@/components/ui/input";
5import { Textarea } from "@/components/ui/textarea";
6
7import {
8 Card,
9 CardContent,
10 CardFooter,
11 CardHeader,
12 CardTitle,
13} from "@/components/ui/card";
14
15import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
16
17import { SubmitButton } from "@/components/custom/submit-button";
18import ReactMarkdown from "react-markdown";
19// import { DeleteButton } from "@/components/custom/delete-button";
20
21export function SummaryCardForm({
22 item,
23 className,
24}: {
25 readonly item: any;
26 readonly className?: string;
27}) {
28 // const deleteSummaryById = deleteSummaryAction.bind(null, item.documentId);
29
30 return (
31 <Card className={cn("mb-8 relative h-auto", className)}>
32 <CardHeader>
33 <CardTitle>Video Summary</CardTitle>
34 </CardHeader>
35 <CardContent>
36 <div>
37 <form>
38 <Input
39 id="title"
40 name="title"
41 placeholder="Update your title"
42 required
43 className="mb-4"
44 defaultValue={item.title}
45 />
46 <div className="flex-1 flex flex-col">
47 <Tabs
48 defaultValue="preview"
49 className="flex flex-col h-full gap-2"
50 >
51 <TabsList className="grid w-full grid-cols-2">
52 <TabsTrigger value="preview">Preview</TabsTrigger>
53 <TabsTrigger value="markdown">Edit Markdown</TabsTrigger>
54 </TabsList>
55 <TabsContent value="preview" className="flex-1">
56 <ReactMarkdown
57 className="
58 markdown-preview
59 relative w-full h-[600px]
60 overflow-auto scroll-smooth
61 p-4 px-3 py-2
62 text-sm
63 bg-white dark:bg-gray-800 bg-transparent
64 border border-gray-300 dark:border-gray-700
65 rounded-md
66 shadow-sm
67 mb-4
68 placeholder:text-muted-foreground
69 focus-visible:outline-none
70 focus-visible:bg-gray-50
71 focus-visible:ring-1
72 focus-visible:ring-ring
73 disabled:cursor-not-allowed
74 disabled:opacity-50
75 "
76 >
77 {item.summary}
78 </ReactMarkdown>
79 </TabsContent>
80 <TabsContent value="markdown" className="flex-1">
81 <Textarea
82 name="summary"
83 className="
84 markdown-preview
85 relative w-full h-[600px]
86 overflow-auto scroll-smooth
87 p-4 px-3 py-2
88 text-sm
89 bg-white dark:bg-gray-800 bg-transparent
90 border border-gray-300 dark:border-gray-700
91 rounded-md
92 shadow-sm
93 mb-4
94 placeholder:text-muted-foreground
95 focus-visible:outline-none
96 focus-visible:bg-gray-50
97 focus-visible:ring-1
98 focus-visible:ring-ring
99 disabled:cursor-not-allowed
100 disabled:opacity-50
101 "
102 defaultValue={item.summary}
103 />
104 </TabsContent>
105 </Tabs>
106 </div>
107 <input type="hidden" name="id" value={item.documentId} />
108 <SubmitButton
109 text="Update Summary"
110 loadingText="Updating Summary"
111 />
112 </form>
113 <form>
114 {/* <DeleteButton className="absolute right-4 top-4 bg-red-700 hover:bg-red-600" /> */}
115 </form>
116 </div>
117 </CardContent>
118 <CardFooter></CardFooter>
119 </Card>
120 );
121}
Keep in mind that in the ReactMarkdown component, we are passing tailwind classes for basic styling.
We are also passing a class name markdown-preview
to the ReactMarkdown component.
In order to get the styling, we will need to add the following to our globals.css
file.
1/* ************************** */
2/* markdown preview start */
3/* ************************** */
4
5.markdown-preview {
6 @apply text-base;
7 @apply overflow-auto;
8}
9
10.markdown-preview h1 {
11 @apply text-3xl font-bold mt-6 mb-4;
12}
13
14.markdown-preview h2 {
15 @apply text-2xl font-semibold mt-5 mb-3;
16}
17
18.markdown-preview h3 {
19 @apply text-xl font-medium mt-4 mb-2;
20}
21
22.markdown-preview p {
23 @apply mb-4;
24}
25
26.markdown-preview ul,
27.markdown-preview ol {
28 @apply ml-6 mb-4;
29}
30
31.markdown-preview ul {
32 @apply list-disc;
33}
34
35.markdown-preview ol {
36 @apply list-decimal;
37}
38
39.markdown-preview li {
40 @apply mb-2;
41}
42
43.markdown-preview a {
44 @apply text-blue-600 hover:underline;
45}
46
47.markdown-preview blockquote {
48 @apply border-l-4 border-gray-300 pl-4 italic my-4;
49}
50
51.markdown-preview code {
52 @apply bg-gray-100 rounded px-1 py-0.5 font-mono text-sm;
53}
54
55.markdown-preview pre {
56 @apply bg-gray-100 rounded p-4 overflow-x-auto mb-4;
57}
58
59.markdown-preview pre code {
60 @apply bg-transparent p-0;
61}
62
63.markdown-preview table {
64 @apply w-full border-collapse mb-4;
65}
66
67.markdown-preview th,
68.markdown-preview td {
69 @apply border border-gray-300 px-4 py-2;
70}
71
72.markdown-preview th {
73 @apply bg-gray-100 font-semibold;
74}
75
76.markdown-preview img {
77 @apply max-w-full h-auto my-4;
78}
79
80.markdown-preview hr {
81 @apply my-8 border-t border-gray-300;
82}
83
84/* ************************** */
85/* markdown preview end */
86/* ************************** */
Nice.
Also, notice that we are using a new component, DeleteButton. Let's create it inside our components/custom
folder. Create a delete-button.tsx
file and add the following code.
1"use client";
2import { useFormStatus } from "react-dom";
3import { cn } from "@/lib/utils";
4import { Button } from "@/components/ui/button";
5import { TrashIcon } from "lucide-react";
6import { Loader2 } from "lucide-react";
7
8function Loader() {
9 return (
10 <div className="flex items-center">
11 <Loader2 className="h-4 w-4 animate-spin" />
12 </div>
13 );
14}
15
16interface DeleteButtonProps {
17 className?: string;
18}
19
20export function DeleteButton({ className }: Readonly<DeleteButtonProps>) {
21 const status = useFormStatus();
22 return (
23 <Button
24 type="submit"
25 aria-disabled={status.pending}
26 disabled={status.pending}
27 className={cn(className)}
28 >
29 {status.pending ? <Loader /> : <TrashIcon className="w-4 h-4" />}
30 </Button>
31 );
32}
Let's update our page.tsx
file with the following code.
1import { getSummaryById } from "@/data/loaders";
2import { SummaryCardForm } from "@/components/custom/summary-card-form";
3
4interface ParamsProps {
5 params: {
6 videoId: string;
7 };
8}
9
10export default async function SummaryCardRoute(props: Readonly<ParamsProps>) {
11 const params = await props?.params;
12 const { videoId } = params;
13 const data = await getSummaryById(videoId);
14 return <SummaryCardForm item={data.data} />;
15}
Nice. Everything works.
When creating our summary, we will set the summary/user relation on the backend, where we can confirm the logged-in user creating the content.
This will prevent anyone from the front end from passing a user ID that is not their own.
We are also not handling user credit updates; let's do that in the middleware.
What is a route middleware
In Strapi, a route middleware is a type of middleware that has a more limited scope compared to global middlewares.
They control the request flow and can change the request itself before moving forward.
Now that we know our front end works. Let's revisit our route handler. They can also be used to control access to a route and perform additional logic.
For example, they can be used instead of policies to control access to an endpoint. They could modify the context before passing it down to further core elements of the Strapi server.
You can learn more about route middlewares here.
Let's first start by creating our route middleware.
We can use our cli command. In your backend
folder, run the following command.
yarn strapi generate
Choose to generate a middleware option.
➜ backend git:(main) ✗ yarn strapi generate
yarn run v1.22.22
$ strapi generate
? Strapi Generators
api - Generate a basic API
controller - Generate a controller for an API
content-type - Generate a content type for an API
policy - Generate a policy for an API
❯ middleware - Generate a middleware for an API
migration - Generate a migration
service - Generate a service for an API
Let's call it on-summary-create
and add it to an existing API. Which will be summary
? Strapi Generators middleware - Generate a middleware for an API
? Middleware name on-summary-create
? Where do you want to add this middleware?
Add middleware to root of project
❯ Add middleware to an existing API
Add middleware to an existing plugin
$ strapi generate
? Strapi Generators middleware - Generate a middleware for an API
? Middleware name on-summary-create
? Where do you want to add this middleware? Add middleware to an existing API
? Which API is this for?
global
home-page
❯ summary
Now, let's take a look in the following folder: backend/src/api/summary/middlewares.
You should see the following file: on-summary-create
with the following boilerplate.
1/**
2 * `on-summary-create` middleware
3 */
4
5import type { Core } from "@strapi/strapi";
6
7export default (config, { strapi }: { strapi: Core.Strapi }) => {
8 // Add your own logic here.
9 return async (ctx, next) => {
10 strapi.log.info("In on-summary-create middleware.");
11
12 await next();
13 };
14};
Let's update it with the following code.
1/**
2 * `on-summary-create` middleware
3 */
4
5import type { Core } from "@strapi/strapi";
6
7export default (config, { strapi }: { strapi: Core.Strapi }) => {
8 return async (ctx, next) => {
9 const user = ctx.state.user;
10 if (!user) return ctx.unauthorized("You are not authenticated");
11
12 const availableCredits = user.credits;
13 if (availableCredits === 0)
14 return ctx.unauthorized("You do not have enough credits.");
15
16 console.log("############ Inside middleware end #############");
17
18 // ADD THE AUTHOR ID TO THE BODY
19 const modifiedBody = {
20 ...ctx.request.body,
21 data: {
22 ...ctx.request.body.data,
23 authorId: ctx.state.user.documentId,
24 },
25 };
26
27 ctx.request.body = modifiedBody;
28
29 await next();
30
31 // UPDATE THE USER'S CREDITS
32 try {
33 await strapi.documents("plugin::users-permissions.user").update({
34 documentId: user.documentId,
35 data: {
36 credits: availableCredits - 1,
37 },
38 });
39 } catch (error) {
40 ctx.badRequest("Error Updating User Credits");
41 }
42
43 console.log("############ Inside middleware end #############");
44 };
45};
In the code above, we add the authorId to the body and deduct one credit from the user.
Before testing it out, we have to enable it inside our route.
You can learn more about Strapi's routes here.
Navigate to the backend/src/api/summary/routes/summary.js
file and update with the following.
1/**
2 * summary router
3 */
4
5import { factories } from "@strapi/strapi";
6
7export default factories.createCoreRouter("api::summary.summary", {
8 config: {
9 create: {
10 middlewares: ["api::summary.on-summary-create"],
11 },
12 },
13});
Now, our middleware will fire when we create a new summary.
Now, restart your Strapi backend and Next.js frontend and create a new summary.
You will see that we are now setting our user data.
In this part of our Next.js 15 tutorial series, we tackled generating video summaries using Open AI and LangChain, a highlight feature for our Next.js app.
We built a SummaryForm component to handle user submissions and explored Next.js API routes for server-side logic.
We then leveraged OpenAI to summarize video transcripts, demonstrating the practical use of AI in web development.
In the next post, we will examine our summary details page and discuss updating and deleting posts.
As well as how to add policies to ensure that our user can only modify their content.
Hope you are enjoying this series as much as I am making it.
If you have any questions, feel free to stop by at our Discord Community for our daily "open office hours" from 12:30 PM CST to 1:30 PM CST.
If you have a suggestion or find a mistake in the post, please open an issue on the GitHub repository.
You can also find the blog post content in the Strapi Blog.
Feel free to make PRs to fix any issues you find in the project, or let me know if you have any questions.
Happy coding!