Skip to content

RESOURCES / BLOG

Building a video course platform with Nuxt.js

Be it an enterprise-level organization, startup, or sports community, learning online has become a core aspect of our society and groups. As internet speeds have improved and internet access has increased, the e-learning mode of choice for many has become video as opposed to textual content. In this tutorial, we will learn how we can create a simple video course platform that we can use to host our courses and share them within our circles.

The completed project is available on Codesandbox.

To view the original codebase, click here

To be able to follow along easily with this article, we recommend having some knowledge on the following HTML/CSS, JavaScript, and VueJs. This is however not a hard requirement.

We are going to use NuxtJS as our framework of choice. NuxtJS builds on top of VueJS to provide a plug-and-play modular architecture, performant application, and enjoyable developer experience.

To get started, simply run the following command in your directory of choice


yarn create nuxt-app nuxtjs-video-course-platform

# OR

npx create-nuxt-app nuxtjs-video-course-platform

# OR

npm init nuxt-app nuxtjs-video-course-platform

Code language: PHP (php)

The above command will result in a series of questions. Here are the questions and our recommended answers:

Project name : nuxtjs-video-course-platform

Programming language: JavaScript

Project manager: Yarn

UI framework: TailwindCSS

Nuxt.Js modules: Content – Git-based headless CMS

Linting tools: None

Testing framework: None

Rendering mode: Universal (SSR/SSG)

Deployment target: Server (NodeJS hosting)

Development tools: None

What is your Github username: <your-github-username>

Version control system: Git

This will set up the NuxtJS project in a folder named nuxtjs-video-course-platform. You may now enter the project and launch it:


cd nuxtjs-video-course-platform

  

yarn dev

# OR

npm run dev

Code language: PHP (php)

Cloudinary is a platform providing end-to-end image and video management. We will be using it as our media-management solution.

To install the recommended NuxtJs plugin, add it as a dependency to your project:


yarn add @nuxtjs/cloudinary

# OR

npm install @nuxtjs/cloudinary

Code language: CSS (css)

Add the plugin as a module in the modules section of nuxt.config.js.


export  default  {

modules:  [

'@nuxtjs/cloudinary'

]

}

Code language: CSS (css)

To configure our package to access our Cloudinary account, we need to add the cloud_name. This is a unique string used to identify your cloudinary account. If you do not have one, feel free to signup on https://cloudinary.com.

Because we don’t want to commit our cloud_name to our code repository, we are going to create a .env file and store our secret variables there.


cat .env

Code language: CSS (css)

<!-- .env -->

NUXT_ENV_CLOUDINARY_CLOUD_NAME=<your-cloudinary-cloud-name>

Code language: HTML, XML (xml)

We are then going to configure the package to use the environmental variable above. To achieve this, we will add a cloudinary section to our nuxt.config.js file.


// nuxt.config.js

cloudinary:  {

cloudName:  process.env.NUXT_ENV_CLOUDINARY_CLOUD_NAME,

secure:  true,

useComponent:  true

}

Code language: JavaScript (javascript)

We have added secure: true to ensure the package generates secure https links to all our media assets in the above configuration. We have also added useComponent: true to enable component usage within our project.

Luxon is a library for dealing with dates and times in JavaScript. We will be using it to render formatted timestamps. Installation is simple:


npm install --save luxon

# OR

yarn add luxon

Code language: PHP (php)

This is a tiny, renderless, mobile-friendly, feature-rich modal component for Vue.js. To install, run the following command:


npm install vue-final-modal@latest

# OR

yarn add vue-final-modal@latest

Code language: PHP (php)

We will then create a plugin file:


cat plugins/vue-final-modal.js

Code language: PHP (php)

Add the following into the newly created file:


// plugins/vue-final-modal.js

import  Vue  from  "vue";

  

import  VueFinalModal  from  'vue-final-modal/lib';

  

Vue.use(VueFinalModal());

Code language: JavaScript (javascript)

We will now register the plugin into our nuxt.config.js file by adding it to the plugins and the build sections:


// nuxt.config.js

export  default  {

...

plugins: ['~plugins/vue-final-modal.js'],

...

build: {

transpile:  ['vue-final-modal']

},

...

}

  

Code language: JavaScript (javascript)

Nuxt content module enables us to write content into the contents folder of our projects. We are then able to fetch this content in our project through a MongoDB-like API. Nuxt content allows us to fetch data from Markdown, JSON, YAML, XML, and CSV files.

