Create an e-learning platform using caisy and next.js app router
Tags: nextjs, react, fullstack, webdev
Published: 2023-09-08 03:26:31

Developers learn better when they build or create something out of their learnings. We need to put our learnings into practice and try problem-solving. When we attempt to learn multiple technologies together or try out a tech stack, it makes sense to create meaningful applications that touch all the layers of the tech stack.

Recently, I have been super excited about the Next.js app router. The paradigm of `React Server Components` and the flexibility in data fetching with composable layouts and routes made it a framework to try out. Also, I stumbled upon an excellent, headless content management system `caisy` and fell in love with its capabilities and simplicity.

Yes, you are guessing it right! I combined the Next.js app router and caisy to create a fun project. This tutorial takes you through the steps of creating the same so that you get aware of both technologies and have fun creating something meaningful while learning about them.

Let's create an e-learning platform while learning about a trendy tech stack like `Next.js` app router, `GraphQL`, and a headless CMS platform called `caisy`.

> ⚡ It will be a lengthy tutorial, but it will be fun, I promise. Please take adequate breaks while going through it. Towards the end, I have given a few tasks for you. If you happen to complete them, I will be very glad 😃.

If you like to learn from a video tutorial, this tutorial is also available as a video on my YouTube channel, [`tapaScript`](https://www.youtube.com/tapasadhikary?sub_confirmation=1) 😀.

{% youtube ZEiudFe5NSA %} 

## What are we building today?

We will build an e-learning platform `Coursery` listing many courses users can look into, learn about, purchase, and provide comments and ratings.

![Coursery - an e-learning platform](https://cdn.hashnode.com/res/hashnode/image/upload/v1693530909963/1b423cef-e48d-4f87-859b-faab39d36c0a.png?border=1,CCCCCC&auto=compress align="center")

You can access the deployed version of the app from here: [https://coursery.vercel.app/](https://coursery.vercel.app/)

Also, the application's [source code](https://github.com/atapas/coursey) is an open source that you can refer to and use anytime while reading this tutorial.

### Tech Stack

We will use the following technology stack to build the application:

* [Caisy Headless CMS](https://app.caisy.io/app/signup/mwiyyiglk2er) as a backend data store.
    
* [Next.js App router](https://nextjs.org/docs) for the user interface implementation.
    
* [TailwindCSS](https://tailwindcss.com/#what-is-tailwind) for styling the app.
    
* [GaphQL](https://graphql.org/) as API endpoint
    

## Caisy - a modern headless CMS

[Caisy](https://app.caisy.io/app/signup/mwiyyiglk2er) is a headless CMS that makes content management easy. You can create multiple projects and can switch between them seamlessly. It is easy to tailor your content structure using data models and connections.

It provides a GraphQL API to fetch and mutate content. This provides the control to the developer community to build any app with any tech stack on top of the content in the caisy.

![Caisy UI](https://cdn.hashnode.com/res/hashnode/image/upload/v1693536262006/1581c36b-16c4-41e0-b59f-69655f3ceff1.png?border=1,CCCCCC&auto=compress)

We will learn to use caisy as a data store and content manager for our e-learning platform.

### Setting up caisy

First, create an account with caisy using [this link](https://app.caisy.io/app/signup/mwiyyiglk2er) and then follow a few simple steps to set up your profile and log in. Now, you must set up your project by following a simple wizard.

* First, create an organization with a name and an optional logo. Here, I have given the organization name as `Coursery`. You can provide any name of your choice. Click on continue.
    

![create org](https://cdn.hashnode.com/res/hashnode/image/upload/v1693537005119/f487f74d-bd98-48c1-9bc4-f398a735ae9c.png?border=1,CCCCCC&auto=compress)

* Next, we will create a project under the organization. Caisy provides you with many out-of-the-box templates to create the project. Creating a project based on a template of Next.js, Nust, Astro, Qwik, and many more is a matter of a few quick clicks. In our case, we will create the project from scratch. So, select the `Blank Project` option.
    

![project types](https://cdn.hashnode.com/res/hashnode/image/upload/v1693537172901/cb8cdb6d-3954-4ac9-ae54-b1f38986efb0.png?border=1,CCCCCC&auto=compress)

* Give the name of the project and continue.
    

![image-1](https://cdn.hashnode.com/res/hashnode/image/upload/v1693537459281/7000b04d-0c33-4ce0-b03a-178cc446f36d.png?border=1,CCCCCC&auto=compress)

That's it! You are done with all the basic setups. Now, we will focus on creating content in caisy and utilizing it for our application.

### Setting up the content blueprint

We must create the content structure before adding any content to the caisy. A structure is like the schema we create in a database context. We can define fields, their types, validation, and connections in the blueprint.

The blueprints bring consistency to your content. As you will be creating the content based on a blueprint, all the content for a blueprint will be structurally consistent. To create a blueprint, click on the `Blueprints` option from the sidebar.

![left nav bar](https://cdn.hashnode.com/res/hashnode/image/upload/v1693539381805/069de17a-4eb7-4067-9a95-246e8b73fce5.png?border=1,CCCCCC&auto=compress)

You can create two types of blueprints in caisy,

1. `Documents`: Use it to create the structure for the generic content of your application. For example, we need to create content for the courses in our app. So we can create a `documents` type blueprint for it.
    
2. `Components`: Components are reusable entities. Our app will have a comment section for users to provide a rating and comment on the courses. Our comment section will be reusable for all the courses. We can create the content structure of the comment section as `components` blueprint.
    

By default, caisy provides a system-generated blueprint called `Asset` to create various assets like media files and images.

To create a blueprint, click the `CREATE` button at the top-right of the Blueprints page and select a blueprint type.

![image-2](https://cdn.hashnode.com/res/hashnode/image/upload/v1693540533332/c422a5e7-1f6e-4c9f-9e03-5cfdb0241fdf.png?border=1,CCCCCC&auto=compress)

We will create the course blueprint, so select the `Document blueprint` option. It will open up a form to fill in the details. Provide the title and description of the blueprint. You can also turn on the `Mutations` option if you want any external API like GraphQL to manipulate(Create/Update/Delete) content for this document.

Also, notice that an API endpoint name gets associated with the blueprint to refer to it using the external API.

![image-3](https://cdn.hashnode.com/res/hashnode/image/upload/v1693540714924/7262edc8-94de-42a7-b9f1-16d40264c1d9.png?border=1,CCCCCC&auto=compress)

Go ahead and create the blueprint. Now, you will get an interface to drag and drop fields to create the content structure. A course may have a name, description, category, cover image, link, and many other fields. So, drag and drop the relevant type of fields to create them.

For example, the course name could be a string-type field. Drag and drop, and `String field` to provide the required details on the settings page. You can also add a few validation options to the field. Also, notice the API name to refer to this field when you want to fetch/mutate using the GraphQL API.

![string field](https://cdn.hashnode.com/res/hashnode/image/upload/v1693541223218/f482dbe6-15f3-42ce-8207-ea2f92c49154.png?border=1,CCCCCC&auto=compress)

This is what the `courses` document blueprint may look like.

![courses blueprint](https://cdn.hashnode.com/res/hashnode/image/upload/v1693541431196/1d288a0c-5ef6-4773-948b-9babee6feb19.png)

Please pay attention to the type of fields we have created above. Apart from regular string types, `price` is a number-type field, and there are a couple of `Date/Time` fields. Also, three types are of special importance:

* The `cover` field is of type `Asset`. If you recall, `Asset` is a system-generated document. We are now extending it as a type to create another document.
    
* The `description` is a `Richtext` field. It means you can provide formatted texts, images, and rich content for the course description.
    
* The `comments` is a `Connection` field. A connection field helps connect multiple blueprints in a relation. It is like establishing relationships between tables in the RDBMS. As a course can have multiple comments, we have created a 1:N connection here.
    

We have not created the `Comments` blueprint yet. We are going to do it next. However, after creating the `Comments` blueprint, please edit the `Courses` document and add a connection field for comments, as shown below.

![Connection field](https://cdn.hashnode.com/res/hashnode/image/upload/v1693542263532/010cc244-481d-4ea8-9d7e-0f8f1f30935f.png?border=1,CCCCCC&auto=compress)

Now, let us create the `Comments` blueprint. Again, click on the `CREATE` button on the blueprints page and, this time, select the `Component blueprint` option. A similar interface to the `courses` blueprint appears. Please drag and drop the fields like the last time and create the `Comments` blueprint.

![Comments blue print](https://cdn.hashnode.com/res/hashnode/image/upload/v1693542527304/2ee1fc99-98e5-40b1-8ecc-82606025f8e2.png?border=1,CCCCCC&auto=compress)

### Managing the content in caisy

We are done with creating the content structure. Now, we will create and manage the content. In caisy, we can manage content in a couple of ways:

* `Manually using the caisy app`: If you are not a developer but want to manage the application's content, caisy provides a form-based user interface for creating content quickly.
    
    Browse to the `Content` option from the sidebar and click on the `Create` button at the top-right. Select a blueprint to start creating the content.
    

![Create content](https://cdn.hashnode.com/res/hashnode/image/upload/v1693547242652/0e9f5abc-92e6-4eb0-bf6b-f9344e16fab5.png?border=1,CCCCCC&auto=compress)

Now, start filling up the content for the courses one by one.

![Course create form](https://cdn.hashnode.com/res/hashnode/image/upload/v1693561618681/676b81e9-8f7e-40f6-a6d3-03f330326fda.png?border=1,CCCCCC&auto=compress)

* `Programmatically using external API`: You can use an external API like GraphQL to create and manage content in caisy. This is mainly for developers who want to use caisy as a backend content management system and build user interfaces on top of it. We can use this method for creating comments from our e-learning app.
    

### API Playground to access course content

Caisy provides an API Playground for developers to query and mutate the content. It is a handy interface to realize the data responses from GraphQL API before you use them in your app development.

![Playground](https://cdn.hashnode.com/res/hashnode/image/upload/v1693549673042/dd2a400c-4aff-4188-93e3-d47298c4d07d.png?border=1,CCCCCC&auto=compress)

The playground has an explorer that lets you pick a blueprint and its attributes to query. As you pick them, the editor panel will automatically form a GraphQL query for you. You can then click on the `Execute` button to run the query and see the response.

You can also select the mutation option from the bottom of the explorer for forming the mutation queries to create/edit/delete content.

![mutation](https://cdn.hashnode.com/res/hashnode/image/upload/v1693549916240/e0b615b0-5d3f-4840-aee8-8f3d65803065.png?border=1,CCCCCC&auto=compress)

### Get secure access to the API

The external GraphQL API is powerful, but with great power comes great responsibility! You must protect your data so that it is not exposed to everyone publicly using the external API.

You can protect your API using the API Key. You must supply the API KEY when interacting with the content in caisy using an external API. To create an API Key, click on the `Setting` option in the sidebar, and then select the `Development` &gt; `API Keys` Option. Now click on the `CREATE API KEY` button.

> You can also find the project id from the `General Settings` option.

![API Key List](https://cdn.hashnode.com/res/hashnode/image/upload/v1693550330101/cfc6bb2c-a730-4ff0-8057-eec82870b7b0.png?border=1,CCCCCC&auto=compress)

Provide a name and get the API Key Code.

![API Key](https://cdn.hashnode.com/res/hashnode/image/upload/v1693550377198/8dd02b31-bb9f-4727-89c3-b3cf0ade9a45.png?border=1,CCCCCC&auto=compress)

A few important points to note here:

* You must copy and securely save the API Key code. You will not be able to view it again at a later point in time using caisy. You must create a new API Key if you forget or lose it.
    
* Caisy also provides a `Personal token` which is available under your profile settings. Don't confuse the personal token with the API Key. While API Key is used for accessing content using external APIs like GraphQL, the personal token is for caisy's internal APIs.
    
    ![personal token](https://cdn.hashnode.com/res/hashnode/image/upload/v1693550514128/004fc2a0-473f-4976-a3a3-92ffb65c86d1.png?border=1,CCCCCC&auto=compress)
    

That's great! We are now done with setting up the data layer using caisy. We know how to use the GraphQL playground to form the fetch queries and mutations. We know how to get the API Key. Now, we start diving into the application building using the Next.js app router and GraphQL.

## Build the course management UI using the Next.js app router and GraphQL.

Open a terminal/command prompt and use the following command to create a Next.js project using App router support.

```bash
npx create-next-app@latest
```

It will ask you a few simple questions. Answer them to create the project.

![nextjs app install](https://cdn.hashnode.com/res/hashnode/image/upload/v1693551460160/a0ce6c19-00c8-4a60-ae6c-7a9cc9fa90a8.png?border=1,CCCCCC&auto=compress)

You can use your favourite editor to import the project and get started. First, create a `.env.local` file at the root of your project folder and create these environment variables.

```json
CAISY_PROJECT_ID=<YOUR_CAISY_PROJECT_ID>
CAISY_API_KEY=<YOUR_CAISY_PROJECT_API_KEY>
```

> Make sure you provide the project id and the API KEY from your caisy project. We have already learned how to get both of them from the caisy app.

### Create a GraphQL client

We will now write the code to interact with the content in caisy using the GraphQL API. We will need a GraphQL client to make the API calls and handle request-response.

Install these two dependencies using the following command:

```bash
npm install graphql graphql-request
```

Next, create a folder called `service` at the root of the project. Now, create a file `graphQLClient.ts` under the service folder with the following content.

```typescript
import { GraphQLClient } from "graphql-request";

export const getClient = (preview: boolean): GraphQLClient => {
  const headers: any = {
    "x-caisy-apikey": process.env.CAISY_API_KEY!,
  }; 
  if (preview) {
    headers["x-caisy-preview"] = "true";
  }
  const client = new GraphQLClient(
    `https://cloud.caisy.io/api/v3/e/${process.env.CAISY_PROJECT_ID}/graphql`,
    {
      headers: headers,
    }
  );

  return client;
}
```

Here, we export a simple function called `getClient()` that creates a `GraphQLClient` using the API URL. Notice the API URL uses the project ID from the environment variable. We must pass a mandatory header `x-caisy-apikey` with the API Key value from the environment variable. Finally, we return the client.

### Data Fetching using GraphQL

We will use the GraphQL client to interact with the content in caisy and fetch the courses. Create a folder called `data` at the root of the project folder. Create a file `course.tsx` under the data folder with the following content.

```typescript
import { gql } from "graphql-request";
import { getClient } from '@/service/graphQLClient';

/**
 * Get all the courses from the data store
 * @returns An Array of course data
 */
export const getAllCourses = async() => {
  const client = getClient(false);
  const gqlResponse: any = await client.request(
    gql`
      query allCourses {
        allCourses {
          totalCount
          edges {
            node {
              name
              id
              slug
              category
              price
              cover {
                src
              }
            }
          }
        }
      }
    `
  );
 
  return {
    data: gqlResponse?.allCourses?.edges || [],
    totalCount: gqlResponse?.allCourses?.totalCount || 0,
  }; 
}
```

Let's go over the code.

* We first import `gql` from the `graphql-request` library. We will use it for the GraphQL query.
    
* Next, we import the GraphQL Client we created before.
    
* The async function `getAllCourses()` gets the client, forms the query, and makes a request using the query.
    
* We get a GraphQL response on a successful request completion.
    
* Finally, we destructure the response and return the `data` and the `totalCount` for the course data and total number of courses, respectively.
    

We can get an individual course using a course ID or slug. Look at the code snippet below to get a course by slug. You can add this function to the `course.tsx` file.

```typescript
/**
 * 
 * @param slug Get a Course information by the slug
 * @returns A Course data object
 */
export const getCourse = async(slug: String) => {
  const client = getClient(false);
  const gqlResponse: any = await client.request(
    gql`
      query allCourses($slug: String) {
        allCourses(where: { slug: { eq: $slug } }) {
          edges {
            node {
              comments {
                ... on Comments {
                  id
                  email
                  name
                  rating
                  commentedOn
                  data{
                    json
                  }
                }
              }
              cover {
                src
              }
              description {
                json
              }
              duration
              id
              link
              name
              category
              price
              publishedOn
              type
              updatedOn
            }
          }
        }
      }
    `,
    { slug: slug }
  );
 
  return {
    data: gqlResponse?.allCourses?.edges?.[0]?.node || {},
  }; 
}
```

The code is almost similar to the function of getting all courses. Here, we use a slug to fetch a specific course. So, we must pass the slug as a variable to the request.

### The Courses Page using Next.js App Router

At the core of the Next.js app router, it uses a new paradigm called `React Server Components`. If you are new to it, I [highly recommend this video](https://www.youtube.com/watch?v=5DZvdoMogys) to learn about RSC(React Server Component) with examples.

At a high level, with RSC, the components are nearer to the server than the client. That allows the RSC to use the async function and call the server-side APIS directly into the components without any network roundtrips.

In Next.js, all the pages, layouts, and components are server components by default. To create a client component, you need to mark it explicitly using the `'use client'` directive.

The Next.js app router skeleton project code comes with the top-level layout and route. Expand the `app` folder and open the `layout.tsx` file. It is a Layout file for a page located close to it. You will also find a `page.tsx` file at the same level as the layout file. Next.js will take care of applying this layout to the colocated page automatically.

Now, replace the content of the `layout.tsx` file with the following content.

```typescript
import "./globals.css";

import type { Metadata } from "next";
import { Inter } from "next/font/google";

const inter = Inter({ subsets: ["latin"] });

export const metadata: Metadata = {
  title: "Coursery",
  description: "Get all the courses by tapaScript",
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body className={inter.className}>
        {children}
      </body>
    </html>
  );
}
```

Here, we have provided relevant metadata information for our application. Also, notice the `children` prop gets passed to the layout. It is the colocated page. If you want to add a Navbar or Footer, you can add them to the layout.

Now open the `page.tsx` file under the `app` folder. This file creates the top-level route(/). Replace the content of the file with the following content.

```typescript
import Link from 'next/link';
import Image from 'next/image';

import { getAllCourses } from '@/data/course';
import Search from './components/Search';
import FeaturePrice from './components/FeaturePrice';

export default async function Home() {
  const courses: any = await getAllCourses();
  
  return (
    <main>
      <div className='container mx-auto px-4'>
        <Search />
        <div className='flex flex-wrap items-center'>
        {
          courses?.data?.map((course: any, index: number) =>(
            <Link
              key={course?.node?.id}
              href={`/${course?.node?.slug}`}
              className='md:flex md:flex-col md:mr-8 w-full md:w-[calc(50%-1rem)] lg:w-[calc(23.7%-1rem)] even:mr-0 lg:even:mr-8 mb-8 h-auto border-solid border-2 border-gray-300 rounded'>
                <div>
                  <FeaturePrice price={course?.node?.price}/>
                  <Image
                    src={course?.node?.cover?.src}
                    alt="Picture of the author"
                    width={320} 
                    height={300}
                    className='w-full h-auto' 
                  />
                  <div className='p-2'>
                    <p className='text-sm bg-gray-100 border border-gray-500 rounded-lg p-1 mb-1 font-semibold w-max'>{ course ?.node?.category }</p>
                    <h2 className='text-xl'>{ course?.node?.name }</h2>
                  </div>
                </div>
            </Link>
          ))

        }
        </div>
      </div>
    </main>
  )
}
```

A few points need your attention here:

* The component is an async component.
    
* It is making the GraphQL call directly. You do not need any `useEffect` or `useState` to manage things like before. You also do not need the complexity of the `getStaticProps` and `getserversideprops` of the earlier version of Next.js.
    
* We iterate over the courses and create a card layout for each course using its cover image, name, and category.
    
* Each course card is linked to a dynamic URL(route).
    
* We have imported two components `Search` and `FeaturePrice` to show a search bar at the top and a price tag on each of the course cards.
    

Create a folder called `components` and add a file `Search.tsx` with the following content:

```typescript
"use client";

export default function Search() {

  return(
    
    <div className="flex mt-3 mb-3 w-full justify-center">
      <label htmlFor="search-id"></label>
      <input
        className="border-2 border-gray-200 rounded-lg w-full py-2 px-4 text-gray-700 leading-tight focus:outline-none focus:bg-white focus:border-purple-500"
        id="search-id"
        type="text"
        placeholder="Search a Course by Name"
        
      />
    </div>   
  )
}
```

Please note `Search` is a client component, and we have marked it using the `'use client'` directive. As we will need a client interaction like a key-down search on the search textbox, it can not be a server component. The search functionality is not implemented here; I am leaving it to you to improvise and add.

Now, create a file `FeaturePrice.tsx` under the `components` folder with the following content:

```typescript
const FeaturePrice = ({price}: {price: number}) => {
  return (
    <>
      {
        <div className="flex justify-end">
          <div className="z-10 py-2 px-3 text-xs bg-green-500 absolute text-center text-gray-900 font-bold uppercase leading-tight tracking-normal shadow-lg">
            {price === 0 ? 'FREE' : `$${price}`}
          </div>
        </div>
      }
    </>
  );
};

export default FeaturePrice;
```

It is a simple component that takes course price as a prop and shows it like a price tag on each course card.

Make sure you are running the app locally. If not, use the `yarn dev` command to run the app locally. Now, open your browser and access the app on `http://localhost:3000`. You should see the content from caisy rendering on the page. In my case, the home page shows all the five courses' content I had created in caisy.

![Course landing page](https://cdn.hashnode.com/res/hashnode/image/upload/v1693553648999/a0a004bd-fd5e-418a-9ace-d3a354bc5bae.png?border=1,CCCCCC&auto=compress)

### The Individual Course Page

Now, we will create a dynamic route. Whenever someone clicks on the course cards on the home page, it goes to a 404 page. Let's fix that.

Each of the courses has a slug property, and it is dynamic. Create a folder named `[slug]` under the `app` folder. The `[slug]` folder defines a dynamic route. We must create a `page.tsx` file under the `[slug]` folder to define the page for the dynamic route.

Create the `page.tsx` file under the `[slug]` folder with the following content.

```typescript
import Image from 'next/image';
import { RichTextRenderer } from "@caisy/rich-text-react-renderer";
import { getCourse } from '@/data/course';
import YoutubeEmbed from '@/app/components/YouTubeEmbed';
import Rating from '../components/Rating';

import { BsFillClockFill } from 'react-icons/bs';
import {IoPricetags} from 'react-icons/io5'
import Link from 'next/link';


export default async function CoursePage({params}: {
  params: {slug: string}
}) {
  
  const course: any  = await getCourse(params?.slug);
  const comments: any = course?.data?.comments;

  const cummulativeRating = comments.reduce((accumulator: number, comment: any) => accumulator + comment.rating, 0);
  const avgRating = comments.length > 0 ? Math.ceil(cummulativeRating/comments.length) : 0;
  
  return(
    <div className='p-2 flex flex-col justify-center'>
      <div>
        <h2 className='text-4xl font-semibold mt-2 mb-2'>{ course?.data?.name}</h2>
        <div className='text-xl mb-2 p-2'>
          {course?.data?.description?.json 
          && <RichTextRenderer node={course?.data?.description?.json} />}
        </div>
      </div>

      <YoutubeEmbed src={course?.data?.link} />

      <div className='flex flex-col flex-wrap items-center mb-2'>
        <div>
           {avgRating === 0 ? 'No Rating Available' : <Rating count={avgRating} size={32}/>}
        </div>
        
        <div className='flex'>
          <div className=' flex mr-7 p-1'>
            <IoPricetags size={24}/> 
            <p className='text-lg ml-1'>
              <Link
                href="https://paypal.me/greenrootsinfo"
                target='_blank'>
                Get it for <strong>{course?.data?.price === 0 ? `Free` : `$${course?.data?.price}`}</strong>
              </Link>
            </p>
          </div>
          <div className='flex p-1'>
            <BsFillClockFill size={20}/> 
            <p className='text-lg ml-1'>{ `${course?.data?.duration} minutes` }</p>
          </div>
        </div>
      </div>

      <div className='flex flex-col'>        
        <div className='mt-2 mb-2'>
          <ul className='text-xl mb-2'>{comments.length} Comments</ul>
          <div className=' bg-gray-100 mb-6 flex flex-wrap'>
          {
            comments.length > 0 ? (
              comments.map((comment: any) => (
                <li className='m-2 bg-white max-w-xl rounded-2xl px-10 py-8 shadow-lg hover:shadow-2xl transition duration-500' key={comment?.id}>
                  <Rating count={comment?.rating} size={24}/>

                  <div className="mt-4 text-md text-gray-600">{comment?.data?.json && <RichTextRenderer node={comment?.data?.json} />}</div>

                  <div className="mt-4 flex items-center space-x-4 py-6">
                    <div>
                      <img className="w-12 h-12 rounded-full" src="https://xsgames.co/randomusers/avatar.php?g=male" alt="" />
                    </div>
                    <div className="text-sm font-semibold">{comment?.name} • <span className="font-normal"> {comment?.commentedOn} </span></div>
                  </div>
                  
                </li>
              ))
            ) : (<p>No Comments Yet! Add One.</p>)
          }
          </div>
        </div>
      </div>
    </div>
  )
}
```

Things to note here:

* You can destructure the slug from the params passed to the component by Next.js.
    
* This is also an async component as it makes an asynchronous call to get course details by the slug.
    
* Rest all is about using the course response to render the details.
    
* The `YoutubeEmbed` and `Rating` are two simple components you can [find here](https://github.com/atapas/coursey/tree/main/site/app/components).
    
* But a couple of things to pay attention to. We have imported the RichTextRenderer to handle the response from caisy's rich text editor and a bunch of react—icons icons. So, we need to install these dependencies.
    
    ```bash
    npm install @caisy/rich-text-react-renderer react-icons
    ```
    

That's it. Now, access a course by clicking on it from the home page. You should see the details page with the course information like this:

![Details Page](https://cdn.hashnode.com/res/hashnode/image/upload/v1693553941985/3bcdd9d6-5f58-4863-9634-53bce9bbb574.png?border=1,CCCCCC&auto=compress)

## Conclusion & What's Next?

So, to recap:

* We have created content in caisy
    
* We have exposed them using GraphQl API
    
* We have used it in the Next.js app router and built a few pages(including dynamic routes).
    

All the source code used in this tutorial is in my GitHub repository. It is an open-source project, so feel free to check out, enhance, and use it.

{% github atapas/coursey %} 

> Please give the project a star(⭐) if you like the work

### Tasks for you

If you have come so long reading and implementing things, here are a few tasks to improve your understanding. You may try all or some of them if you wish.

* Add the functionality of posting a comment for a course. The code is already placed in the above repo for reference.
    
* Fork and Contribute to the above Open Source repo
    
    * Make the search functionality work.
        
    * Make the look and feel better.
        
    * Make the navbar work in the responsive mode.
        
    * Add a checkout system to buy a course.
        
* Deploy the project and share it on social media, tagging me.
    

---

## **Before We End...**

That's all. Thanks for reading it. I hope it was insightful. If you liked the tutorial, please post likes and share it in your circles.

Let's connect. I share web development, content creation, Open Source, and career tips on these platforms.

* [**tapaScript on YouTube**](https://www.youtube.com/tapasadhikary?sub_confirmation=1)
    
* [**X(eka Twitter)**](https://twitter.com/tapasadhikary)
    
* [**LinkedIn**](https://www.linkedin.com/in/tapasadhikary/)
    
* [**GitHub**](https://github.com/atapas)