I’m Building an AI-Powered Blog: Here’s How…

I’m Building an AI-Powered Blog: Here’s How…

The AI era is upon us. To get ahead as devs, it’s great to have some AI-powered projects in your portfolio.

Today, we will go through building an AI-powered blog platform with some awesome functionality such as research, autocomplete and a Copilot.

I built an initial version of this project here. A commenter had some really cool suggestions for taking it to the next level.

So we decided to build it!

TL;DR

We’re building an AI-powered blogging platform Pt. II

CopilotKit: The framework for building in-app AI copilots

CopilotKit is an open-source AI copilot platform. We make it easy to integrate powerful AI into your React apps.

Build:

ChatBot: Context-aware in-app chatbots that can take actions in-app 💬
CopilotTextArea: AI-powered textFields with context-aware autocomplete & insertions 📝
Co-Agents: In-app AI agents that can interact with your app & users 🤖

Star CopilotKit ⭐️

Now back to the article!

Prerequisites

To fully understand this tutorial, you need to have a basic understanding of React or Next.js.

Here are the tools required to build the AI-powered blog:

Quill Rich Text Editor – a text editor that enables you to easily format text, add images, add code, and create custom, interactive content in your web app.

Supabase – a PostgreSQL hosting service that provides you with all the backend features you need for your project.

Langchain – provides a framework that enables AI agents to search the web and research any topic.

OpenAI API – provides an API key that enables you to carry out various tasks using ChatGPT models.

Tavily AI – a search engine that enables AI agents to conduct research and access real-time knowledge within the application.

CopilotKit – an open-source copilot framework for building custom AI chatbots, in-app AI agents, and text areas.

Project Set up and Package Installation

First, create a Next.js application by running the code snippet below in your terminal:

npx createnextapp@latest aiblogapp

Select your preferred configuration settings. For this tutorial, we’ll be using TypeScript and Next.js App Router.

Next, install Quill rich text editor, Supabase, and Langchain packages and their dependencies.

npm install quill reactquill @supabase/supabase-js @supabase/ssr @supabase/auth-helpers-nextjs @langchain/langgraph

Finally, install the CopilotKit packages. These packages enable us to retrieve data from the React state and add AI copilot to the application.

npm install @copilotkit/react-ui @copilotkit/reacttextarea @copilotkit/react-core @copilotkit/backend

Congratulations! You’re now ready to build an AI-powered blog.

Building The Blog Frontend

In this section, I will walk you through the process of creating the blog’s frontend with static content to define the blog’s user interface.

The blog’s frontend will consist of four pages: the home page, post page, create post page, and login/sign up page.

To get started, go to /[root]/src/app in your code editor and create a folder called components. Inside the components folder, create five files named Header.tsx, Posts.tsx, Post.tsx, Comment.tsx and QuillEditor.tsx

In the Header.tsx file, add the following code that defines a functional component named Header that will render the blog’s navbar.

use client;

import Link from next/link;

export default function Header() {
return (
<>
<header className=“flex flex-wrap sm:justify-start sm:flex-nowrap z-50 w-full bg-gray-800 border-b border-gray-200 text-sm py-3 sm:py-0 “>
<nav
className=“relative max-w-7xl w-full mx-auto px-4 sm:flex sm:items-center sm:justify-between sm:px-6 lg:px-8”
aria-label=“Global”>
<div className=“flex items-center justify-between”>
<Link
className=“flex-none text-xl text-white font-semibold “
href=“/”
aria-label=“Brand”>
AIBlog
</Link>
</div>
<div id=“navbar-collapse-with-animation” className=“”>
<div className=“flex flex-col gap-y-4 gap-x-0 mt-5 sm:flex-row sm:items-center sm:justify-end sm:gap-y-0 sm:gap-x-7 sm:mt-0 sm:ps-7”>
<Link
className=“flex items-center font-medium text-gray-500 border-2 border-indigo-600 text-center p-2 rounded-md hover:text-blue-600 sm:border-s sm:my-6 “
href=“/createpost”>
Create Post
</Link>

<form action={“”}>
<button
className=“flex items-center font-medium text-gray-500 border-2 border-indigo-600 text-center p-2 rounded-md hover:text-blue-600 sm:border-s sm:my-6 “
type=“submit”>
Logout
</button>
</form>

<Link
className=“flex items-center font-medium text-gray-500 border-2 border-indigo-600 text-center p-2 rounded-md hover:text-blue-600 sm:border-s sm:my-6 “
href=“/login”>
Login
</Link>
</div>
</div>
</nav>
</header>
</>
);
}

In the Posts.tsx file, add the following code that defines a functional component named Posts that renders the blogging platform homepage that will display a list of published articles.

use client;

import React, { useEffect, useState } from react;
import Image from next/image;
import Link from next/link;

export default function Posts() {
const [articles, setArticles] = useState<any[]>([]);

return (
<div className=“max-w-[85rem] h-full px-4 py-10 sm:px-6 lg:px-8 lg:py-14 mx-auto”>
<div className=“grid sm:grid-cols-2 lg:grid-cols-3 gap-6”>
<Link
key={“”}
className=“group flex flex-col h-full bg-gray-800 border border-gray-200 hover:border-transparent hover:shadow-lg transition-all duration-300 rounded-xl p-5 “
href=“”>
<div className=“aspect-w-16 aspect-h-11”>
<Image
className=“object-cover h-48 w-96 rounded-xl”
src={`https://source.unsplash.com/featured/?${encodeURIComponent(
Hello World
)}`}
width={500}
height={500}
alt=“Image Description”
/>
</div>
<div className=“my-6”>
<h3 className=“text-xl font-semibold text-white “>Hello World</h3>
</div>
</Link>
</div>
</div>
);
}

