Comment on page
Vue Blog Example
In this guide we're going to demonstrate how to quickly build a basic CMS-Powered Blog with Vue.js and Comfortable.
The complete code for this tutorial is available on GitHub. You can also explore and play around with it on CodeSandbox.
To get started, we'll need a repository for our project and some models for the content. If you don't have a Comfortable account yet, you can sign up here. If you already have an account, log in. 🙂
Create a new repository for your blog. When you're done, head over to the
Content Types
page to create some models.
First things first, for any blogpost there is someone who wrote it. Therefore, we're going to create a Content Type
Author
.On the Content Types page, click the green
+ Add Content Type
button on the top to create a new type Author
. Make sure you have checked Create a Collection
field.
Next, add some fields for this content type. We'll need:
Name
– Field Type:Text
(Single line) Hint: You can simply rename theTitle
field, which is created automatically for new Content Types.Avatar
– Field Type:Asset
Switch back to the Content Types page and add another type
Blogpost
. Again, make sure to have the field Create a Collection
enabled. This time, add the following fields:Title
– Field Type:Text
(Single line) This will be the title for any blogpostSlug
– Field Type:Text
(Single line) We're going to create a URL for each Blogpost from this field.Image
– Field Type:Asset
An image to show with each post.Content
– Field Type:Richtext
This field will contain a Blogposts main content.Author
– Field Type:Relation
As each Blogpost has an Author, we'll connect both with a relation. In the field configuration, we'll set a one-to-one relation for the field and select the Content TypeAuthor
, that we have just created.

The last Content Type we are going to create is really simple. We call it About page in this example, but keep it general so you could create standardised pages from it. Create a new type
Page
and skip the Checkbox Create a Collection
this time. We need the following fields:Title
– Field Type:Text
(Single line) This will be the page titleContent
– Field Type:Richtext
This field contains the main content for the page
We need to create some content to display in the example. Let's start with some blog posts. Click the
+
Button on the page header and create a new document of type Blogpost
.For the first post, you'll also have to create a new
Author
. You can do while creating a blog post, by simply clicking the button Create new Entry
on the Author field. For the next posts use Select Relations
to pick the Author from a list.For the demo, we'd recommend to have at least 3-5 pieces of content in your blog.
Also, don't forget to add the About page.
As already mentioned, there is a checkbox
Create a Collection
when creating a new Content Type. This option will add a Collection to the Content Tree for this particular type.Each collection comes with an individual endpoint, which makes it very easy to retrieve content or change the content output.
You can also link individual pages to the Content Tree and create an endpoint for a single page. We'll do that for the About page. Create your page the same way you created blog posts and when you're done, click the
Add Link
Button above the Content Tree:

