Our community is looking for talented writers passionate about our ecosystem (Jamstack, open-source, javascript) and willing to share their knowledge/experiences through our Write for the community program.
Note: The content of this tutorial was revised and updated on February 8, 2022. Some other information, such as the title, might have been updated later.
If you are familiar with our blog you must have seen that we've released a series of tutorials on how to make blogs using Strapi with a lot of frontend frameworks:
Blogging is excellent for letting you share experiences, beliefs, or testimonials. And Strapi is useful in helping you create your blog! So, I am pretty sure that you now understand what this post is about. Let’s learn how to create a blog with your favorite tech: Strapi.
Vue.js is an open-source approachable, performant and versatile framework for building web user interfaces. It was created by Evan You and is maintained by him and the rest of the active core team members.
The goal here is to be able to create a simple static blog website using Strapi as the backend and Vue.js for the frontend We will make this tutorial shorter and more efficient using our new templates.
This tutorial will always use the latest version of Strapi. That is awesome, right!? You'll understand why below. You need to have node v.12 installed, and that's all.
take blog-strapi
This is the easiest part of this tutorial, thanks to our expansion team, who developed a series of Strapi templates that you can use for some different use cases.
These templates are Strapi applications containing existing collection types and single types suited for the appropriate use case and data. In this tutorial, we'll use the Blog template and connect a React application.
# Using yarn
yarn create strapi-app backend --quickstart --template @strapi/template-blog@1.0.0 blog
# Using npm
npx create-strapi-app backend --quickstart --template @strapi/template-blog@1.0.0 blog
Don't forget that Strapi is running on http://localhost:1337. Create your admin user by signing up!
That's it! You're done with Strapi! I'm not kidding, we can start to create our React application now in order to fetch our content from Strapi.
Ok ok wait, let's talk about this amazing template you just created.
You should know that before we had starters and templates, we only had tutorials. The idea of creating starters came to us when we realized that we could do something with the result of our tutorials. Thus our starters were born. However, Strapi evolves quickly, very quickly. At the time, starters comprised a repository that included the back and frontend. This meant that updating the Strapi version on all our starters took too much time. Therefore we decided to develop templates that are always created with the latest versions of Strapi. This was achieved simply by passing the repository parameter to the desired template like you just did. This method also gives a recommended architecture for your Strapi project.
These templates provide a solid basis for your Strapi application and include the following:
Feel free to modify any of this if you want. However, for this tutorial, this initial setup should be enough.
Nice! Now that Strapi is ready, you will create your React application.
Well, the easiest part has been completed; let's get our hands dirty developing our blog!
Vue setup
yarn global add @vue/cli
Create a Vue front-end
server by running the following command:
vue create frontend
Note: The terminal will prompt for some details about your project. Chose default Vue 3 (babel, eslint)
. Go ahead and press enter all the way!
Once the installation is over, you can start your frontend app to ensure everything went ok.
1cd frontend
2yarn serve
As you might want people to read your blog or to make it "cute & pretty" we will use a popular CSS framework for styling: UiKit
and also Apollo
to query Strapi with GraphQL:
Dependencies setup
Make sure you are in the frontend
folder before running the following commands:
Apollo setup
yarn add graphql graphql-tag @apollo/client @vue/apollo-option
vue-apollo.js
file inside your src
folder containing the following code:1import { ApolloClient } from "apollo-client";
2import { createHttpLink } from "apollo-link-http";
3import { InMemoryCache } from "apollo-cache-inmemory";
4
5// HTTP connection to the API
6const httpLink = createHttpLink({
7 // You should use an absolute URL here
8 uri: process.env.VUE_APP_GRAPHQL_URL || "http://localhost:1337/graphql",
9});
10
11// Cache implementation
12const cache = new InMemoryCache();
13
14// Create the apollo client
15const apolloClient = new ApolloClient({
16 link: httpLink,
17 cache,
18});
19
20export default apolloClient;
As you can see we are using a VUE_APP_GRAPHQL_URL
env variable, let's create it in a .env
file:
./env
file at the root of your frontend application containing the following line:VUE_APP_STRAPI_API_URL=http://localhost:1337
VUE_APP_GRAPHQL_URL=http://localhost:1337/graphql
You are going to use the VUE_APP_STRAPI_API_URL
later on. Now let's head to our main.js
file
./src/main.js
file to include apollo in your project:1import { createApp, h } from "vue";
2import { createApolloProvider } from "@vue/apollo-option";
3import apolloClient from "./vue-apollo";
4
5const apolloProvider = createApolloProvider({
6 defaultClient: apolloClient,
7});
8
9import App from "./App.vue";
10
11const app = createApp({
12 render: () => h(App),
13});
14
15app.use(apolloProvider);
16app.mount("#app");
Here you are defining the apollo configurations in the vue-apollo.js
and then you create the provider that you include in your Vue 3 application.
Great, apollo is ready now!
UIkit setup
UIkit is a lightweight, modular frontend framework for developing fast and powerful web interfaces.
public/index.html
file by adding the following lines:1...
2<!-- UIkit CSS -->
3<link
4 rel="stylesheet"
5 href="https://cdn.jsdelivr.net/npm/uikit@3.3.1/dist/css/uikit.min.css"
6/>
7
8<link
9 rel="stylesheet"
10 href="https://fonts.googleapis.com/css?family=Staatliches"
11/>
12
13<!-- UIkit JS -->
14<script src="https://cdn.jsdelivr.net/npm/uikit@3.3.1/dist/js/uikit.min.js"></script>
15<script src="https://cdn.jsdelivr.net/npm/uikit@3.3.1/dist/js/uikit-icons.min.js"></script>
16...
Awesome! It's time to structure our code a little bit!
App.vue
with the following one:1<template>
2 <div id="app">Hello world</div>
3</template>
4
5<script>
6export default {
7 name: "App"
8};
9</script>
10
11<style lang="css">
12a {
13 text-decoration: none;
14}
15
16h1 {
17 font-family: Staatliches;
18 font-size: 120px;
19}
20
21#category {
22 font-family: Staatliches;
23 font-weight: 500;
24}
25
26#title {
27 letter-spacing: 0.4px;
28 font-size: 22px;
29 font-size: 1.375rem;
30 line-height: 1.13636;
31}
32
33#banner {
34 margin: 20px;
35 height: 800px;
36}
37
38#editor {
39 font-size: 16px;
40 font-size: 1rem;
41 line-height: 1.75;
42}
43
44.uk-navbar-container {
45 background: #fff !important;
46 font-family: Staatliches;
47}
48
49img:hover {
50 opacity: 1;
51 transition: opacity 0.25s cubic-bezier(0.39, 0.575, 0.565, 1);
52}
53</style>
components/HelloWorld.vue
componentPerfect! You should have a blank page now! I know it's sound weird but it means that you did everything well!
First of all we are going to create the routing of our application using vue-router
vue-router
by running the following command in your terminal:yarn add vue-router
Head to your main.js
file and replace the code by the following one:
1import { createApp, h } from "vue";
2import { createRouter, createWebHashHistory } from "vue-router";
3import { createApolloProvider } from "@vue/apollo-option";
4import apolloClient from "./vue-apollo";
5import App from "./App.vue";
6
7const apolloProvider = createApolloProvider({
8 defaultClient: apolloClient,
9});
10
11const routes = [{ path: "/" }];
12
13// 3. Create the router instance and pass the `routes` option
14// You can pass in additional options here, but let's
15// keep it simple for now.
16const router = createRouter({
17 // 4. Provide the history implementation to use. We are using the hash history for simplicity here.
18 history: createWebHashHistory(),
19 routes, // short for `routes: routes`
20});
21
22const app = createApp({
23 render: () => h(App),
24});
25
26app.use(router);
27app.use(apolloProvider);
28app.mount("#app");
As you can see, we are simply importing vue-router and telling Vue to use it. We then create our routes. The first one is the main page /
and is not using any components yet, we'll do it later.
We will create a Nav that will be present on every page of your application. To do this, we will simply call it in our App.vue
components/Nav.vue
file containing the following code:1<template>
2 <div>
3 <nav class="uk-navbar-container" uk-navbar>
4 <div class="uk-navbar-left">
5 <ul class="uk-navbar-nav">
6 <li>
7 <a href="/">Strapi Blog </a>
8 </li>
9 </ul>
10 </div>
11
12 <div class="uk-navbar-right">
13 <ul class="uk-navbar-nav">
14 <li v-for="category in categories.data" v-bind:key="category.id">
15 <router-link
16 :to="{ path: '/category/' + category.attributes.slug }"
17 :key="category.attributes.slug"
18 >
19 {{ category.attributes.name }}
20 </router-link>
21 </li>
22 </ul>
23 </div>
24 </nav>
25 </div>
26</template>
27
28<script>
29import gql from "graphql-tag";
30export default {
31 name: "Nav",
32 data() {
33 return {
34 categories: [],
35 };
36 },
37 apollo: {
38 categories: gql`
39 query Categories {
40 categories {
41 data {
42 id
43 attributes {
44 slug
45 name
46 }
47 }
48 }
49 }
50 `,
51 },
52};
53</script>
Here, we are defining a categories
array that will be filled with the response of this GraphQL query:
1apollo: {
2 categories: gql`
3 query Categories {
4 categories {
5 data {
6 attributes {
7 slug
8 name
9 }
10 }
11 }
12 }
13 `
14}
Let's use this new component inside our App.vue
component
App.vue
component1<template>
2 <div id="app">
3 <Nav />
4 </div>
5</template>
6
7<script>
8import Nav from "./components/Nav.vue";
9
10export default {
11 name: "App",
12 components: { Nav }
13};
14</script>
15<style lang="css">
16...
Awesome! You should see your brand new Nav!
Note The current code is unsuitable for displaying many categories as you may encounter a UI issue. Since this blog post is supposed to be short, I will let you improve the code to add a lazy load or something maybe.
For now, the links are not working, you'll work on it later in the tutorial ;)
let's display your articles from Strapi now!
src/containers
folder and create a src/containers/Articles.vue
file containing the following code:1<template>
2 <div>
3 <div class="uk-section">
4 <div class="uk-container uk-container-large">
5 <h1>Strapi blog</h1>
6
7 <ArticlesList :articles="articles"></ArticlesList>
8 </div>
9 </div>
10 </div>
11</template>
12
13<script>
14import ArticlesList from "../components/ArticlesList.vue";
15import gql from "graphql-tag";
16
17export default {
18 components: {
19 ArticlesList,
20 },
21 data() {
22 return {
23 articles: [],
24 };
25 },
26 apollo: {
27 articles: gql`
28 query Articles {
29 articles {
30 data {
31 attributes {
32 slug
33 title
34 category {
35 data {
36 attributes {
37 slug
38 name
39 }
40 }
41 }
42 image {
43 data {
44 attributes {
45 url
46 }
47 }
48 }
49 }
50 }
51 }
52 }
53 `,
54 },
55};
56</script>
Here we are just creating the page that will use a ArticlesList
component to display our articles. We will give these articles as props from the response of this GraphQL query:
1apollo: {
2 articles: gql`
3 query Articles {
4 articles {
5 data {
6 attributes {
7 slug
8 title
9 category {
10 data {
11 attributes {
12 slug
13 name
14 }
15 }
16 }
17 image {
18 data {
19 attributes {
20 url
21 }
22 }
23 }
24 }
25 }
26 }
27 }
28 `,
29 },
components/ArticlesList.vue
file containing the following code:1<template>
2 <div>
3 <div class="uk-child-width-1-2" uk-grid>
4 <div>
5 <router-link
6 v-for="article in leftArticles"
7 :to="{ path: '/article/' + article.attributes.slug }"
8 class="uk-link-reset"
9 :key="article.attributes.slug"
10 >
11 <div class="uk-card uk-card-muted">
12 <div class="uk-card-media-top">
13 <img
14 :src="api_url + article.attributes.image.data.attributes.url"
15 alt=""
16 height="100"
17 />
18 </div>
19 <div class="uk-card-body">
20 <p
21 id="category"
22 v-if="article.attributes.category"
23 class="uk-text-uppercase"
24 >
25 {{ article.attributes.category.data.attributes.name }}
26 </p>
27 <p id="title" class="uk-text-large">
28 {{ article.attributes.title }}
29 </p>
30 </div>
31 </div>
32 </router-link>
33 </div>
34 <div>
35 <div class="uk-child-width-1-2@m uk-grid-match" uk-grid>
36 <router-link
37 v-for="article in rightArticles"
38 :to="{ path: '/article/' + article.attributes.slug }"
39 class="uk-link-reset"
40 :key="article.attributes.slug"
41 >
42 <div class="uk-card uk-card-muted">
43 <div class="uk-card-media-top">
44 <img
45 :src="api_url + article.attributes.image.data.attributes.url"
46 alt=""
47 height="100"
48 />
49 </div>
50 <div class="uk-card-body">
51 <p
52 id="category"
53 v-if="article.attributes.category"
54 class="uk-text-uppercase"
55 >
56 {{ article.attributes.category.data.attributes.name }}
57 </p>
58 <p id="title" class="uk-text-large">
59 {{ article.attributes.title }}
60 </p>
61 </div>
62 </div>
63 </router-link>
64 </div>
65 </div>
66 </div>
67 </div>
68</template>
69
70<script>
71export default {
72 data: function () {
73 return {
74 api_url: process.env.VUE_APP_STRAPI_API_URL,
75 };
76 },
77 props: {
78 articles: Object,
79 },
80 computed: {
81 leftArticlesCount() {
82 return Math.ceil(this.articles.data.length / 5);
83 },
84 leftArticles() {
85 return this.articles.data.slice(0, this.leftArticlesCount);
86 },
87 rightArticles() {
88 return this.articles.data.slice(
89 this.leftArticlesCount,
90 this.articles.data.length
91 );
92 },
93 },
94};
95</script>
Here we are simply displaying our articles by separating them on left and right side for design purpose:
1computed: {
2 leftArticlesCount() {
3 return Math.ceil(this.articles.data.length / 5);
4 },
5 leftArticles() {
6 return this.articles.data.slice(0, this.leftArticlesCount);
7 },
8 rightArticles() {
9 return this.articles.data.slice(this.leftArticlesCount, this.articles.data.length);
10 },
11 },
We are using the api_url: process.env.VUE_APP_STRAPI_API_URL
in order to display images from Strapi
Now it's time to display this page, remember the route you defined without a component? Let's tell your Vue app to use this containers/Articles
component when you are visiting /
main.js
file:1...
2import Articles from "./containers/Articles";
3
4...
5
6const routes = [{ path: "/", component: Articles }];
7...
One last thing, we need to tell Vue where to place this component.
router-view
component just under your Nav
component inside your App.vue
component:1<template>
2 <div id="app">
3 <Nav />
4 <router-view :key="$route.fullPath"></router-view>
5 </div>
6</template>
7...
First of all you'll need to install some dependencies:
moment
and vue-markdown-it
by running the following command:yarn add moment vue-markdown-it
You can see that if you click on the article, there is nothing. Let's create the article page together!
containers/Article.vue
file containing the following:1<template>
2 <div v-if="articles.data">
3 <div
4 v-if="articles.data[0].attributes.image"
5 id="banner"
6 class="uk-height-small uk-flex uk-flex-center uk-flex-middle uk-background-cover uk-light uk-padding"
7 :data-src="
8 api_url + articles.data[0].attributes.image.data.attributes.url
9 "
10 uk-img
11 >
12 <h1>{{ articles.data[0].attributes.title }}</h1>
13 </div>
14
15 <div class="uk-section">
16 <div class="uk-container uk-container-small">
17 <vue-markdown-it
18 v-if="articles.data[0].attributes.content"
19 :source="articles.data[0].attributes.content"
20 id="editor"
21 />
22 <p v-if="articles.data[0].attributes.publishedAt">
23 {{
24 moment(articles.data[0].attributes.publishedAt).format("MMM Do YY")
25 }}
26 </p>
27 </div>
28 </div>
29 </div>
30</template>
31
32<script>
33var moment = require("moment");
34import VueMarkdownIt from "vue-markdown-it";
35import gql from "graphql-tag";
36
37export default {
38 data() {
39 return {
40 articles: {},
41 moment: moment,
42 api_url: process.env.VUE_APP_STRAPI_API_URL,
43 routeParam: this.$route.params.slug,
44 };
45 },
46 components: {
47 VueMarkdownIt,
48 },
49 apollo: {
50 articles: {
51 query: gql`
52 query Article($slug: String!) {
53 articles(filters: { slug: { eq: $slug } }) {
54 data {
55 attributes {
56 slug
57 title
58 content
59 category {
60 data {
61 attributes {
62 slug
63 name
64 }
65 }
66 }
67 image {
68 data {
69 attributes {
70 url
71 }
72 }
73 }
74 }
75 }
76 }
77 }
78 `,
79 variables() {
80 return {
81 slug: this.routeParam,
82 };
83 },
84 },
85 },
86};
87</script>
Here we are fetching the url id with routeParam: this.$route.params.id
and setting it in our GraphQL variables:
1variables() {
2 return {
3 slug: this.routeParam
4 };
5}
Now we simply need to configure the router in our main.js
file
main.js
file1...
2import Articles from "./containers/Articles";
3import Article from "./containers/Article";
4
5...
6
7const routes = [
8 { path: "/", component: Articles },
9 {
10 path: "/article/:slug",
11 component: Article,
12 },
13];
14...
Click on any article!
Let's create a page for each category now!
containers/Category.vue
file containing the following code:1<template>
2 <div>
3 <div class="uk-section">
4 <div class="uk-container uk-container-large">
5 <h1>{{ category.data[0].attributes.name }}</h1>
6
7 <ArticlesList
8 :articles="category.data[0].attributes.articles.data || []"
9 ></ArticlesList>
10 </div>
11 </div>
12 </div>
13</template>
14
15<script>
16import ArticlesList from "../components/ArticlesList";
17import gql from "graphql-tag";
18
19export default {
20 data() {
21 return {
22 category: [],
23 routeParam: this.$route.params.slug,
24 };
25 },
26 components: {
27 ArticlesList,
28 },
29 apollo: {
30 category: {
31 query: gql`
32 query Category($slug: String!) {
33 categories(filters: { slug: { eq: $slug } }) {
34 data {
35 attributes {
36 slug
37 name
38 articles {
39 data {
40 attributes {
41 slug
42 title
43 content
44 category {
45 data {
46 attributes {
47 name
48 }
49 }
50 }
51 image {
52 data {
53 attributes {
54 url
55 }
56 }
57 }
58 }
59 }
60 }
61 }
62 }
63 }
64 }
65 `,
66 variables() {
67 return { slug: this.routeParam };
68 },
69 },
70 },
71};
72</script>
The code is pretty similar to the previous containers/Article.vue
file. We are fetching articles depending on the category we are in the url:
1apollo: {
2 category: {
3 query: gql`
4 query Category($slug: String!) {
5 categories(filters: { slug: { eq: $slug } }) {
6 data {
7 attributes {
8 slug
9 name
10 articles {
11 data {
12 attributes {
13 slug
14 title
15 content
16 category {
17 data {
18 attributes {
19 name
20 }
21 }
22 }
23 image {
24 data {
25 attributes {
26 url
27 }
28 }
29 }
30 }
31 }
32 }
33 }
34 }
35 }
36 }
37 `,
38 variables() {
39 return { slug: this.routeParam };
40 },
41 },
42 },
Again we simply need to configure the router in our main.js
file
main.js
file1import Article from "./containers/Article";
2import Articles from "./containers/Articles";
3import Category from "./containers/Category";
4
5...
6
7const routes = [
8 { path: "/", component: Articles },
9 {
10 path: "/article/:slug",
11 component: Article,
12 },
13 {
14 path: "/category/:slug",
15 component: Category,
16 },
17];
Click on any Category on your Nav!
Awesome! You can now navigate through categories :)
Huge congrats, you successfully completed this tutorial. I hope you enjoyed it!
Still hungry?
Feel free to add additional features, adapt this project to your needs, and give feedback in the comments section.
If you want to deploy your application, check our documentation
Contribute and collaborate on educational content for the Strapi Community https://strapi.io/write-for-the-community
I can't wait to see your contribution!
Please note: Since we initially published this blog, we released new versions of Strapi and tutorials may be outdated. Sorry for the inconvenience if it's the case, and please help us by reporting it here.
Get started with Strapi by creating a project using a starter or trying our live demo. Also, consult our forum if you have any questions. We will be there to help you.
See you soon!
Maxime started to code in 2015 and quickly joined the Growth team of Strapi. He particularly likes to create useful content for the awesome Strapi community. Send him a meme on Twitter to make his day: @MaxCastres