In the QuillEditor.tsx file, add the following code that dynamically imports the QuillEditor component, defines modules configuration for the Quill editor toolbar, and defines text formats for the Quill editor.

// Import the dynamic function from the “next/dynamic” package
import dynamic from next/dynamic;
// Import the CSS styles for the Quill editor’s “snow” theme
import react-quill/dist/quill.snow.css;

// Export a dynamically imported QuillEditor component
export const QuillEditor = dynamic(() => import(react-quill), { ssr: false });

// Define modules configuration for the Quill editor toolbar
export const quillModules = {
toolbar: [
// Specify headers with different levels
[{ header: [1, 2, 3, false] }],
// Specify formatting options like bold, italic, etc.
[bold, italic, underline, strike, blockquote],
// Specify list options: ordered and bullet
[{ list: ordered }, { list: bullet }],
// Specify options for links and images
[link, image],
// Specify alignment options
[{ align: [] }],
// Specify color options
[{ color: [] }],
// Specify code block option
[code-block],
// Specify clean option for removing formatting
[clean],
],
};

// Define supported formats for the Quill editor
export const quillFormats = [
header,
bold,
italic,
underline,
strike,
blockquote,
list,
bullet,
link,
image,
align,
color,
code-block,
];

In the Post.tsx file, add the following code that defines a functional component named CreatePost that will be used to render the article creation form.

use client;

// Importing React hooks and components
import { useRef, useState } from react;
import { QuillEditor } from ./QuillEditor;
import { quillModules } from ./QuillEditor;
import { quillFormats } from ./QuillEditor;
import react-quill/dist/quill.snow.css;

// Define the CreatePost component
export default function CreatePost() {
// Initialize state variables for article outline, copilot text, and article title
const [articleOutline, setArticleOutline] = useState(“”);
const [copilotText, setCopilotText] = useState(“”);
const [articleTitle, setArticleTitle] = useState(“”);

// State variable to track if research task is running
const [publishTaskRunning, setPublishTaskRunning] = useState(false);

// Handle changes to the editor content
const handleEditorChange = (newContent: any) => {
setCopilotText(newContent);
};

return (
<>
{/* Main */}
<div className=“p-3 max-w-3xl mx-auto min-h-screen”>
<h1 className=“text-center text-white text-3xl my-7 font-semibold”>
Create a post
</h1>

{/* Form for creating a post */}
<form action={“”} className=“flex flex-col gap-4 mb-2 mt-2”>
<div className=“flex flex-col gap-4 sm:flex-row justify-between mb-2”>
{/* Input field for article title */}
<input
type=“text”
id=“title”
name=“title”
placeholder=“Title”
value={articleTitle}
onChange={(event) => setArticleTitle(event.target.value)}
className=“flex-1 block w-full rounded-lg border text-sm border-gray-600 bg-gray-700 text-white placeholder-gray-400 focus:border-cyan-500 focus:ring-cyan-500”
/>
</div>

{/* Hidden textarea for article content */}
<textarea
className=“p-4 w-full aspect-square font-bold text-xl bg-slate-800 text-white rounded-lg resize-none hidden”
id=“content”
name=“content”
value={copilotText}
placeholder=“Write your article content here”
onChange={(event) => setCopilotText(event.target.value)}
/>

{/* Quill editor component */}
<QuillEditor
onChange={handleEditorChange}
modules={quillModules}
formats={quillFormats}
className=“h-80 mb-12 text-white”
/>
{/* Submit button for publishing the post */}
<button
type=“submit”
disabled={publishTaskRunning}
className={`bg-blue-500 text-white font-bold py-2 px-4 rounded ${
publishTaskRunning
? opacity-50 cursor-not-allowed
: hover:bg-blue-700
}`}
onClick={async () => {
try {
setPublishTaskRunning(true);
} finally {
setPublishTaskRunning(false);
}
}}>
{publishTaskRunning ? Publishing… : Publish}
</button>
</form>
</div>
</>
);
}

In the Comment.tsx file, add the following code that defines a functional component called Comment that renders post comment form and post comments.

// Client-side rendering
use client;

// Importing React and Next.js components
import React, { useEffect, useRef, useState } from react;
import Image from next/image;

// Define the Comment component
export default function Comment() {
// State variables for comment, comments, and article content
const [comment, setComment] = useState(“”);
const [comments, setComments] = useState<any[]>([]);
const [articleContent, setArticleContent] = useState(“”);

return (
<div className=“max-w-2xl mx-auto w-full p-3”>
{/* Form for submitting a comment */}
<form action={“”} className=“border border-teal-500 rounded-md p-3 mb-4”>
{/* Textarea for entering a comment */}
<textarea
id=“content”
name=“content”
placeholder=“Add a comment…”
rows={3}
onChange={(e) => setComment(e.target.value)}
value={comment}
className=“hidden”
/>

{/* Submit button */}
<div className=“flex justify-between items-center mt-5”>
<button
type=“submit”
className=“bg-blue-500 text-white font-bold py-2 px-4 rounded”>
Submit
</button>
</div>
</form>

{/* Comments section */}
<p className=“text-white mb-2”>Comments:</p>

{/* Comment item (currently hardcoded) */}
<div key={“”} className=“flex p-4 border-b dark:border-gray-600 text-sm”>
<div className=“flex-shrink-0 mr-3”>
{/* Profile picture */}
<Image
className=“w-10 h-10 rounded-full bg-gray-200”
src={`(link unavailable){encodeURIComponent(
“Silhouette”
)}`
}
width={500}
height={500}
alt=“Profile Picture”
/>
</div>
<div className=“flex-1”>
<div className=“flex items-center mb-1”>
{/* Username (currently hardcoded as “Anonymous”) */}
<span className=“font-bold text-white mr-1 text-xs truncate”>
Anonymous
</span>
</div>
{/* Comment text (currently hardcoded as “No Comments”) */}
<p className=“text-gray-500 pb-2”>No Comments</p>
</div>
</div>
</div>
);
}

