Gridsome, a static site generator, is a Vue.js powered JAMstack framework for building static sites. A static site generator essentially takes dynamic content and data and generates static HTML files that can be deployed over a content delivery network or static web host.
Strapi is a headless content management system. A Headless CMS is a content management system that makes content accessible via REST APIs, GraphQL APIs, or Git workflow. As the name implies, it gives the developer the freedom to choose their most suitable frontend setup. Strapi is even more perfect because it’s 100% JavaScript, and my favorite feature is that you can fully customize everything. It’s almost like you built your own CMS!
Basic knowledge of the following would be required:
In this article, we will build a simple learning platform using Strapi and Gridsome. Strapi will handle the backend and it should be able to do the following:
Then, we’ll use Gridsome on the frontend and we’ll be able to perform the following:
Here's a demo of what we’ll be building in action:
First, let’s scaffold a new Strapi project. Run this command on your terminal:
1yarn create strapi-app strapi-learning-backend
2// or
3npx create-strapi-app strapi-learning-backend
Choose Quickstart as the installation type
This will install all the necessary dependencies, configurations, and plugins required in starting a new project. This installation also includes SQLite as the database for local development.
Strapi would automatically start the dev server after the project has successfully been installed. However, if you run into any errors (that are not Strapi related), run this command to start the Strapi development server.
1yarn develop
2// or
3npm run develop
The Strapi application will be available at http://localhost:1337/admin which opens the Strapi admin dashboard prompting you to create an admin account:
Now that you’ve successfully launched the admin dashboard, let’s go ahead and start creating the courses.
Strapi content type lets you define how your content would be structured by adding types and populating them with fields. Let’s go ahead and create a collection type for Courses.
Click on the button that says CREATE YOUR FIRST CONTENT-TYPE.
Next, you would be prompted to give the collection type a name.
Note: If the name is course, your API endpoint would be /courses
. Check this reference on the docs.
Click on continue and you would be prompted with this screen to select a field for your collection type
These are the fields we will be adding to our courses type:
I have demonstrated how to create a field in the GIF below:
Make sure that you click on save when you’re done with creating fields.
Now, we’ve created fields for our content type, let’s go ahead to add courses through Strapi WYSIWYG editor.
Click on Strapi-courses (or whatever name you gave your collection type) on the left sidebar of the dashboard, then click on Add New Strapi-Courses and you will be presented with this screen
Now, you can populate the fields. Let’s try adding one course first :
1course_title: Strapi CMS with Gatsby,
2short_description: Learn how to use Strapi CMS with Gatsby to build powerful web applications,
3long_description: Gatsby is a React-based open-source framework for creating websites and apps. It's great whether you're building a portfolio site or blog, or a high-traffic e-commerce store or company homepage,
4course_image: https://res.cloudinary.com/samtech/image/upload/v1608285507/with_gatsby_e4dca7b0c3_9e4d8db26e.png,
5course_video: https://res.cloudinary.com/samtech/video/upload/v1608287197/Strapi_and_Gatsby_Blog_Starter_b98bcc1544.mp4
When you have done this, make sure you click the SAVE button. This means that the content is in the draft stage. Obviously, clicking on the Publish button would move the content from draft to published.
The course summary page should look like this now:
Awesome! Now, let’s fetch the course that we just created.
In your Strapi codebase, you will find the list of endpoints available in api/strapi-courses/config/routes.json
1// api/strapi-courses/config/routes.json
2
3{
4 "routes": [
5 {
6 "method": "GET",
7 "path": "/strapi-courses",
8 "handler": "strapi-courses.find",
9 "config": {
10 "policies": []
11 }
12 },
13 {
14 "method": "GET",
15 "path": "/strapi-courses/count",
16 "handler": "strapi-courses.count",
17 "config": {
18 "policies": []
19 }
20 },
21 {
22 "method": "GET",
23 "path": "/strapi-courses/:id",
24 "handler": "strapi-courses.findOne",
25 "config": {
26 "policies": []
27 }
28 },
29 {
30 "method": "POST",
31 "path": "/strapi-courses",
32 "handler": "strapi-courses.create",
33 "config": {
34 "policies": []
35 }
36 },
37 {
38 "method": "PUT",
39 "path": "/strapi-courses/:id",
40 "handler": "strapi-courses.update",
41 "config": {
42 "policies": []
43 }
44 },
45 {
46 "method": "DELETE",
47 "path": "/strapi-courses/:id",
48 "handler": "strapi-courses.delete",
49 "config": {
50 "policies": []
51 }
52 }
53 ]
54}
The /strapi-courses
would GET all the courses we’ve created but at the moment we don’t have authorized access to the endpoints. Let’s go back to the admin dashboard and on the left sidebar, click on Settings and under USERS & PERMISSIONS PLUGIN > Roles > Public > Permissions. Tick the following boxes:
Now when you query http://localhost:1337/strapi-courses
you will get this
Congratulations! If you’ve gotten this far. Let’s now deal with uploads before we add more courses.
Currently, thanks to strapi-plugin-upload
package, your media files uploads are in public/uploads
by default. If you’ve gone through the results of the query we made, you will notice that course_image.provider
is local. This would work fine on the strapi admin server but when connecting with your frontend, you’ll need a provider to serve your media files. Strapi allows you to integrate external providers such as Cloudinary, AWS-S3, Google cloud storage and more. For the purpose of this tutorial, we will be using Cloudinary.
Go to your Cloudinary dashboard and copy your CLOUDINARY_NAME
, CLOUDINARY_KEY
, and CLOUDINARY_SECRET
we will be needing them soon.
In this demo, we are going to be adding these values in our code instead of accessing them from an .env
file.
We have to set up the Strapi provider Cloudinary plugin. Install the package by running this command on your terminal.
1yarn add strapi-provider-upload-cloudinary
To register the plugin, go to the config directory and create a plugins.js
file:
1// config/plugin.js
2
3module.exports = ({ env }) => ({
4 upload: {
5 provider: 'cloudinary',
6 providerOptions: {
7 cloud_name: env('CLOUDINARY_NAME', 'YOUR_CLOUDINARY NAME'),
8 api_key: env('CLOUDINARY_KEY', 'YOUR_CLOUDINARY_KEY'),
9 api_secret: env('CLOUDINARY_SECRET', 'YOUR_CLOUDINARY_API_SECRET')
10 }
11 }
12})
Paste the cloudinary values accordingly.
When you save this, your server would restart automatically. Now, let’s confirm that it’s working fine by adding another course.
1course_title: Strapi CMS with Vue,
2short_description: Learn how to build web apps with Strapi CMS and Vuejs,
3long_description: Vue (pronounced /vjuː/, like view) is a progressive framework for building user interfaces. Unlike other monolithic frameworks, Vue is designed from the ground up to be incrementally adoptable. The core library is focused on the view layer only, and is easy to pick up and integrate with other libraries or existing projects.,
4download this image or just upload via url: https://res.cloudinary.com/samtech/image/upload/v1608287448/with_vue_9386f31664_ccce641ecf.png,
5download this video or just upload via url: https://res.cloudinary.com/samtech/video/upload/v1608288859/Strapi_and_Gridsome_Portfolio_Starter_5bae22f749.mp4
P.S: Uploading a video might take some time.
After you have uploaded media and published the new course successfully, query http://localhost:1337/strapi-courses
and you will get something like this:
Awesome! The provider is now cloudinary
****and we even have a thumbnail format.
You can go on to add more courses here.
However, we still need to deal with one more thing. Remember, our app would have authentication, so we have to set permissions for authenticated users.
Go to Settings > Users & Permissions Plugin > Roles > Authenticated Tick the boxes and save.
Awesome, we just succeeded in Setting up our API! That’s really cool.
Deploying a Strapi application is easy and straightforward. However, we’ll not be covering that in this article. The Strapi docs handle this topic very well. We’ve gone this far you could trust me right? 🙃. Learn how to Deploy your Strapi app here. I recommend you use Heroku.
Before we begin, Gridsome is popularly known for building “statically generated websites” but for the purpose of this article, we will go extra lengths into working with its Client API. The Client API lets you install Vue plugins such as Vuex which we will be using to handle authentication later in this section.
Install the Gridsome CLI globally by executing this command on your terminal:
1npm install --global @gridsome/cli
This will enable you to use the gridsome
CLI in your machine. Then run this command to create a new Gridsome project:
1gridsome create strapi-learning
This might take some time but when it’s done, go to the project directory by running this command:
1cd strapi-learning
Finally, run gridsome develop
to start your local development server.
Navigate to http://localhost:8080
to view the Gridsome app.
Let’s begin by creating the layout component for our app. In Gridsome, a layout component is the one where you insert other components you want to use across the site/app. Components such as the navbar, footer, and so on.
Gridsome create
has already set up the Layout to be used globally across pages. Go to src/main.js
and you will see your layouts being configured.
Also, we will be adding a font-family to the head
of our site.
1// src/main.js
2
3import DefaultLayout from '~/layouts/Default.vue'
4
5export default function (Vue) {
6 head.link.push({
7 rel: 'stylesheet',
8 href: 'https://fonts.googleapis.com/css2?family=Karla&display=swap',
9 })
10 // Set default layout as a global component
11 Vue.component('Layout', DefaultLayout)
12}
Now, head over to src/layouts/Default.vue
and replace the existing project’s Default.vue
code:
1// src/layouts/Default.vue
2
3<template>
4 <div class="layout">
5 <header class="header">
6 <Navbar />
7 </header>
8
9 <p style="color: #fff"> Auth Status: <span>{{authStatus}}</span> </p>
10 <slot />
11 </div>
12</template>
13
14<static-query>
15query {
16 metadata {
17 siteName
18 }
19}
20</static-query>
21
22<script>
23import Navbar from '~/components/Navbar';
24export default {
25 components: {
26 Navbar,
27 },
28 computed: {
29 authStatus(){
30 return this.$store.getters.authStatus
31 }
32 }
33};
34</script>
35<style>
36body {
37 font-family: 'Karla', sans-serif;
38 margin: 0;
39 padding: 0;
40 background: #753ff6;
41 line-height: 1.5;
42}
43
44span{
45 text-transform: uppercase;
46}
47
48.layout {
49 max-width: 950px;
50 margin: 0 auto;
51 padding-left: 20px;
52 padding-right: 20px;
53}
54
55.header {
56 display: flex;
57 justify-content: space-between;
58 align-items: center;
59 margin-bottom: 20px;
60
61}
62
63.nav__link {
64 margin-left: 20px;
65}
66
67@media (max-width: 700px){
68 nav{
69 flex-direction: column;
70 margin-bottom: 1rem;
71 }
72}
73</style>
Let’s now create the Navbar
component. Go to src/Components
and create a Navbar.vue
file with the following content:
1// src/Components/Navbar.vue
2
3<template>
4 <div class="navbar">
5 <nav>
6 <div>
7 <h1>
8 <g-link to="/"> Strapi Learning Platform </g-link>
9 </h1>
10 </div>
11 <ul>
12 <div v-if="disableNav" class="ul">
13 <li>Welcome, {{ user.username }}</li>
14 <li>
15 <button class="btn btn-outline" @click="logout">
16 Logout
17 </button>
18 </li>
19 </div>
20 <div v-else class="ul">
21 <li>
22 <g-link to="/signup" class="btn"> Register </g-link>
23 </li>
24 <li>
25 <g-link to="/login" class="btn btn-outline">
26 Login
27 </g-link>
28 </li>
29 </div>
30 </ul>
31 </nav>
32 </div>
33</template>
34
35<script>
36export default {
37 data() {
38 return {
39 user: {},
40 };
41 },
42 computed: {
43 isLoggedIn() {
44 return this.$store.getters.isLoggedIn;
45 },
46
47 disableNav() {
48 if (this.$store.getters.authStatus === 'success') {
49 return true;
50 } else if (this.$store.getters.authStatus === 'error') {
51 return false;
52 }
53 },
54 },
55
56 mounted() {
57 this.user = JSON.parse(localStorage.getItem('user'));
58 },
59 methods: {
60 logout() {
61 this.$store
62 .dispatch('logout')
63 .then(() => {
64 console.log('I am serious');
65 this.$router.push('/login');
66 })
67 .catch(err => {
68 console.log(err);
69 });
70 },
71 },
72};
73</script>
74
75<style scoped>
76.navbar {
77 width: 100%;
78}
79h1 a {
80 color: #fff;
81 text-decoration: none;
82}
83nav {
84 display: flex;
85 justify-content: space-between;
86 align-items: center;
87}
88
89ul {
90 list-style-type: none;
91 margin: 0;
92 padding: 0;
93}
94
95.ul {
96 display: flex;
97 align-items: baseline;
98}
99
100.btn {
101 border: none;
102 box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25);
103 cursor: pointer;
104 height: 4.375em;
105 border-radius: 1.5em;
106 text-decoration: none;
107 padding: 1.5em 1.5em;
108 background-color: rgb(87, 41, 178);
109 color: #fff;
110 font-size: 14px;
111 font-weight: 600;
112 font-family: 'Karla';
113 outline: none;
114}
115
116.btn-outline {
117 background: #fff;
118 border: 1px solid rgb(87, 41, 178);
119 color: #000;
120}
121
122li {
123 margin-left: 0.5rem;
124 margin-right: 0.5rem;
125 color: #fff;
126}
127
128.nav-link {
129 color: #fff;
130 text-decoration: none;
131}
132
133@media (max-width: 700px) {
134 ul {
135 justify-content: space-between;
136 }
137}
138</style>
One more thing. Let’s replace the code in ~/pages/Index.vue
with this code snippet:
1// src/pages/Index.vue
2
3<template>
4 <Layout>
5 <section class="hero-section">
6 <h1>Welcome!</h1>
7
8 <p>
9 Strapi is the leading open-source headless CMS. It’s 100%
10 Javascript, fully customizable and developer-first.
11 </p>
12
13 <p>
14 <g-link to="/login"> Login </g-link> to enjoy all our courses
15 here
16 </p>
17 </section>
18 </Layout>
19</template>
20
21<style>
22.home-links a {
23 margin-right: 1rem;
24}
25
26.hero-section {
27 background: #fff;
28 color: #2f2e8b;
29 padding: 1rem;
30}
31</style>
Great! The page should look like this:
We have succeeded in setting up the page layout.
Next, we will handle the register and login functionality.
Earlier on, we ensured that only authenticated users would be able to access our courses. This means that a user would have to provide a JWT
token in the Authorization header of the GET request.
Firstly, we need to install Axios **and Vuex.** Run this command on your terminal:
1npm install axios vuex
We will need Axios for our HTTP requests. Open the src/main.js
file and add the following:
1// src/main.js
2
3...
4 Vue.prototype.$http = axios;
5const token = process.isClient ? localStorage.getItem(`Bearer ${token}`) : false
6if (token) {
7 Vue.prototype.$http.defaults.headers.common['Authorization'] = `Bearer ${token}`
8 }
9...
Here, we set Authorization on the Axios header so when we want to make HTTP requests, we don’t need to set Authorization header every time.
Also, you will notice the process.client
condition every time we use localStorage. This is because localStorage is only available in the browser. So, you should check for process.isClient
to avoid code running during server-side rendering.
Now we have Vuex installed, how do we use it? Gridsome Client API defines a client context which is a reference to options for the Vue app. In essence, the Vuex store is one of those appOptions
we will have to pass to the main Vue instance.
Modify this line of code:
1// src/main.js
2
3export default function (Vue, {
4 head,
5 appOptions
6}) {
7 ...
8 Vue.use(Vuex)
9 appOptions.store = new Vuex.Store({
10 state: {
11
12 },
13 mutations:{},
14 actions: {},
15 getters: {}
16 ...
17}
The store would be available in our components with $store
. Let’s utilize the store to handle authentication.
First, let’s define the attributes of the state. The state is a store object that holds the application-level data that needs to be shared between components.
1// src/main.js
2
3...
4state:{
5 status: '',
6 token: process.isClient ? localStorage.getItem('token') || '' : false,
7 user: {}
8},
9...
Here, the state would hold the authentication status, JWT
token, and user data.
Next, let’s write mutations. Mutations are methods that modify the store state. They usually consist of a string type and a handler that accepts the state and payload as parameters. In the mutations object, add this code snippet:
1// src/main.js
2
3 mutations: {
4 AUTH_REQUEST(state) {
5 state.status = 'loading'
6 },
7 AUTH_SUCCESS(state, token, user) {
8 state.status = 'success',
9 state.token = token,
10 state.user = user
11 },
12 AUTH_ERROR(state) {
13 state.status = 'error'
14 },
15 LOGOUT(state) {
16 state.status = 'logged out',
17 state.token = ''
18 }
19 },
Next, let’s write actions. Actions are methods that trigger the mutations. When handling asynchronous tasks, actions are used before calling the corresponding mutations. In our project, we will be handling actions for login, register, and logout.
Create a .env
file in the root directory and add the strapi API url. If you did not deploy the API to heroku, you just have to spin up the local strapi dev server and use it as STRAPI_URL
in your env file.
1// .env
2STRAPI_URL= <your strapi api url here> or http://localhost:1337
Let’s start with the action for login:
1// src/main.js
2
3...
4actions: {
5 async login({ commit }, user) {
6 commit('AUTH_REQUEST')
7 await axios.post(`${process.env.STRAPI_URL}/auth/local/`, user)
8 .then(response => {
9 const token = response.data.jwt
10 const user = response.data.user
11
12 if (process.isClient) {
13 localStorage.setItem('token', token)
14 localStorage.setItem('user', JSON.stringify(user))
15 }
16 axios.defaults.headers.common['Authorization'] = `Bearer ${token}`
17 const something = axios.defaults.headers.common['Authorization']
18 console.log({something})
19 commit('AUTH_SUCCESS', token, user)
20 console.log({user, token})
21 })
22
23 .catch(err => {
24 commit('AUTH_ERROR')
25 process.isClient ? localStorage.removeItem('token') : false
26 console.error(err)
27 })
28 },
29 }
In this code snippet, we first pass a commit
that will trigger the corresponding mutation, which in turn would make changes to the Vuex store. Then, we make an HTTP call to our Strapi server login route. We store the JWT
token and user information on localStorage, then commit the to the AUTH_SUCCESS
mutation which would update the state attributes.
We also assign the token to Axios Authorization header. If our HTTP call fails, we update the state and remove the token if it even exists.
Let’s write the action for register:
1// src/main.js
2
3...
4 async register({commit}, user) {
5 commit('AUTH_REQUEST')
6 await axios.post(`${process.env.STRAPI_URL}/auth/local/register`, user)
7 .then(response => {
8 const token = response.data.jwt
9 const user = response.data.user
10
11 process.isClient ? localStorage.setItem('token', token) : false
12
13 axios.defaults.headers.common['Authorization'] = `Bearer ${token}`
14
15 commit('AUTH_SUCCESS', token, user)
16 })
17 .catch(err => {
18 commit('AUTH_ERROR')
19 process.isClient ? localStorage.removeItem('token') : false
20 console.error(err)
21 })
22
23 },
24...
This is similar with the login action.
Let’s now handle logout
1// src/main.js
2
3...
4logout({commit}){
5 commit('LOGOUT')
6 process.isClient ? localStorage.removeItem('token') : false
7 delete axios.defaults.headers.common['Authorization']
8}
9...
That is it for actions.
Finally, let’s write the getters. Getters are to an application store what computed properties are to a component. They return computed information from the store state.
1// src/main.js
2
3...
4getters: {
5 isLoggedIn: state => !!state.token,
6 authStatus: state => state.status
7}
8...
Great! We have handled authentication with Vuex.
Now, let’s deal with the login and signup pages. Create a L``ogin``.vue
page in the pages
directory and add the code snippet below:
1// src/pages/Login.vue
2
3<template>
4 <Layout>
5 <form class="signup" onsubmit="return false" autocomplete="off">
6 <h1>Welcome Back</h1>
7 <h2>Don't have an account? <g-link to="/signup">Sign up</g-link></h2>
8 <div class="signup__field">
9 <input class="signup__input" type="text" v-model="user.identifier" name="email" required />
10 <label class="signup__label" for="email">Email</label>
11 </div>
12
13 <div class="signup__field">
14 <input class="signup__input" type="password" v-model="user.password" name="password" required />
15 <label class="signup__label" for="password">Password</label>
16 </div>
17
18 <button @click="login">Sign in</button>
19 </form>
20 </Layout>
21</template>
22
23<script>
24export default {
25 data(){
26 return{
27 user: {
28 identifier: '',
29 password: ''
30 }
31 }
32 },
33 methods:{
34
35 login(){
36 let identifier = this.user.identifier
37 let password = this.user.password
38 console.log({identifier, password})
39 this.$store.dispatch('login', {identifier, password})
40
41 .then(() => this.$router.push('/home'))
42 .catch((err) => {
43 this.$router.push('/')
44 console.error({err})
45 })
46 }
47 }
48}
49</script>
It’s important to note here that the Strapi login endpoint requires identifier
(email or username) and password
****fields.
Similar to the component for login, create a Si``gnup``.vue
file in the pages directory and add this code snippet:
1// src/pages/Signup.vue
2
3<template>
4 <Layout>
5 <form class="signup" onsubmit="return false" autocomplete="off">
6 <h1>Create account</h1>
7 <h2>Already have an account? <span><g-link to="/login">Sign in</g-link></span></h2>
8
9 <div class="signup__field">
10 <input class="signup__input" type="text" v-model="user.name" name="username" required />
11 <label class="signup__label" for="username">Username</label>
12 </div>
13
14 <div class="signup__field">
15 <input class="signup__input" type="email" v-model="user.email" name="email" required />
16 <label class="signup__label" for="email">Email</label>
17 </div>
18
19 <div class="signup__field">
20 <input class="signup__input" type="password" v-model="user.password" name="password" required />
21 <label class="signup__label" for="password">Password</label>
22 </div>
23
24 <button @click="register">Sign up</button>
25 </form>
26 </Layout>
27</template>
28
29<script>
30import axios from 'axios';
31export default {
32 data(){
33 return{
34 user: {
35 email: '',
36 name: '',
37 password: ''
38 }
39 }
40 },
41 methods:{
42
43 register(){
44 let reqObj = {
45 username: this.user.name,
46 email: this.user.email,
47 password: this.user.password,
48 }
49
50 this.$store.dispatch('register', reqObj)
51 .then(() => this.$router.push('/login'))
52 .catch(err => console.log(err))
53 }
54 }
55}
56</script>
Just like the login component, we dispatched the reqObj
to the Vuex store register
action. If it’s successful, then the user will be redirected to login.
Next, let’s style the login and register pages:
1// src/pages/Login.vue
2// src/pages/signup.vue
3
4<style scoped>
5
6body {
7 background-color: #753ff6;
8 width: 100%;
9 min-height: 100vh;
10
11 display: flex;
12 align-items: center;
13 justify-content: center;
14}
15
16button,
17input {
18 border: none;
19 outline: none;
20}
21
22/****************
23 FORM
24*****************/
25.signup {
26 background-color: white;
27 width: 100%;
28 max-width: 500px;
29 padding: 50px 70px;
30 display: flex;
31 flex-direction: column;
32 margin: auto;
33
34 border-radius: 20px;
35 box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1),
36 0 4px 6px -2px rgba(0, 0, 0, 0.05);
37}
38
39h1 {
40 text-align: center;
41 color: #753ff6;
42}
43h2 {
44 text-align: center;
45 font-size: 1.2rem;
46 font-weight: lighter;
47
48 margin-bottom: 40px;
49}
50
51h2 span {
52 text-decoration: underline;
53 cursor: pointer;
54 color: #753ff6;
55}
56
57/* Field */
58.signup__field {
59 display: flex;
60 flex-direction: column;
61 width: 100%;
62 position: relative;
63 margin-bottom: 50px;
64
65}
66
67.signup__field:before {
68 content: "";
69 display: inline-block;
70 position: absolute;
71 width: 0px;
72 height: 2px;
73 background: #753ff6;
74 bottom: 0;
75 left: 50%;
76 transform: translateX(-50%);
77 transition: all 0.4s ease;
78}
79
80.signup__field:hover:before {
81 width: 100%;
82}
83
84/* Input */
85.signup__input {
86 width: 100%;
87 height: 100%;
88 font-size: 1.2rem;
89 padding: 10px 2px 0;
90 border-bottom: 2px solid #e0e0e0;
91}
92
93/* Label */
94.signup__label {
95 color: #bdbdbd;
96 position: absolute;
97 top: 50%;
98 transform: translateY(-50%);
99 left: 2px;
100 font-size: 1.2rem;
101 transition: all 0.3s ease;
102}
103
104.signup__input:focus + .signup__label,
105.signup__input:valid + .signup__label {
106 top: 0;
107 font-size: 1rem;
108 background-color: white;
109}
110
111/* Button */
112button {
113 background: #753ff6;
114 color: white;
115 padding: 12px 0;
116 font-size: 1.2rem;
117 border-radius: 25px;
118 cursor: pointer;
119}
120
121button:hover {
122 background: #753ff6;
123}
124
125@media (max-width: 700px){
126 .signup{
127 padding: .4rem;
128 }
129}
130</style>
Great! Let’s add a feature that shows the authentication status based on our vuex store state. Add this code snippet to your layout component:
1// src/layouts/default.vue
2
3<template>
4 ...
5 <p style="color: #fff"> Auth Status: <span>{{authStatus}}</span> </p>
6 ...
7</template>
8<script>
9...
10 computed: {
11 authStatus(){
12 return this.$store.getters.authStatus
13 }
14 }
15...
16};
Here, we get the authentication status state by accessing the Vuex store getters in our component via this.$store
.
Now, let’s create the page the user will be redirected to after login is successful.
At this point, the user has access to the courses, we will now have to fetch the courses data from our Strapi API. We will handle this with the Gridsome server API.
Gridsome server API allows you to manipulate the server-side features of the framework. In your project, you should have a gridsome.server.js
. Here you would export a function that receives an API that will let you perform actions such as:
In our case, what we need is to create custom pages programmatically from an external API. To enable this, we will make use of the DataStore API. The Data Store API fetches the data from our Strapi API endpoint and makes it available in our components/pages through GraphQL.
Let’s get to it already.
Replace the existing code in gridsome.server.js
with this:
1// gridsome.server.js
2
3const axios = require('axios')
4module.exports = function (api) {
5 api.loadSource(async actions => {
6 const { data } = await axios.get(`${process.env.STRAPI_URL}/strapi-courses/`)
7 const collection = actions.addCollection({
8 typeName: 'Course',
9 path: '/course/:id'
10 })
11 for(const course of data) {
12 collection.addNode({
13 id: course.id,
14 path: '/course/' + course.id,
15 title: course.course_title,
16 description: course.short_description,
17 course_image: course.course_image,
18 course_video: course.course_video
19 })
20 }
21 })
22}
In this code snippet, the collection
is equivalent to creating an empty array. typeName
is a required parameter and we assign the template ****name to it while path
would define a dynamic route using the node.id
. A node contains the specific data we want to get from our courses API such as title, course_image, etc.
Let’s now use these nodes on our courses page. Create a home.vue
page in the pages directory and add the following code:
1// src/pages/home.vue
2<template>
3<Layout>
4 <div>
5 <hr />
6
7 <div
8 class="course_list"
9 v-for="course in $page.allCourse.edges"
10 :key="course.node.id"
11 >
12 <div>
13 <g-image
14 :src="course.node.course_image.url"
15 alt=""
16 class="course-image"
17 />
18 </div>
19
20 <div class="course-content">
21 <h3>{{ course.node.title }}</h3>
22 <p>{{ course.node.description }}</p>
23 <div v-if="disableCourse">
24 <button
25 @click="$router.push(`/course/${course.node.id}`)"
26 class="btn"
27 >
28 Start Course
29 </button>
30 </div>
31
32 <div v-else>
33 <button @click="logout" class="btn ">
34 Login to start course
35 </button>
36 </div>
37 </div>
38 </div>
39 </div>
40</Layout>
41</template>
42
43<page-query>
44 query {
45 allCourse {
46 edges {
47 node {
48 id
49 title
50 description
51 course_image{
52 url
53 }
54 }
55 }
56 }
57 }
58</page-query>
59
60<script>
61export default {
62 data() {
63 return {
64 courses: [],
65 };
66 },
67 computed: {
68 disableCourse() {
69 if (this.$store.getters.authStatus === 'success') {
70 return true;
71 } else if (this.$store.getters.authStatus === 'error') {
72 return false;
73 }
74 },
75 },
76 mounted() {
77 this.courses = this.$page.allCourse.edges;
78 },
79 methods: {
80 logout() {
81 this.$store
82 .dispatch('logout')
83 .then(() => {
84 this.$router.push('/login');
85 })
86 .catch(err => {
87 console.log(err);
88 });
89 },
90 },
91};
92</script>
93
94<style scoped>
95.course_list {
96 display: flex;
97 margin-top: 2rem;
98 margin-bottom: 2rem;
99 /* justify-content: space-between; */
100 color: #fff;
101 border-bottom: 2px solid #fff;
102}
103
104.btn {
105 border: none;
106 box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25);
107 cursor: pointer;
108 height: 4.375em;
109 border-radius: 1.5em;
110 padding: 0 1.5em;
111 background-color: rgb(87, 41, 178);
112 color: #fff;
113 font-size: 14px;
114 font-weight: 600;
115 font-family: 'Karla';
116 outline: none;
117}
118
119.course-content {
120 margin-right: 5rem;
121 margin-left: 5rem;
122}
123
124.price {
125 font-weight: 700;
126}
127
128@media (max-width: 700px) {
129 .course_list {
130 display: flex;
131 flex-wrap: wrap;
132 }
133
134 .course-content {
135 margin: 0;
136 }
137
138 img {
139 width: 100%;
140 }
141}
142</style>
Let’s go through some of the working parts of this code snippet. Let’s start by examining the query:
1<page-query>
2 query {
3 allCourse {
4 edges {
5 node {
6 id
7 title
8 description
9 price
10 course_image{
11 url
12 }
13 }
14 }
15 }
16 }
17</page-query>
Remember, our data is now in the Gridsome's GraphQL data layer. We are now querying data into this page by using <page-query>
. GraphQL queries always start with query
, allCourse
is the name of the collection, edges
represents the array of courses and node is how we get the specific course data we want on the page.
1mounted() {
2 this.courses = this.$page.allCourse.edges;
3},
Here, we assign the array that has all the courses to an empty courses
array. More important to note that the results of our graphql query are stored in $page
for pages. If we were querying data into a component, we will use <static-query>
and the results will be stored in $static
.
1<div v-if="disableCourse">
2 <button
3 @click="$router.push(`/course/${course.node.id}`)"
4 class="btn"
5 >
6 Start Course
7 </button>
8</div>
9
10<div v-else>
11 <button @click="logout" class="btn ">
12 Login to start course
13 </button>
14</div>
Finally, this code snippet performs the following logical conditions:
Let’s now work on a single course using Gridsome Templates. Gridsome templates are used to create single pages for nodes in collections. In our project, we have sourced our nodes from the Strapi API.
Go to the templates directory and create a Course.vue
file. Add the following code:
1// src/templates/Course.vue
2
3<template>
4 <Layout>
5 <h1>{{ $page.course.title }}</h1>
6
7 <div class="course_desc">
8 <div class="long">
9 <p>{{ $page.course.description }}</p>
10 </div>
11 <img :src="$page.course.course_image.url" class="image" alt="" />
12 </div>
13
14 <div class="video" v-if="$page.course.course_video">
15 <video controls class="video-width">
16 <source
17 :src="$page.course.course_video.url"
18 :type="$page.course.course_video.mime"
19 />
20
21 Sorry, your browser doesn't support embedded videos.
22 </video>
23 </div>
24 </Layout>
25</template>
26
27<page-query>
28query ($id: ID!){
29 course(id: $id) {
30 title
31 description
32 course_image{
33 url
34 }
35 course_video{
36 url
37 mime
38 }
39 }
40}
41
42</page-query>
43
44<script>
45export default {
46 mounted(){
47 console.log('vidoe', this.$page.course )
48 }
49}
50</script>
51
52<style scoped>
53.course_desc {
54 display: flex;
55 align-items: center;
56 flex-wrap: wrap;
57 color: #fff;
58 justify-content: space-between;
59}
60.long {
61 max-width: 50%;
62 flex-basis: 50%;
63}
64
65h1 {
66 color: #fff;
67}
68.image {
69 height: 200px;
70}
71.video {
72 margin-top: 5rem;
73 margin-bottom: 5rem;
74}
75.video-width {
76 width: 100%;
77}
78
79@media (max-width: 700px) {
80 .long {
81 max-width: 100%;
82 flex-basis: 100%;
83 }
84 .image {
85 width: 100%;
86 height: 100%;
87 }
88}
89</style>
Awesome! We just have to deal with two more features.
First, when a user is logged in or vice versa, the information on the Navbar
should reflect the authentication state. Replace your Navbar.vue
code with this:
1// src/components/Navbar.vue
2
3<template>
4 <div class="navbar">
5 <nav>
6 <div>
7 <h1>
8 <g-link to="/"> Strapi Learning Platform </g-link>
9 </h1>
10 </div>
11 <ul>
12 <div v-if="disableNav" class="ul">
13 <li>Welcome, {{ user.username }}</li>
14 <li>
15 <button class="btn btn-outline" @click="logout">
16 Logout
17 </button>
18 </li>
19 </div>
20 <div v-else class="ul">
21 <li>
22 <g-link to="/signup" class="btn"> Register </g-link>
23 </li>
24 <li>
25 <g-link to="/login" class="btn btn-outline">
26 Login
27 </g-link>
28 </li>
29 </div>
30 </ul>
31 </nav>
32 </div>
33</template>
34
35<script>
36export default {
37 data() {
38 return {
39 user: {},
40 };
41 },
42 computed: {
43 isLoggedIn() {
44 return this.$store.getters.isLoggedIn;
45 },
46
47 disableNav() {
48 if (this.$store.getters.authStatus === 'success') {
49 return true;
50 } else if (this.$store.getters.authStatus === 'error') {
51 return false;
52 }
53 },
54 },
55
56 mounted() {
57 this.user = JSON.parse(localStorage.getItem('user'));
58 },
59 methods: {
60 logout() {
61 this.$store
62 .dispatch('logout')
63 .then(() => {
64 console.log('I am serious');
65 this.$router.push('/login');
66 })
67 .catch(err => {
68 console.log(err);
69 });
70 },
71 },
72};
73</script>
74
75<style scoped>
76.navbar {
77 width: 100%;
78}
79h1 a {
80 color: #fff;
81 text-decoration: none;
82}
83nav {
84 display: flex;
85 justify-content: space-between;
86 align-items: center;
87}
88
89ul {
90 list-style-type: none;
91 margin: 0;
92 padding: 0;
93}
94
95.ul {
96 display: flex;
97 align-items: baseline;
98}
99
100.btn {
101 border: none;
102 box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25);
103 cursor: pointer;
104 height: 4.375em;
105 border-radius: 1.5em;
106 text-decoration: none;
107 padding: 1.5em 1.5em;
108 background-color: rgb(87, 41, 178);
109 color: #fff;
110 font-size: 14px;
111 font-weight: 600;
112 font-family: 'Karla';
113 outline: none;
114}
115
116.btn-outline {
117 background: #fff;
118 border: 1px solid rgb(87, 41, 178);
119 color: #000;
120}
121
122li {
123 margin-left: 0.5rem;
124 margin-right: 0.5rem;
125 color: #fff;
126}
127
128.nav-link {
129 color: #fff;
130 text-decoration: none;
131}
132
133@media (max-width: 700px) {
134 ul {
135 justify-content: space-between;
136 }
137}
138</style>
Finally, the i``ndex
page should also be aware of the user’s authentication status:
1// src/pages/index.vue
2
3<template>
4 <Layout>
5 <section class="hero-section">
6 <div v-if="!authStatus">
7 <h1>Welcome!</h1>
8
9 <p>
10 Strapi is the leading open-source headless CMS. It’s 100%
11 Javascript, fully customizable and developer-first.
12 </p>
13
14 <p>
15 <g-link to="/login"> Login </g-link> to enjoy all our
16 courses here
17 </p>
18 </div>
19
20 <div v-else>
21 <h2>You're logged in</h2>
22
23 <p>
24 Continue viewing your <g-link to="/home"> courses </g-link>
25 </p>
26 </div>
27 </section>
28 </Layout>
29</template>
30
31<script>
32export default {
33 metaInfo: {
34 title: 'Learning Platform',
35 },
36 computed: {
37 authStatus() {
38 if (this.$store.getters.authStatus === 'success') {
39 return true;
40 } else if (this.$store.getters.authStatus === 'error') {
41 return false;
42 } else if (this.$store.getters.authStatus === '') {
43 return false;
44 }
45 },
46 },
47};
48</script>
49
50<style>
51.home-links a {
52 margin-right: 1rem;
53}
54
55.hero-section {
56 background: #fff;
57 color: #2f2e8b;
58 padding: 1rem;
59}
60</style>
Save and restart the Gridsome dev server.
Congratulations!! 🙌
In this tutorial, you didn’t just build a learning platform but also in the process, picked up insights in using Strapi as a headless CMS and Gridsome for building statically generated websites/apps. I am confident that even if this was your first time using Strapi or Gridsome, you’d be capable of building or handling tasks relating to any of the tools. Stuck somewhere? Here is the source code on GitHub.