Simply copy and paste the following command line in your terminal to create your first Strapi project.
npx create-strapi-app
my-project
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:
1
2
3
yarn create strapi-app strapi-learning-backend
// or
npx 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.
1
2
3
yarn develop
// or
npm 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 :
1
2
3
4
5
course_title: Strapi CMS with Gatsby,
short_description: Learn how to use Strapi CMS with Gatsby to build powerful web applications,
long_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,
course_image: https://res.cloudinary.com/samtech/image/upload/v1608285507/with_gatsby_e4dca7b0c3_9e4d8db26e.png,
course_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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// api/strapi-courses/config/routes.json
{
"routes": [
{
"method": "GET",
"path": "/strapi-courses",
"handler": "strapi-courses.find",
"config": {
"policies": []
}
},
{
"method": "GET",
"path": "/strapi-courses/count",
"handler": "strapi-courses.count",
"config": {
"policies": []
}
},
{
"method": "GET",
"path": "/strapi-courses/:id",
"handler": "strapi-courses.findOne",
"config": {
"policies": []
}
},
{
"method": "POST",
"path": "/strapi-courses",
"handler": "strapi-courses.create",
"config": {
"policies": []
}
},
{
"method": "PUT",
"path": "/strapi-courses/:id",
"handler": "strapi-courses.update",
"config": {
"policies": []
}
},
{
"method": "DELETE",
"path": "/strapi-courses/:id",
"handler": "strapi-courses.delete",
"config": {
"policies": []
}
}
]
}
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.
1
yarn add strapi-provider-upload-cloudinary
To register the plugin, go to the config directory and create a plugins.js
file:
1
2
3
4
5
6
7
8
9
10
11
12
// config/plugin.js
module.exports = ({ env }) => ({
upload: {
provider: 'cloudinary',
providerOptions: {
cloud_name: env('CLOUDINARY_NAME', 'YOUR_CLOUDINARY NAME'),
api_key: env('CLOUDINARY_KEY', 'YOUR_CLOUDINARY_KEY'),
api_secret: env('CLOUDINARY_SECRET', 'YOUR_CLOUDINARY_API_SECRET')
}
}
})
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.
1
2
3
4
5
course_title: Strapi CMS with Vue,
short_description: Learn how to build web apps with Strapi CMS and Vuejs,
long_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.,
download this image or just upload via url: https://res.cloudinary.com/samtech/image/upload/v1608287448/with_vue_9386f31664_ccce641ecf.png,
download 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:
1
npm 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:
1
gridsome create strapi-learning
This might take some time but when it’s done, go to the project directory by running this command:
1
cd 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
2
3
4
5
6
7
8
9
10
11
12
// src/main.js
import DefaultLayout from '~/layouts/Default.vue'
export default function (Vue) {
head.link.push({
rel: 'stylesheet',
href: 'https://fonts.googleapis.com/css2?family=Karla&display=swap',
})
// Set default layout as a global component
Vue.component('Layout', DefaultLayout)
}
Now, head over to src/layouts/Default.vue
and replace the existing project’s Default.vue
code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
// src/layouts/Default.vue
<template>
<div class="layout">
<header class="header">
<Navbar />
</header>
<p style="color: #fff"> Auth Status: <span>{{authStatus}}</span> </p>
<slot />
</div>
</template>
<static-query>
query {
metadata {
siteName
}
}
</static-query>
<script>
import Navbar from '~/components/Navbar';
export default {
components: {
Navbar,
},
computed: {
authStatus(){
return this.$store.getters.authStatus
}
}
};
</script>
<style>
body {
font-family: 'Karla', sans-serif;
margin: 0;
padding: 0;
background: #753ff6;
line-height: 1.5;
}
span{
text-transform: uppercase;
}
.layout {
max-width: 950px;
margin: 0 auto;
padding-left: 20px;
padding-right: 20px;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.nav__link {
margin-left: 20px;
}
@media (max-width: 700px){
nav{
flex-direction: column;
margin-bottom: 1rem;
}
}
</style>
Let’s now create the Navbar
component. Go to src/Components
and create a Navbar.vue
file with the following content:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
// src/Components/Navbar.vue
<template>
<div class="navbar">
<nav>
<div>
<h1>
<g-link to="/"> Strapi Learning Platform </g-link>
</h1>
</div>
<ul>
<div v-if="disableNav" class="ul">
<li>Welcome, {{ user.username }}</li>
<li>
<button class="btn btn-outline" @click="logout">
Logout
</button>
</li>
</div>
<div v-else class="ul">
<li>
<g-link to="/signup" class="btn"> Register </g-link>
</li>
<li>
<g-link to="/login" class="btn btn-outline">
Login
</g-link>
</li>
</div>
</ul>
</nav>
</div>
</template>
<script>
export default {
data() {
return {
user: {},
};
},
computed: {
isLoggedIn() {
return this.$store.getters.isLoggedIn;
},
disableNav() {
if (this.$store.getters.authStatus === 'success') {
return true;
} else if (this.$store.getters.authStatus === 'error') {
return false;
}
},
},
mounted() {
this.user = JSON.parse(localStorage.getItem('user'));
},
methods: {
logout() {
this.$store
.dispatch('logout')
.then(() => {
console.log('I am serious');
this.$router.push('/login');
})
.catch(err => {
console.log(err);
});
},
},
};
</script>
<style scoped>
.navbar {
width: 100%;
}
h1 a {
color: #fff;
text-decoration: none;
}
nav {
display: flex;
justify-content: space-between;
align-items: center;
}
ul {
list-style-type: none;
margin: 0;
padding: 0;
}
.ul {
display: flex;
align-items: baseline;
}
.btn {
border: none;
box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25);
cursor: pointer;
height: 4.375em;
border-radius: 1.5em;
text-decoration: none;
padding: 1.5em 1.5em;
background-color: rgb(87, 41, 178);
color: #fff;
font-size: 14px;
font-weight: 600;
font-family: 'Karla';
outline: none;
}
.btn-outline {
background: #fff;
border: 1px solid rgb(87, 41, 178);
color: #000;
}
li {
margin-left: 0.5rem;
margin-right: 0.5rem;
color: #fff;
}
.nav-link {
color: #fff;
text-decoration: none;
}
@media (max-width: 700px) {
ul {
justify-content: space-between;
}
}
</style>
One more thing. Let’s replace the code in ~/pages/Index.vue
with this code snippet:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// src/pages/Index.vue
<template>
<Layout>
<section class="hero-section">
<h1>Welcome!</h1>
<p>
Strapi is the leading open-source headless CMS. It’s 100%
Javascript, fully customizable and developer-first.
</p>
<p>
<g-link to="/login"> Login </g-link> to enjoy all our courses
here
</p>
</section>
</Layout>
</template>
<style>
.home-links a {
margin-right: 1rem;
}
.hero-section {
background: #fff;
color: #2f2e8b;
padding: 1rem;
}
</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:
1
npm install axios vuex
We will need Axios for our HTTP requests. Open the src/main.js
file and add the following:
1
2
3
4
5
6
7
8
9
// src/main.js
...
Vue.prototype.$http = axios;
const token = process.isClient ? localStorage.getItem(`Bearer ${token}`) : false
if (token) {
Vue.prototype.$http.defaults.headers.common['Authorization'] = `Bearer ${token}`
}
...
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// src/main.js
export default function (Vue, {
head,
appOptions
}) {
...
Vue.use(Vuex)
appOptions.store = new Vuex.Store({
state: {
},
mutations:{},
actions: {},
getters: {}
...
}
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
2
3
4
5
6
7
8
9
// src/main.js
...
state:{
status: '',
token: process.isClient ? localStorage.getItem('token') || '' : false,
user: {}
},
...
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// src/main.js
mutations: {
AUTH_REQUEST(state) {
state.status = 'loading'
},
AUTH_SUCCESS(state, token, user) {
state.status = 'success',
state.token = token,
state.user = user
},
AUTH_ERROR(state) {
state.status = 'error'
},
LOGOUT(state) {
state.status = 'logged out',
state.token = ''
}
},
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
2
// .env
STRAPI_URL= <your strapi api url here> or http://localhost:1337
Let’s start with the action for login:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// src/main.js
...
actions: {
async login({ commit }, user) {
commit('AUTH_REQUEST')
await axios.post(`${process.env.STRAPI_URL}/auth/local/`, user)
.then(response => {
const token = response.data.jwt
const user = response.data.user
if (process.isClient) {
localStorage.setItem('token', token)
localStorage.setItem('user', JSON.stringify(user))
}
axios.defaults.headers.common['Authorization'] = `Bearer ${token}`
const something = axios.defaults.headers.common['Authorization']
console.log({something})
commit('AUTH_SUCCESS', token, user)
console.log({user, token})
})
.catch(err => {
commit('AUTH_ERROR')
process.isClient ? localStorage.removeItem('token') : false
console.error(err)
})
},
}
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// src/main.js
...
async register({commit}, user) {
commit('AUTH_REQUEST')
await axios.post(`${process.env.STRAPI_URL}/auth/local/register`, user)
.then(response => {
const token = response.data.jwt
const user = response.data.user
process.isClient ? localStorage.setItem('token', token) : false
axios.defaults.headers.common['Authorization'] = `Bearer ${token}`
commit('AUTH_SUCCESS', token, user)
})
.catch(err => {
commit('AUTH_ERROR')
process.isClient ? localStorage.removeItem('token') : false
console.error(err)
})
},
...
This is similar with the login action.
Let’s now handle logout
1
2
3
4
5
6
7
8
9
// src/main.js
...
logout({commit}){
commit('LOGOUT')
process.isClient ? localStorage.removeItem('token') : false
delete axios.defaults.headers.common['Authorization']
}
...
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
2
3
4
5
6
7
8
// src/main.js
...
getters: {
isLoggedIn: state => !!state.token,
authStatus: state => state.status
}
...
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// src/pages/Login.vue
<template>
<Layout>
<form class="signup" onsubmit="return false" autocomplete="off">
<h1>Welcome Back</h1>
<h2>Don't have an account? <g-link to="/signup">Sign up</g-link></h2>
<div class="signup__field">
<input class="signup__input" type="text" v-model="user.identifier" name="email" required />
<label class="signup__label" for="email">Email</label>
</div>
<div class="signup__field">
<input class="signup__input" type="password" v-model="user.password" name="password" required />
<label class="signup__label" for="password">Password</label>
</div>
<button @click="login">Sign in</button>
</form>
</Layout>
</template>
<script>
export default {
data(){
return{
user: {
identifier: '',
password: ''
}
}
},
methods:{
login(){
let identifier = this.user.identifier
let password = this.user.password
console.log({identifier, password})
this.$store.dispatch('login', {identifier, password})
.then(() => this.$router.push('/home'))
.catch((err) => {
this.$router.push('/')
console.error({err})
})
}
}
}
</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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// src/pages/Signup.vue
<template>
<Layout>
<form class="signup" onsubmit="return false" autocomplete="off">
<h1>Create account</h1>
<h2>Already have an account? <span><g-link to="/login">Sign in</g-link></span></h2>
<div class="signup__field">
<input class="signup__input" type="text" v-model="user.name" name="username" required />
<label class="signup__label" for="username">Username</label>
</div>
<div class="signup__field">
<input class="signup__input" type="email" v-model="user.email" name="email" required />
<label class="signup__label" for="email">Email</label>
</div>
<div class="signup__field">
<input class="signup__input" type="password" v-model="user.password" name="password" required />
<label class="signup__label" for="password">Password</label>
</div>
<button @click="register">Sign up</button>
</form>
</Layout>
</template>
<script>
import axios from 'axios';
export default {
data(){
return{
user: {
email: '',
name: '',
password: ''
}
}
},
methods:{
register(){
let reqObj = {
username: this.user.name,
email: this.user.email,
password: this.user.password,
}
this.$store.dispatch('register', reqObj)
.then(() => this.$router.push('/login'))
.catch(err => console.log(err))
}
}
}
</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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
// src/pages/Login.vue
// src/pages/signup.vue
<style scoped>
body {
background-color: #753ff6;
width: 100%;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
button,
input {
border: none;
outline: none;
}
/****************
FORM
*****************/
.signup {
background-color: white;
width: 100%;
max-width: 500px;
padding: 50px 70px;
display: flex;
flex-direction: column;
margin: auto;
border-radius: 20px;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1),
0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
h1 {
text-align: center;
color: #753ff6;
}
h2 {
text-align: center;
font-size: 1.2rem;
font-weight: lighter;
margin-bottom: 40px;
}
h2 span {
text-decoration: underline;
cursor: pointer;
color: #753ff6;
}
/* Field */
.signup__field {
display: flex;
flex-direction: column;
width: 100%;
position: relative;
margin-bottom: 50px;
}
.signup__field:before {
content: "";
display: inline-block;
position: absolute;
width: 0px;
height: 2px;
background: #753ff6;
bottom: 0;
left: 50%;
transform: translateX(-50%);
transition: all 0.4s ease;
}
.signup__field:hover:before {
width: 100%;
}
/* Input */
.signup__input {
width: 100%;
height: 100%;
font-size: 1.2rem;
padding: 10px 2px 0;
border-bottom: 2px solid #e0e0e0;
}
/* Label */
.signup__label {
color: #bdbdbd;
position: absolute;
top: 50%;
transform: translateY(-50%);
left: 2px;
font-size: 1.2rem;
transition: all 0.3s ease;
}
.signup__input:focus + .signup__label,
.signup__input:valid + .signup__label {
top: 0;
font-size: 1rem;
background-color: white;
}
/* Button */
button {
background: #753ff6;
color: white;
padding: 12px 0;
font-size: 1.2rem;
border-radius: 25px;
cursor: pointer;
}
button:hover {
background: #753ff6;
}
@media (max-width: 700px){
.signup{
padding: .4rem;
}
}
</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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// src/layouts/default.vue
<template>
...
<p style="color: #fff"> Auth Status: <span>{{authStatus}}</span> </p>
...
</template>
<script>
...
computed: {
authStatus(){
return this.$store.getters.authStatus
}
}
...
};
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// gridsome.server.js
const axios = require('axios')
module.exports = function (api) {
api.loadSource(async actions => {
const { data } = await axios.get(`${process.env.STRAPI_URL}/strapi-courses/`)
const collection = actions.addCollection({
typeName: 'Course',
path: '/course/:id'
})
for(const course of data) {
collection.addNode({
id: course.id,
path: '/course/' + course.id,
title: course.course_title,
description: course.short_description,
course_image: course.course_image,
course_video: course.course_video
})
}
})
}
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
// src/pages/home.vue
<template>
<Layout>
<div>
<hr />
<div
class="course_list"
v-for="course in $page.allCourse.edges"
:key="course.node.id"
>
<div>
<g-image
:src="course.node.course_image.url"
alt=""
class="course-image"
/>
</div>
<div class="course-content">
<h3>{{ course.node.title }}</h3>
<p>{{ course.node.description }}</p>
<div v-if="disableCourse">
<button
@click="$router.push(`/course/${course.node.id}`)"
class="btn"
>
Start Course
</button>
</div>
<div v-else>
<button @click="logout" class="btn ">
Login to start course
</button>
</div>
</div>
</div>
</div>
</Layout>
</template>
<page-query>
query {
allCourse {
edges {
node {
id
title
description
course_image{
url
}
}
}
}
}
</page-query>
<script>
export default {
data() {
return {
courses: [],
};
},
computed: {
disableCourse() {
if (this.$store.getters.authStatus === 'success') {
return true;
} else if (this.$store.getters.authStatus === 'error') {
return false;
}
},
},
mounted() {
this.courses = this.$page.allCourse.edges;
},
methods: {
logout() {
this.$store
.dispatch('logout')
.then(() => {
this.$router.push('/login');
})
.catch(err => {
console.log(err);
});
},
},
};
</script>
<style scoped>
.course_list {
display: flex;
margin-top: 2rem;
margin-bottom: 2rem;
/* justify-content: space-between; */
color: #fff;
border-bottom: 2px solid #fff;
}
.btn {
border: none;
box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25);
cursor: pointer;
height: 4.375em;
border-radius: 1.5em;
padding: 0 1.5em;
background-color: rgb(87, 41, 178);
color: #fff;
font-size: 14px;
font-weight: 600;
font-family: 'Karla';
outline: none;
}
.course-content {
margin-right: 5rem;
margin-left: 5rem;
}
.price {
font-weight: 700;
}
@media (max-width: 700px) {
.course_list {
display: flex;
flex-wrap: wrap;
}
.course-content {
margin: 0;
}
img {
width: 100%;
}
}
</style>
Let’s go through some of the working parts of this code snippet. Let’s start by examining the query:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<page-query>
query {
allCourse {
edges {
node {
id
title
description
price
course_image{
url
}
}
}
}
}
</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.
1
2
3
mounted() {
this.courses = this.$page.allCourse.edges;
},
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
2
3
4
5
6
7
8
9
10
11
12
13
14
<div v-if="disableCourse">
<button
@click="$router.push(`/course/${course.node.id}`)"
class="btn"
>
Start Course
</button>
</div>
<div v-else>
<button @click="logout" class="btn ">
Login to start course
</button>
</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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
// src/templates/Course.vue
<template>
<Layout>
<h1>{{ $page.course.title }}</h1>
<div class="course_desc">
<div class="long">
<p>{{ $page.course.description }}</p>
</div>
<img :src="$page.course.course_image.url" class="image" alt="" />
</div>
<div class="video" v-if="$page.course.course_video">
<video controls class="video-width">
<source
:src="$page.course.course_video.url"
:type="$page.course.course_video.mime"
/>
Sorry, your browser doesn't support embedded videos.
</video>
</div>
</Layout>
</template>
<page-query>
query ($id: ID!){
course(id: $id) {
title
description
course_image{
url
}
course_video{
url
mime
}
}
}
</page-query>
<script>
export default {
mounted(){
console.log('vidoe', this.$page.course )
}
}
</script>
<style scoped>
.course_desc {
display: flex;
align-items: center;
flex-wrap: wrap;
color: #fff;
justify-content: space-between;
}
.long {
max-width: 50%;
flex-basis: 50%;
}
h1 {
color: #fff;
}
.image {
height: 200px;
}
.video {
margin-top: 5rem;
margin-bottom: 5rem;
}
.video-width {
width: 100%;
}
@media (max-width: 700px) {
.long {
max-width: 100%;
flex-basis: 100%;
}
.image {
width: 100%;
height: 100%;
}
}
</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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
// src/components/Navbar.vue
<template>
<div class="navbar">
<nav>
<div>
<h1>
<g-link to="/"> Strapi Learning Platform </g-link>
</h1>
</div>
<ul>
<div v-if="disableNav" class="ul">
<li>Welcome, {{ user.username }}</li>
<li>
<button class="btn btn-outline" @click="logout">
Logout
</button>
</li>
</div>
<div v-else class="ul">
<li>
<g-link to="/signup" class="btn"> Register </g-link>
</li>
<li>
<g-link to="/login" class="btn btn-outline">
Login
</g-link>
</li>
</div>
</ul>
</nav>
</div>
</template>
<script>
export default {
data() {
return {
user: {},
};
},
computed: {
isLoggedIn() {
return this.$store.getters.isLoggedIn;
},
disableNav() {
if (this.$store.getters.authStatus === 'success') {
return true;
} else if (this.$store.getters.authStatus === 'error') {
return false;
}
},
},
mounted() {
this.user = JSON.parse(localStorage.getItem('user'));
},
methods: {
logout() {
this.$store
.dispatch('logout')
.then(() => {
console.log('I am serious');
this.$router.push('/login');
})
.catch(err => {
console.log(err);
});
},
},
};
</script>
<style scoped>
.navbar {
width: 100%;
}
h1 a {
color: #fff;
text-decoration: none;
}
nav {
display: flex;
justify-content: space-between;
align-items: center;
}
ul {
list-style-type: none;
margin: 0;
padding: 0;
}
.ul {
display: flex;
align-items: baseline;
}
.btn {
border: none;
box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25);
cursor: pointer;
height: 4.375em;
border-radius: 1.5em;
text-decoration: none;
padding: 1.5em 1.5em;
background-color: rgb(87, 41, 178);
color: #fff;
font-size: 14px;
font-weight: 600;
font-family: 'Karla';
outline: none;
}
.btn-outline {
background: #fff;
border: 1px solid rgb(87, 41, 178);
color: #000;
}
li {
margin-left: 0.5rem;
margin-right: 0.5rem;
color: #fff;
}
.nav-link {
color: #fff;
text-decoration: none;
}
@media (max-width: 700px) {
ul {
justify-content: space-between;
}
}
</style>
Finally, the i``ndex
page should also be aware of the user’s authentication status:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
// src/pages/index.vue
<template>
<Layout>
<section class="hero-section">
<div v-if="!authStatus">
<h1>Welcome!</h1>
<p>
Strapi is the leading open-source headless CMS. It’s 100%
Javascript, fully customizable and developer-first.
</p>
<p>
<g-link to="/login"> Login </g-link> to enjoy all our
courses here
</p>
</div>
<div v-else>
<h2>You're logged in</h2>
<p>
Continue viewing your <g-link to="/home"> courses </g-link>
</p>
</div>
</section>
</Layout>
</template>
<script>
export default {
metaInfo: {
title: 'Learning Platform',
},
computed: {
authStatus() {
if (this.$store.getters.authStatus === 'success') {
return true;
} else if (this.$store.getters.authStatus === 'error') {
return false;
} else if (this.$store.getters.authStatus === '') {
return false;
}
},
},
};
</script>
<style>
.home-links a {
margin-right: 1rem;
}
.hero-section {
background: #fff;
color: #2f2e8b;
padding: 1rem;
}
</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.