Next, go to /[root]/src/app and create a folder called [slug]. Inside the [slug] folder, create a page.tsx file.

Then add the following code into the file that imports Comment and Header components and defines a functional component named Post that will render the navbar, post content, comment form, and post comments.

import Header from ../components/Header;
import Comment from ../components/Comment;

export default async function Post() {
return (
<>
<Header />
<main className=“p-3 flex flex-col max-w-6xl mx-auto min-h-screen”>
<h1 className=“text-3xl text-white mt-10 p-3 text-center font-serif max-w-2xl mx-auto lg:text-4xl”>
Hello World
</h1>
<div className=“flex justify-between text-white p-3 border-b border-slate-500 mx-auto w-full max-w-2xl text-xs”>
<span></span>
<span className=“italic”>0 mins read</span>
</div>
<div className=“p-3 max-w-2xl text-white mx-auto w-full post-content border-b border-slate-500 mb-2”>
No Post Content
</div>
<Comment />
</main>
</>
);
}

After that, go to /[root]/src/app and create a folder called createpost. Inside the createpost folder, create a file called page.tsx file.

Then add the following code into the file that imports CreatePost and Header components and defines a functional component named WriteArticle that will render the navbar and the post creation form.

import CreatePost from ../components/Post;
import Header from ../components/Header;
import { redirect } from next/navigation;

export default async function WriteArticle() {
return (
<>
<Header />
<CreatePost />
</>
);
}

Next, go to /[root]/src/page.tsx file, and add the following code that imports Posts and Header components and defines a functional component named Home.

import Header from ./components/Header;
import Posts from ./components/Posts;

export default async function Home() {
return (
<>
<Header />
<Posts />
</>
);
}

After that, go to the next.config.mjs file and rename it to next.config.js . Then add the following code that allows you to use images from Unsplash as cover images for the published articles.

module.exports = {
images: {
remotePatterns: [
{
protocol: https,
hostname: source.unsplash.com,
},
],
},
};

Next, remove the CSS code in the globals.css file and add the following CSS code.

@tailwind base;
@tailwind components;
@tailwind utilities;

body {
height: 100vh;
background-color: rgb(16, 23, 42);
}

.ql-editor {
font-size: 1.25rem;
}

.post-content p {
margin-bottom: 0.5rem;
}

.post-content h1 {
font-size: 1.5rem;
font-weight: 600;
font-family: sans-serif;
margin: 1.5rem 0;
}

.post-content h2 {
font-size: 1.4rem;
font-family: sans-serif;
margin: 1.5rem 0;
}

.post-content a {
color: rgb(73, 149, 199);
text-decoration: none;
}

.post-content a:hover {
text-decoration: underline;
}

Finally, run the command npm run dev on the command line and then navigate to http://localhost:3000/.

Now you should view the blogging platform frontend on your browser, as shown below.

Integrating AI Functionalities To The Blog Using CopilotKit

In this section, you will learn how to add an AI copilot to the blog to perform blog topic research and content autosuggestions using CopilotKit.

CopilotKit offers both frontend and backend packages. They enable you to plug into the React states and process application data on the backend using AI agents.

First, let’s add the CopilotKit React components to the blog frontend.

Adding CopilotKit to the Blog Frontend

Here, I will walk you through the process of integrating the blog with the CopilotKit frontend to facilitate blog article research and article outline generation.

To get started, use the code snippet below to import useMakeCopilotReadable, useCopilotAction, CopilotTextarea, and HTMLCopilotTextAreaElement custom hooks at the top of the /[root]/src/app/components/Post.tsx file.

import {
useMakeCopilotReadable,
useCopilotAction,
} from @copilotkit/react-core;
import {
CopilotTextarea,
HTMLCopilotTextAreaElement,
} from @copilotkit/react-textarea;

Inside the CreatePost function, below the state variables, add the following code that uses the useMakeCopilotReadable hook to add the article outline that will be generated as context for the in-app chatbot. The hook makes the article outline readable to the copilot.

useMakeCopilotReadable(
Blog article outline: + JSON.stringify(articleOutline)
);

Below the useMakeCopilotReadable hook, use the code below to create a reference called copilotTextareaRef to a textarea element called HTMLCopilotTextAreaElement.

const copilotTextareaRef = useRef<HTMLCopilotTextAreaElement>(null);

Below the code above, add the following code that uses the useCopilotAction hook to set up an action called researchBlogArticleTopic which will enable research on a given topic for a blog article.

The action takes in two parameters called articleTitle and articleOutline which enables generation of an article title and outline.

The action contains a handler function that generates an article title and outline based on a given topic.

Inside the handler function, articleOutline state is updated with the newly generated outline while articleTitle state is updated with the newly generated title, as shown below.

// Define a Copilot action
useCopilotAction(
{
// Action name and description
name: researchBlogArticleTopic,
description: Research a given topic for a blog article.,

// Parameters for the action
parameters: [
{
// Parameter 1: articleTitle
name: articleTitle,
type: string,
description: Title for a blog article.,
required: true, // This parameter is required
},
{
// Parameter 2: articleOutline
name: articleOutline,
type: string,
description: Outline for a blog article that shows what the article covers.,
required: true, // This parameter is required
},
],

// Handler function for the action
handler: async ({ articleOutline, articleTitle }) => {
// Set the article outline and title using state setters
setArticleOutline(articleOutline);
setArticleTitle(articleTitle);
},
},
[] // Dependencies (empty array means no dependencies)
);