We are going to create a course directory to store our courses. To have multiple data blocks (our lessons) in each course, we are going to utilize JSON files. This is because we will be able to use arrays.

Let us now create a file called car-types.json.


{

"title":  " How to identify different cars",

"description":  "In this course, we review how different cars are designed and branded",

"image_public_id__cloudinary_":  "nuxtjs-video-course-platform/how-to-identify-cars/course-preview.jpg",

"lessons":  [

{

"template":  "lesson",

"title":  "Lamborghini",

"description":  "Automobili Lamborghini S.p.A. is an Italian brand and manufacturer of luxury sports cars and SUVs based in Sant'Agata Bolognese. The company is owned by the Volkswagen Group through its subsidiary Audi. Ferruccio Lamborghini, an Italian manufacturing magnate, founded Automobili Ferruccio Lamborghini S.p.A. - Wikipedia",

"video_public_id__cloudinary_":  "nuxtjs-video-course-platform/how-to-identify-cars/lamborghini"

},

{

"template":  "lesson",

"title":  "BMW",

"video_public_id__cloudinary_":  "nuxtjs-video-course-platform/how-to-identify-cars/bmw",

"description":  "Bayerische Motoren Werke AG, commonly referred to as BMW, is a German multinational corporation which produces luxury vehicles and motorcycles headquartered in Munich, Germany."

},

{

"template":  "lesson",

"title":  "Chevrolet",

"description":  "Chevrolet colloquially referred to as Chevy and formally the Chevrolet Division of General Motors Company, is an American automobile division of the American manufacturer General Motors (GM).",

"video_public_id__cloudinary_":  "nuxtjs-video-course-platform/how-to-identify-cars/chevloret"

},

{

"template":  "lesson",

"description":  "Volkswagen shortened to VW is a German motor vehicle manufacturer founded in 1937 by the German Labour Front, known for their iconic Beetle and headquartered in Wolfsburg.",

"title":  "Volkswagen",

"video_public_id__cloudinary_":  "nuxtjs-video-course-platform/how-to-identify-cars/volkswagen"

}

]

}

Code language: JSON / JSON with Comments (json)

To breakdown the file above, we have:

Title – Title of the course.

Description – Short description of the course.

image_public_id__cloudinary_ – Cloudinary public_id of the image to be shown for the course. You get this by uploading the image onto Cloudinary, copying the URL then removing everything but the folders, sub-folders and filename.

lessons – An array of all the lessons in the course.

For each lesson, we have the following:

template: The template to be used (to be discussed later)

description: A short description of the lesson

title: The title of the lesson

video_public_id__cloudinary_: Cloudinary public_id of the video lesson.You get this by uploading the video onto Cloudinary, copying the URL then removing everything but the folders, sub-folders, and filename.

To fetch data from Nuxt Content, we will use the asyncData hook. This hook fetches data asynchronously and merges it into the current local state. It is only available within pages.

Let us fetch the entire course list:


// pages/index.vue

  

<script>

export default {

...

async  asyncData({  $content  })  {

const courses  =  await  $content("courses").fetch();

  

return { courses  };

},

...

}

</script>

Code language: HTML, XML (xml)

We simply need to iterate the courses array like any other array to display the above courses. We will separate this code and place it in a component named CourseList.vue to maintain code readability:


// components/Index/CourseList.vue

<template>

<ul>

<li v-for="(course, idx) in courses"  :key="idx">

<nuxt-link  :to="`course/${course.slug}`">

<div>

<div>

<div>

<cld-image

:public-id="course.image_public_id__cloudinary_"

:alt="course.title"

width="100"

height="100"

/>

</div>

<div>

<div>

<p>

{{ course.title  }}

</p>

<p>

{{ course.description  }}

</p>

</div>

<div

>

<span>

Created on

{{  "  "  }}

<time :datetime="course.createdAt">{{

formatedDate(course.createdAt)

}}</time>

</span>

</div>

</div>

</div>

<div>

<svg

class="w-4 h-4"

fill="none"

stroke="currentColor"

viewBox="0 0 24 24"

xmlns="http://www.w3.org/2000/svg"

>

<path

stroke-linecap="round"

stroke-linejoin="round"

stroke-width="2"

d="M9 5l7 7-7 7"

></path>

</svg>

</div>

</div>

</nuxt-link>

</li>

</ul>

</template>

Code language: HTML, XML (xml)

Displaying a course is interesting because we need to display each course dynamically. This is so that more courses can be added and removed from the content without altering the code itself. To do this, we will utilize slugs. As you can see in the code snippet below, we added a slug to the nuxt-link.


