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.

If you're new to Vue.js, we'd recommend checking out this great Vue.js introduction first.

The complete code for this tutorial is available on GitHub. You can also explore and play around with it on CodeSandbox.

Preparing the CMS – Creating content models

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.

The Author Model

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 the Title field, which is created automatically for new Content Types.

  • Avatar – Field Type: Asset

The Blogpost Model

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 blogpost

  • Slug – 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 Type Author, that we have just created.

About Page

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 title

  • Content – Field Type: Richtext This field contains the main content for the page

Create some content

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.

Collections & Pages: The Content Tree

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.

Summary

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

Installing Vue and Dependencies

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.

Let's install the Comfortable JavaScript SDK for a convenient way to interact with the API.

npm install comfortable-javascript --save

We'll also use lodash

npm install lodash --save

Coding the app

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.

Getting started

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>

Display a list of posts

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.

Display a single post

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>

The About page

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>

Running and building the app

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!

Join our Slack team to get in touch with the community and ask questions!

Bonus: Serverless deployment with Netlify

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 updated