Below the code above, go to the form component and add the following CopilotTextarea component that will enable you to add text completions, insertions, and edits to your article content.

<CopilotTextarea
className=“p-4 h-72 w-full rounded-lg mb-2 border text-sm border-gray-600 bg-gray-700 text-white placeholder-gray-400 focus:border-cyan-500 focus:ring-cyan-500 resize-none”
ref={copilotTextareaRef}
placeholder=“Start typing for content autosuggestion.”
value={articleOutline}
rows={5}
autosuggestionsConfig={{
textareaPurpose: articleTitle,
chatApiConfigs: {
suggestionsApiConfig: {
forwardedParams: {
max_tokens: 5,
stop: [n, ., ,],
},
},
insertionApiConfig: {},
},
debounceTime: 250,
}}
/>

After that, go to /[root]/src/app/createpost/page.tsx file and import CopilotKit frontend packages and styles at the top using the code below.

import { CopilotKit } from @copilotkit/react-core;
import { CopilotPopup } from @copilotkit/react-ui;
import @copilotkit/react-ui/styles.css;

Then use CopilotKit to wrap the CopilPopup and CreatePost components, as shown below. The CopilotKit component specifies the URL for CopilotKit’s backend endpoint (/api/copilotkit/) while the CopilotPopup renders the in-app chatbot that you can give prompts to research any topic for an article.

export default async function WriteArticle() {
return (
<>
<Header />
<CopilotKit url=“/api/copilotkit”>
<CopilotPopup
instructions=“Help the user research a blog article topic.”
defaultOpen={true}
labels={{
title: Blog Article Research AI Assistant,
initial:
Hi! 👋 I can help you research any topic for a blog article.,
}}
/>
<CreatePost />
</CopilotKit>
</>
);
}

After that, use the code snippet below to import useMakeCopilotReadable, CopilotKit, CopilotTextarea, and HTMLCopilotTextAreaElement custom hooks at the top of the /[root]/src/app/components/Comment.tsx file.

import { useMakeCopilotReadable, CopilotKit } from @copilotkit/react-core;
import {
CopilotTextarea,
HTMLCopilotTextAreaElement,
} from @copilotkit/react-textarea;

Inside the Comment function, below the state variables, add the following code that uses the useMakeCopilotReadable hook to add the post content as context for the comment content autosuggestion.

useMakeCopilotReadable(
Blog article content: + JSON.stringify(articleContent)
);

const copilotTextareaRef = useRef<HTMLCopilotTextAreaElement>(null);

Then add the CopilotTextarea component to the comment form and use CopilotKit to wrap the form, as shown below.

<CopilotKit url=“/api/copilotkit”>
<form
action={“”}
className=“border border-teal-500 rounded-md p-3 mb-4”>
<textarea
id=“content”
name=“content”
placeholder=“Add a comment…”
rows={3}
onChange={(e) => setComment(e.target.value)}
value={comment}
className=“hidden”
/>

<CopilotTextarea
className=“p-4 w-full rounded-lg mb-2 border text-sm border-gray-600 bg-gray-700 text-white placeholder-gray-400 focus:border-cyan-500 focus:ring-cyan-500 resize-none”
ref={copilotTextareaRef}
placeholder=“Start typing for content autosuggestion.”
onChange={(event) => setComment(event.target.value)}
rows={5}
autosuggestionsConfig={{
textareaPurpose: articleContent,
chatApiConfigs: {
suggestionsApiConfig: {
forwardedParams: {
max_tokens: 5,
stop: [n, ., ,],
},
},
insertionApiConfig: {},
},
debounceTime: 250,
}}
/>

<div className=“flex justify-between items-center mt-5”>
<button
type=“submit”
className=“bg-blue-500 text-white font-bold py-2 px-4 rounded”>
Submit
</button>
</div>
</form>
</CopilotKit>

After that, run the development server and navigate to http://localhost:3000/createpost. You should see that the popup in-app chatbot and CopilotTextarea were integrated into the Blog.

Congratulations! You’ve successfully added CopilotKit to the Blog Frontend*.*

Adding CopilotKit Backend to the Blog

Here, I will walk you through the process of integrating the blog with the CopilotKit backend that handles requests from frontend, and provides function calling and various LLM backends such as GPT.

Also, we will integrate an AI agent named Tavily that can research any topic on the web.

To get started, create a file called .env.local in the root directory. Then add the environment variables below in the file that hold your ChatGPT and Tavily Search API keys.

OPENAI_API_KEY=”Your ChatGPT API key”
TAVILY_API_KEY=”Your Tavily Search API key”

To get the ChatGPT API key, navigate to https://platform.openai.com/api-keys.

To get the Tavily Search API key, navigate to https://app.tavily.com/home

After that, go to /[root]/src/app and create a folder called api. In the api folder, create a folder called copilotkit.

In the copilotkit folder, create a file called research.ts. Then Navigate to this research.ts gist file, copy the code, and add it to the research.ts file

Next, create a file called route.ts in the /[root]/src/app/api/copilotkit folder. The file will contain code that sets up a backend functionality to process POST requests. It conditionally includes a “research” action that performs research on a given topic.

Now import the following modules at the top of the file.

import { CopilotBackend, OpenAIAdapter } from @copilotkit/backend; // For backend functionality with CopilotKit.
import { researchWithLangGraph } from ./research; // Import a custom function for conducting research.
import { AnnotatedFunction } from @copilotkit/shared; // For annotating functions with metadata.

Below the code above, define a runtime environment variable and a function named researchAction that researches a certain topic using the code below.