// components/Index/CourseList.vue

<nuxt-link  :to="`course/${course.slug}`">

Code language: HTML, XML (xml)

The slug is automatically added to the courses by nuxt-content. We are going to get it from the params object and use it to fetch the specific course we want.


// pages/

<script>

export default {

...

async  asyncData({  $content,  params  })  {

const course  =  await  $content("courses",  params.slug).fetch();

  

return { course  };

},

...

};

</script>

Code language: HTML, XML (xml)

To display the course content, we simply access it like any other property in our local scope.


<!-- pages/course/_slug.vue -->

<template>

<div>

<div>

<div>

<div

>

<svg

fill="currentColor"

viewBox="0 0 100 100"

preserveAspectRatio="none"

aria-hidden="true"

>

<polygon  points="50,0 100,0 50,100 0,100"  />

</svg>

  

<main

>

<div>

<h1

>

<span>

{{  course.title  }}

</span>

</h1>

<p>

{{  course.description  }}

</p>

  

<nuxt-link  to="/">

<button

type="button"

>

<svg

fill="none"

stroke="currentColor"

viewBox="0 0 24 24"

xmlns="http://www.w3.org/2000/svg"

>

<path

stroke-linecap="round"

stroke-linejoin="round"

stroke-width="2"

d="M15 19l-7-7 7-7"

></path>

</svg>

&nbsp;  Back  to  Course  List

</button>

</nuxt-link>

</div>

</main>

</div>

</div>

<div>

<cld-image

:public-id="course.image_public_id__cloudinary_"

alt=""

/>

</div>

</div>

<ul

role="list"

>

<li

v-for="(lesson, index) in course.lessons"

:key="index"

class="relative"

>

<Lesson  :lesson="lesson"  />

</li>

</ul>

</div>

</template>

Code language: HTML, XML (xml)

The above code will render the course title, description, image, and all the lessons in the course. Let’s take a look at the Lesson component to see how they are displayed:


// components/course/Lesson.vue

<template>

<div>

<div>

<cld-video

:public-id="lesson.video_public_id__cloudinary_"

alt=""

/>

<button

@click="show  =  true"

type="button"

>

<svg

fill="none"

stroke="currentColor"

viewBox="0 0 24 24"

xmlns="http://www.w3.org/2000/svg"

>

<path

stroke-linecap="round"

stroke-linejoin="round"

stroke-width="2"

d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"

></path>

<path

stroke-linecap="round"

stroke-linejoin="round"

stroke-width="2"

d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"

></path>

</svg>

</button>

</div>

<p>

{{ lesson.title  }}

</p>

<p>

{{ lesson.description  }}

</p>

<vue-final-modal  v-model="show"  class="grid align-center">

<cld-video

:public-id="lesson.video_public_id__cloudinary_"

alt=""

controls="true"

/>

</vue-final-modal>

</div>

</template>

  

<script>

export default {

props: {

lesson:  {

required:  true,

type:  Object,

},

},

data()  {

return {

show:  false,

};

},

};

</script>

Code language: HTML, XML (xml)

Let’s break down the important components. cld-video allows us to render the video.


// components/course/Lesson.vue

...

<cld-video

:public-id="lesson.video_public_id__cloudinary_"

alt=""

/>

Code language: PHP (php)

To add controls to the video, we add controls=true. To display the video player, we use the vue-final-modal component.


// components/course/Lesson.vue

...

<vue-final-modal

v-model="show"

>

<cld-video

:public-id="lesson.video_public_id__cloudinary_"

controls="true"

/>

</vue-final-modal>

Code language: PHP (php)

So far, we have been able to edit the data purely by editing the content in our contents directory.

Once the website is live, this will be a less-than-ideal approach.

The good news, we are able to plug in Forestry CMS to our repository. Forestry describes itself as a headless CMS that commits.

To get set up, simply create an account on https://forestry.io. Once your account is created, import your Github repository.

Once the setup is complete, the next step is to set up the content types into the sidebar.

Click on “Configure sidebar”.

Add a section.

Label: Courses

Content directory: content/courses

File match: **/*.json

Content type: Documents

New file extension: json

This will add the courses onto the sidebar for easy access, editing, and deleting.

Feel free to read [this article] to learn more about Forestry CMS with NuxtJs, feel free to read this article.

With the above knowledge, we are able to push the limits in how we can deliver video content to our communities. Feel free to push the limits of what’s possible.

Start Using Cloudinary

Sign up for our free plan and start creating stunning visual experiences in minutes.

Sign Up for Free