Any collection or document node on the Content Tree has an individual endpoint to fetch content. If you're using the SDK, like in this example, all we need is the
API ID
. We'll come back to this later.By the end of this section we've learned:
- How to create Content Types (Models)
- How to create Documents (Content)
- How to create Collections and single linked Documents in the Content Tree
We'll use the Vue CLI for this example. Run the following command in your terminal, if you haven't already installed Vue CLI.
npm install -g @vue/cli
Next, create the Vue app
vue create --default comfortable-vue-blog
Switch to the directory that was created by the CLI
cd comfortable-vue-blog
We'll use the Vue router for this project
vue add router
The CLI will probably ask you if you'd like use the HTML5 history mode. Usually you'd want to choose 'Yes', and redirect any request to
index.html
. You can read more about the HTML5 History Mode in the documentation.npm install comfortable-javascript --save
We'll also use lodash
npm install lodash --save
Enough preparations, start your favourite IDE let's finally start hacking. 😃
Tip: There are some great IDE extensions for Vue to provide syntax highlighting, autocompletion, etc. We'd highly recommend to check them out.
router.js
Let's prepare the routing with Vue Router first. We'll use the existing base route
/
to display a paginated list of all blogposts and create a new route /blog/:slug
to display individual posts. If you want to learn more about the Vue Router, you can find their documentation here: https://router.vuejs.org/import Vue from 'vue'
import Router from 'vue-router'
import Home from './views/Home.vue'
import BlogPost from './views/BlogPost.vue'
import Page from './views/Page.vue'
Vue.use(Router)
export default new Router({
mode: 'history',
routes: [
{
path: '/',
name: 'home',
component: Home
},
{
path: '/blog/:slug',
name: 'blog-post',
component: BlogPost
},
{
path: '/:slug',
name: 'page',
component: Page
}
]
})
comfortable.js
Now we're going to set up the Comforable SDK for the app. Create a new file comfortable.js in
src
:import Comfortable from 'comfortable-javascript';
export const comfortable = Comfortable.api('<Your API ID>', '<Your API KEY>');
Import this file into any component you want to use Comfortable.
To find your repositories API ID go to the
Settings
page. You'll also find your API Keys in Settings>API Keys.App.vue
The
App
components purpose is to display a header on top of each page and to provide the <router-view />
component to display components we've defined in router.js
.<template>
<div id="app">
<div id="nav">
<router-link to="/">Home</router-link> |
<router-link to="/about">About</router-link>
</div>
<main>
<router-view/>
</main>
</div>
</template>
<style>
@import url('<https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.0/normalize.min.css>');
#app {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
}
#nav {
background-color: #F5F5F5;
color: #555555;
}
#nav a {
color: #555555;
text-decoration: none;
display: inline-block;
padding: .5rem;
}
#nav a:hover {
text-decoration: underline;
}
.content-wrapper {
max-width: 840px;
margin: 42px auto;
padding: 21px;
}
a {
text-decoration: none;
}
</style>
Home.vue
On our blog page we want to display all posts and a
Load more
button to paginate the list. Remember we told Vue to load the Home
Component for the /
in router.js
? Now let's have a look at the Home
Component:<template>
<div class="home content-wrapper">
<div v-for="post in posts" :key="post.meta.id">
<router-link :to="`/blog/${post.fields.slug}`">
<article>
<div class="image">
<img :alt="post.fields.title" :src="`${post.fields.image[0].fields.file.url}?w=840&h=400&fit=crop`">
</div>
<h2>{{ post.fields.title }}</h2>
</article>
</router-link>
</div>
<button v-if="totalPosts > posts.length" @click="getPosts">
{{loading ? 'Loading...' : 'Load more posts'}}
</button>
</div>
</template>
<script>
import { comfortable } from '@/comfortable.js'
export default {
name: 'home',
data() {
return {
posts: [],
totalPosts: 0,
loading: false
}
},
methods: {
getPosts() {
this.loading = true;
const options = {
embedAssets: true,
offset: this.posts.length
};
comfortable.getCollection('blogpost', options)
.then(result => {
this.posts.push(...result.data);
this.totalPosts = result.meta.total;
this.loading = false;
})
.catch(err => {
this.loading = false;
throw err;
})
}
},
created() {
this.getPosts();
}
}
</script>
<style>
article {
margin-bottom: 42px;
text-align: left;
}
article .image {
width: 100%;
}
article .image img {
max-width: 100%;
height: auto;
}
article h2 {
color: #2d2d33;
}
.home article {
border: 1px solid #ccc;
}
.home article h2 {
margin-left: 21px;
}
</style>
What's happening here?
In short: In the script part, we fetch a list of post items from the API. For each post, Vue creates an article and displays its content, wrapped into a link to the single view.
By keeping track of the total number of posts stored in Comfortable, we are able to determine wether to display a
Load more
button. When the button gets clicked, the next set of post items gets fetched from the API.Now that we have a list of posts, we want to be able to load single blogposts on a separate page. We've already used the
Slug
field from the Blogpost
model to build links on the posts list page. As you remember in router.js
, we have defined the second part of a single view URL to be a route parameter :slug
. So the piece of information we need to retrieve a single post is present as this.$route.params.slug
. All we have to do now, is use a filter query.BlogPost.vue
<template>
<div class="post" v-if="post && author">
<article>
<div class="image">
<img :alt="post.fields.title" :src="`${post.fields.image[0].fields.file.url}?w=1680&h=750&fit=crop`">
</div>
<div class="content-wrapper">
<h2>{{ post.fields.title }}</h2>
<div class="content" v-html="post.fields.content.html"></div>
<div class="author">
<img :src="`${author.fields.avatar[0].fields.file.url}?w=30&h=30&fit=crop`" alt="author.fields.name"> Written by {{ author.fields.name }}
</div>
</div>
</article>
</div>
</template>
<script>
import Comfortable from 'comfortable-javascript';
import { comfortable } from '@/comfortable.js'
import _ from 'lodash';
export default {
name: 'blogPost',
data() {
return {
post: null,
author: null,
}
},
methods: {
getPost() {
const options = {
embedAssets: true,
includes: 1,
filters: new Comfortable.Filter()
.addAnd('slug', 'equal', this.$route.params.slug)
};
comfortable.getDocuments(options)
.then(result => {
this.post = result.data[0];
this.author = _.find(result.includes.author, { meta: { id: this.post.fields.author[0].meta.id } });
})
.catch(err => {
throw err;
})
}
},
created() {
this.getPost();
}
}
</script>
<style>
.post .image{
width: 100%;
}
.post .image img{
width: 100%;
}
.post .author img {
margin-top: 10px;
}
.post .author img {
display: inline-block;
margin-bottom: -8px;
margin-right: 10px;
border-radius: 50%;
}
</style>
At last we add the About page. We're going to create this page from the basic Content Type
Page
. This way you are able create several pages from the same Component and Content Type.The important part here is, that we're fetching this page by a route parameter again, and this time we'll use the document alias that we've created in the Content Tree, instead of a filter.
Page.vue
<template>
<div class="page" v-if="page">
<article>
<div class="content-wrapper">
<h2>{{ page.fields.title }}</h2>
<div class="content" v-html="page.fields.content.html"></div>
</div>
</article>
</div>
</template>
<script>
import { comfortable } from '@/comfortable.js'
export default {
name: 'page',
data() {
return {
page: null
}
},
methods: {
getPage() {
comfortable.getAlias(this.$route.params.slug)
.then(result => {
this.page = result;
})
.catch(err => {
throw err;
})
}
},
created() {
this.getPage();
}
}
</script>
That's it 🙂 Run
npm run serve
to view the app in your browser, or npm run build
to get a deployable blog from your codebase. Don't forget to handle the HTML5 History Mode on your server.Happy coding!
Deploying your Blog with Netlify is perfect if you don't want to handle a server or webspace yourself. You'll be provided with a really fast and reliable deployment and hosting for any static website.
Using Netlify for this example is as simple as clicking this link. You'll be asked to connect your GitHub or GitLab account to let Netlify create a Repository on your behalf. The Repository will include the code from this example and you can change it and play around as you like.
That's all. Your Blog will be available within seconds. 🚀 😊
Last modified 5yr ago