// Define a runtime environment variable, indicating the environment where the code is expected to run.
export const runtime = edge;

// Define an annotated function for research. This object includes metadata and an implementation for the function.
const researchAction: AnnotatedFunction<any> = {
name: research, // Function name.
description: Call this function to conduct research on a certain topic. Respect other notes about when to call this function, // Function description.
argumentAnnotations: [ // Annotations for arguments that the function accepts.
{
name: topic, // Argument name.
type: string, // Argument type.
description: The topic to research. 5 characters or longer., // Argument description.
required: true, // Indicates that the argument is required.
},
],
implementation: async (topic) => { // The actual function implementation.
console.log(Researching topic: , topic); // Log the research topic.
return await researchWithLangGraph(topic); // Call the research function and return its result.
},
};

Then add the code below under the code above to define an asynchronous function that handles POST requests.

// Define an asynchronous function that handles POST requests.
export async function POST(req: Request): Promise<Response> {
const actions: AnnotatedFunction<any>[] = []; // Initialize an array to hold actions.

// Check if a specific environment variable is set, indicating access to certain functionality.
if (process.env[TAVILY_API_KEY]) {
actions.push(researchAction); // Add the research action to the actions array if the condition is true.
}

// Instantiate CopilotBackend with the actions defined above.
const copilotKit = new CopilotBackend({
actions: actions,
});

// Use the CopilotBackend instance to generate a response for the incoming request using an OpenAIAdapter.
return copilotKit.response(req, new OpenAIAdapter());
}

Congratulations! You’ve successfully added the CopilotKit backend to the Blog*.*

Integrating Database to the Blog Using Supabase

In this section, I will walk you through the process of integrating the blog with the Supabase database to insert and fetch blog article and comments data.

To get started, navigate to supabase.com and click the Start your project button on the home page.

Then create a new project called AiBloggingPlatform, as shown below.

Once the project is created, add your Supabase URL and API key to environment variables in the env.local file, as shown below.

NEXT_PUBLIC_SUPABASE_URL=Your Supabase URL
NEXT_PUBLIC_SUPABASE_ANON_KEY=Your Supabase API Key

Setting up Supabase Authentication for the blog

Here, I will walk you through the process of setting up authentication for the blog that enables users to sign up, log in, or log out.

To get started, go to /[root]/src/ and create a folder named utils. Inside the utils folder, create a folder named supabase.

Then create two files named client.ts and server.ts inside the supabase folder.

After that, navigate to this supabase docs link, copy the code provided there, and paste it to the respective files you created in the supabase folder.

Next, create a file named middleware.ts at the root of your project and another file by the same name inside the /[root]/src/utils/supabase folder.

After that, navigate to this supabase docs link, copy the code provided there, and paste it to the respective middleware.ts files.

Next, go to /[root]/src/app/login folder and create a file named actions.ts. After that, navigate to this supabase docs link, copy the code provided there, and paste it to the actions.ts.

After that, change the email template to support the authentication flow. To do that, go to the Auth templates page in your Supabase dashboard.

In the Confirm signup template, change {{ .ConfirmationURL }} to {{ .SiteURL }}/auth/confirm?token_hash={{ .TokenHash }}&type=signup as shown below and then click the save button.

After that, create a route handler for authentication confirmation using email*.* To do that*, go to* /[root]/src/app/ and create a folder named auth. Then create a folder named confirm inside the auth folder.

Inside the confirm folder, create a file named route.ts and navigate to this supabase docs link, copy the code provided there, and paste it to the route.ts file.

After that, go to /[root]/src/app/login/page.tsx file and use the code snippet below to import the Supabase signup and login functions.

import { login, signup } from ./actions;

In the signup/login form, use formAction to call the Supabase signup and login functions when the login and signup buttons are clicked, as shown below.

<button
className=“bg-blue-500 text-white font-bold py-2 px-4 rounded”
formAction={login}>
Log in
</button>
<button
className=“bg-blue-500 text-white font-bold py-2 px-4 rounded”
formAction={signup}>
Sign up
</button>

After that, run the development server and navigate to http://localhost:3000/login. Add an email and password as shown below and click the Sign Up button.

Then go to the inbox of the email you used to sign up and click the Confirm your email button, as shown below.

After that, go to the Auth users page in your Supabase dashboard and you should see the newly created user, as shown below.

Next, set up logout functionality. To do that, go to /[root]/src/app and create a folder named logout. Then create a file named route.ts and add the following code into the file.

// Server-side code
use server;

// Importing Next.js functions
import { revalidatePath } from next/cache;
import { redirect } from next/navigation;

// Importing Supabase client creation function from utils
import { createClient } from @/utils/supabase/server;

// Exporting the logout function
export async function logout() {
// Creating a Supabase client instance
const supabase = createClient();

// Signing out of Supabase auth
const { error } = await supabase.auth.signOut();

// If there’s an error, redirect to the error page
if (error) {
redirect(/error);
}

// Revalidate the “/” path for the “layout” cache
revalidatePath(/, layout);

// Redirect to the homepage
redirect(/);
}

After that, go to /[root]/src/app/components/Header.tsx file and import the Supabase logout function using the code snippet below.

import { logout } from ../logout/actions;

Then add the logout function to form action parameter, as shown below.

<form action={logout}>
<button
className=“flex items-center font-medium text-gray-500 border-2 border-indigo-600 text-center p-2 rounded-md hover:text-blue-600 sm:border-s sm:my-6 “
type=“submit”>
Logout
</button>
</form>

If you click the Logout button, the logged-in user will be logged out.

Setting up user roles and permissions for the blog

Here, I will walk you through the process of setting up user roles and permissions that control what different users can do on the blog.

To get started, go to /[root]/src/app/components/Header.tsx file and import the Supabase createClient function.

import { createClient } from @/utils/supabase/client;

Then import useState and useEffect hooks and define a type named user using the code snippet below.

import { useEffect, useState } from react;

type User = {};

Inside the Header functional component, add the following code that uses the useState hook to store the user and admin data, and the useEffect hook to fetch the user data from Supabase auth when the component mounts. The getUser function checks for errors and sets the user and admin states accordingly.

// State variables for user and admin
const [user, setUser] = useState<User | null>(null);
const [admin, setAdmin] = useState<User | null>(null);

// useEffect hook to fetch user data on mount
useEffect(() => {
// Define an async function to get the user
async function getUser() {
// Create a Supabase client instance
const supabase = createClient();

// Get the user data from Supabase auth
const { data, error } = await supabase.auth.getUser();

// If there’s an error or no user data, log a message
if (error || !data?.user) {
console.log(No User);
}
// If user data is available, set the user state
else {
setUser(data.user);
}

// Define the email of the signed-up user
const userEmail = email of signed-up user;

// Check if the user is an admin (email matches)
if (!data?.user || data.user?.email !== userEmail) {
console.log(No Admin);
}
// If the user is an admin, set the admin state
else {
setAdmin(data.user);
}
}
// Call the getUser function
getUser();
}, []); // Dependency array is empty, so the effect runs only once on mount

After that, update the navbar code as shown below. The updated code controls which buttons will be rendered depending on whether there is a logged-in user or the logged-in user is an admin.

<div id=“navbar-collapse-with-animation” className=“”>
{/* Navbar content container */}
<div className=“flex flex-col gap-y-4 gap-x-0 mt-5 sm:flex-row sm:items-center sm:justify-end sm:gap-y-0 sm:gap-x-7 sm:mt-0 sm:ps-7”>
{/* Conditional rendering for admin link */}
{admin ? (
// If admin is true, show the “Create Post” link
<Link
className=“flex items-center font-medium text-gray-500 border-2 border-indigo-600 text-center p-2 rounded-md hover:text-blue-600 sm:border-s sm:my-6 “
href=“/createpost”>
Create Post
</Link>
) : (
// If admin is false, render an empty div
<div></div>
)}

{/* Conditional rendering for user link/logout button */}
{user ? (
// If user is true, show the logout button
<form action={logout}>
<button
className=“flex items-center font-medium text-gray-500 border-2 border-indigo-600 text-center p-2 rounded-md hover:text-blue-600 sm:border-s sm:my-6 “
type=“submit”>
Logout
</button>
</form>
) : (
// If user is false, show the “Login” link
<Link
className=“flex items-center font-medium text-gray-500 border-2 border-indigo-600 text-center p-2 rounded-md hover:text-blue-600 sm:border-s sm:my-6 “
href=“/login”>
Login
</Link>
)}
</div>
</div>

If you navigate to http://localhost:3000 you should see that only the Create Post and Logout buttons are rendered because the user is logged in and set to admin.

After that, go to /[root]/src/app/createpost/page.tsx file and import the Supabase createClient function.

import { createClient } from @/utils/supabase/client;

Inside the WriteArticle functional component, add the following code that fetches the logged in user using Supabase createClient function and verifies if the user’s email is the same with the email of the user set as admin.

// Define the email of the user you want to make admin
const userEmail = email of admin user;

// Create a Supabase client instance
const supabase = createClient();

// Get the user data from Supabase auth
const { data, error } = await supabase.auth.getUser();

// Check for errors or if the user data doesn’t match the expected email
if (error || !data?.user || data?.user.email !== userEmail) {
// If any of the conditions are true, redirect to the homepage
redirect(/);
}

Now only a user set to admin can access the http://localhost:3000/createpost page, as shown below.

Setting up insert and fetch post data with Supabase functionality

Here, I will walk you through the process of setting up insert and fetch data functionality to the blog using the Supabase database.

To get started, go to the SQL Editor page in your Supabase dashboard. Then add the following SQL code to the editor and click CTRL + Enter keys to create a table called articles.

The articles table has id, title, slug, content, and created_at columns.

create table if not exists
articles (
id bigint primary key generated always as identity,
title text,
slug text,
content text,
created_at timestamp
);

Once the table is created, you should get a success message, as shown below.

After that, go to /[root]/src/utils/supabase folder and create a file called AddArticle.ts. Then add the following code that inserts blog article data into the Supabase database to the file.

// Server-side code
use server;

// Importing Supabase auth helpers and Next.js functions
import { createServerComponentClient } from @supabase/auth-helpers-nextjs;
import { cookies } from next/headers;
import { redirect } from next/navigation;

// Exporting the addArticle function
export async function addArticle(formData: any) {
// Extracting form data
const title = formData.get(title);
const content = formData.get(content);
const slug = formData
.get(title)
.split( )
.join()
.toLowerCase()
.replace(/[^a-zA-Z0-9-]/g, “”); // Generating a slug from the title
const created_at = formData.get(new Date().toDateString()); // Getting the current date

// Creating a cookie store
const cookieStore = cookies();

// Creating a Supabase client instance with cookie store
const supabase = createServerComponentClient({ cookies: () => cookieStore });

// Inserting data into the “articles” table
const { data, error } = await supabase.from(articles).insert([
{
title,
content,
slug,
created_at,
},
]);

// Handling errors
if (error) {
console.error(Error inserting data, error);
return;
}

// Redirecting to the homepage
redirect(/);

// Returning a success message
return { message: Success };
}

Next, go to /[root]/src/app/components/Post.tsx file and import the addArticle function.

import { addArticle } from @/utils/supabase/AddArticle;

Then add the addArticle function as the form action parameter, as shown below.

<form
action={addArticle}
className=“w-full h-full gap-10 flex flex-col items-center p-10”>

</form>

After that, navigate to http://localhost:3000/createpost and give the chatbot on the right side a prompt like, “research a blog article topic on JavaScript frameworks.”

The chatbot will start researching the topic and then generate a blog title and outline, as shown below.

When you start writing on the CopilotKitTextarea, you should see content autosuggestions, as shown below.

If the content is up to your liking, copy and paste it to the Quill rich text editor. Then start editing it as shown below.

Then click the publish button at the bottom to publish the article. Go to your project’s dashboard on Supabase and navigate to the Table Editor section. Click the articles table and you should see that your article data was inserted into the Supabase database, as shown below.

Next, go to /[root]/src/app/components/Posts.tsx file and import the createClient function.

import { createClient } from @/utils/supabase/client;

Inside the Posts functional component, add the following code that uses the useState hook to store the articles data and the useEffect hook to fetch the articles from Supabase when the component mounts. The fetchArticles function creates a Supabase client instance, fetches the articles, and updates the state if data is available.

// State variable for articles
const [articles, setArticles] = useState<any[]>([]);

// useEffect hook to fetch articles on mount
useEffect(() => {
// Define an async function to fetch articles
const fetchArticles = async () => {
// Create a Supabase client instance
const supabase = createClient();

// Fetch articles from the “articles” table
const { data, error } = await supabase.from(articles).select(*);

// If data is available, update the articles state
if (data) {
setArticles(data);
}
};

// Call the fetchArticles function
fetchArticles();
}, []); // Dependency array is empty, so the effect runs only once on mount

After that, update the elements code as shown below to render the published articles on the blog’s homepage.

// Return a div element with a max width, full height, padding, and horizontal margin
return (
<div className=“max-w-[85rem] h-full px-4 py-10 sm:px-6 lg:px-8 lg:py-14 mx-auto”>
// Create a grid container with dynamic number of columns based on screen size
<div className=“grid sm:grid-cols-2 lg:grid-cols-3 gap-6”>
// Map over the articles array and render a Link component for each item
{articles?.map((post) => (
<Link
// Assign a unique key prop to each Link component
key={post.id}
// Apply styles for the Link component
className=“group flex flex-col h-full bg-gray-800 border border-gray-200 hover:border-transparent hover:shadow-lg transition-all duration-300 rounded-xl p-5 “
// Set the href prop to the post slug
href={`/${post.slug}`}>
// Create a container for the image
<div className=“aspect-w-16 aspect-h-11”>
// Render an Image component with a dynamic src based on the post title
<Image
className=“object-cover h-48 w-96 rounded-xl”
src={`(link unavailable){encodeURIComponent(
post.title
)}`
}
// Set the width and height props for the Image component
width={500}
height={500}
// Set the alt prop for accessibility
alt=“Image Description”
/>
</div>
// Create a container for the post title
<div className=“my-6”>
// Render an h3 element with the post title
<h3 className=“text-xl font-semibold text-white “>
{post.title}
</h3>
</div>
</Link>
))}
</div>
</div>
);

Then navigate to  http://localhost:3000 and you should see the article you published, as shown below.

Next, go to /[root]/src/app/[slug]/page.tsx file and import the createClient function.

import { createClient } from @/utils/supabase/client;

Below the imports, define an asynchronous function named ‘getArticleContent’ that retrieves article data from supabase database based on slug parameter, as shown below.

// Define an asynchronous function to retrieve article content
async function getArticleContent(params: any) {
// Extract the slug parameter from the input params object
const { slug } = params;

// Create a Supabase client instance
const supabase = createClient();

// Query the “articles” table in Supabase
// Select all columns (*)
// Filter by the slug column matching the input slug
// Retrieve a single record (not an array)
const { data, error } = await supabase
.from(articles)
.select(*)
.eq(slug, slug)
.single();

// Return the retrieved article data
return data;
}

After that, update the functional component Post as shown below to render the article content.

export default async function Post({ params }: { params: any }) {
// Fetch the post content using the getArticleContent function
const post = await getArticleContent(params);

// Return the post component
return (
// Fragment component to wrap multiple elements
<>
// Header component
<Header />
// Main container with max width and height
<main className=“p-3 flex flex-col max-w-6xl mx-auto min-h-screen”>
// Post title
<h1 className=“text-3xl text-white mt-10 p-3 text-center font-serif max-w-2xl mx-auto lg:text-4xl”>
{post && post.title} // Display post title if available
</h1>
// Post metadata (author, date, etc.)
<div className=“flex justify-between text-white p-3 border-b border-slate-500 mx-auto w-full max-w-2xl text-xs”>
<span></span>
// Estimated reading time
<span className=“italic”>
{post && (post.content.length / 1000).toFixed(0)} mins read
</span>
</div>
// Post content
<div
className=“p-3 max-w-2xl text-white mx-auto w-full post-content border-b border-slate-500 mb-2”
// Use dangerouslySetInnerHTML to render HTML content
dangerouslySetInnerHTML={{ __html: post && post.content }}></div>
// Comment component
<Comment />
</main>
</>
);
}

Navigate to  http://localhost:3000 and click an article displayed on the blog homepage. You should then be redirected to the article’s content, as shown below.

Setting up insert and fetch comment data with Supabase functionality

Here, I will walk you through the process of setting up insert and fetch data functionality for the blog’s content comments using the Supabase database.

To get started, go to the SQL Editor page in your Supabase dashboard. Then add the following SQL code to the editor and click CTRL + Enter keys to create a table called comments. The comments table has id, content, and postId columns.

create table if not exists
comments (
id bigint primary key generated always as identity,
postId text,
content text,
);

Once the table is created, you should get a success message, as shown below.

After that, go to /[root]/src/utils/supabase folder and create a file called AddComment.ts . Then add the following code that inserts blog article comment data into the Supabase database to the file.

// Importing necessary functions and modules for server-side operations
use server;
import { createServerComponentClient } from @supabase/auth-helpers-nextjs;
import { cookies } from next/headers;
import { redirect } from next/navigation;

// Define an asynchronous function named ‘addComment’ that takes form data as input
export async function addComment(formData: any) {
// Extract postId and content from the provided form data
const postId = formData.get(postId);
const content = formData.get(content);

// Retrieve cookies from the HTTP headers
const cookieStore = cookies();

// Create a Supabase client configured with the provided cookies
const supabase = createServerComponentClient({ cookies: () => cookieStore });

// Insert the article data into the ‘comments’ table on Supabase
const { data, error } = await supabase.from(comments).insert([
{
postId,
content,
},
]);

// Check for errors during the insertion process
if (error) {
console.error(Error inserting data, error);
return;
}

// Redirect the user to the home page after successfully adding the article
redirect(/);

// Return a success message
return { message: Success };
}

Next, go to /[root]/src/app/components/Comment.tsx file, import the addArticle createClient functions.

import { addComment } from @/utils/supabase/AddComment;

import { createClient } from @/utils/supabase/client;

Then add postId as prop parameter to Comment functional component.

export default function Comment({ postId }: { postId: any }) {}

Inside the function, add the following code that uses uses the useEffect hook to fetch comments and article content from Supabase when the component mounts or when the postId changes. The fetchComments function fetches all comments, while the fetchArticleContent function fetches the content of the article with the current postId.

useEffect(() => {
// Define an async function to fetch comments
const fetchComments = async () => {
// Create a Supabase client instance
const supabase = createClient();
// Fetch comments from the “comments” table
const { data, error } = await supabase.from(comments).select(*);
// If data is available, update the comments state
if (data) {
setComments(data);
}
};

// Define an async function to fetch article content
const fetchArticleContent = async () => {
// Create a Supabase client instance
const supabase = createClient();
// Fetch article content from the “articles” table
// Filter by the current postId
const { data, error } = await supabase
.from(articles)
.select(*)
.eq(id, postId)
.single();
// If the fetched article ID matches the current postId
if (data?.id == postId) {
// Update the article content state
setArticleContent(data.content);
}
};

// Call the fetch functions
fetchArticleContent();
fetchComments();
}, [postId]); // Dependency array includes postId, so the effect runs when postId changes

Then add the addComment function as the form action parameter, as shown below.

<form
action={addComment}
className=“border border-teal-500 rounded-md p-3 mb-4”>
<textarea
id=“content”
name=“content”
placeholder=“Add a comment…”
rows={3}
onChange={(e) => setComment(e.target.value)}
value={comment}
className=“hidden”
/>

<CopilotTextarea
className=“p-4 w-full rounded-lg mb-2 border text-sm border-gray-600 bg-gray-700 text-white placeholder-gray-400 focus:border-cyan-500 focus:ring-cyan-500 resize-none”
ref={copilotTextareaRef}
placeholder=“Start typing for content autosuggestion.”
onChange={(event) => setComment(event.target.value)}
rows={5}
autosuggestionsConfig={{
textareaPurpose: articleContent,
chatApiConfigs: {
suggestionsApiConfig: {
forwardedParams: {
max_tokens: 5,
stop: [n, ., ,],
},
},
insertionApiConfig: {},
},
debounceTime: 250,
}}
/>
<input
type=“text”
id=“postId”
name=“postId”
value={postId}
className=“hidden”
/>
<div className=“flex justify-between items-center mt-5”>
<button
type=“submit”
className=“bg-blue-500 text-white font-bold py-2 px-4 rounded”>
Submit
</button>
</div>
</form>

Below the form element, add the following code that renders post comments.

{comments?.map(
(postComment: any) =>
postComment.postId == postId && (
<div
key={postComment.id}
className=“flex p-4 border-b dark:border-gray-600 text-sm”>
<div className=“flex-shrink-0 mr-3”>
<Image
className=“w-10 h-10 rounded-full bg-gray-200”
src={`https://source.unsplash.com/featured/?${encodeURIComponent(
Silhouette
)}`}
width={500}
height={500}
alt=“Profile Picture”
/>
</div>
<div className=“flex-1”>
<div className=“flex items-center mb-1”>
<span className=“font-bold text-white mr-1 text-xs truncate”>
Anonymous
</span>
</div>
<p className=“text-gray-500 pb-2”>{postComment.content}</p>
</div>
</div>
)
)}

Next, go to /[root]/src/app/[slug]/page.tsx file and add postId as a prop to the Comment component.

<Comment postId={post && [post.id](http://post.id/)} />

Go to the published article content page and start typing a comment in the textarea. You should get content autosuggestion as you type.

Then click the submit button to add your comment. Go to your project’s dashboard on Supabase and navigate to the Table Editor section. Click the comments table and you should see that your comment data was inserted into the Supabase database, as shown below.

Go back to the published article content page you commented and you should see your comment, as shown below.

Congratulations! You’ve completed the project for this tutorial.

Conclusion

CopilotKit is an incredible tool that allows you to add AI Copilots to your products within minutes. Whether you’re interested in AI chatbots and assistants or automating complex tasks, CopilotKit makes it easy.

If you need to build an AI product or integrate an AI tool into your software applications, you should consider CopilotKit.

You can find the source code for this tutorial on GitHub: https://github.com/TheGreatBonnie/aiblogapp