Why and How You Should Build a Serverless CMS-powered Vue.js App

Jake Lumetta
👁️ 3,567 views
💬 comments

Why Vue.js?

This tutorial isn't intended to convince you that you must use Vue.js. It is a progressive framework for building user interfaces that's gaining in popularity. It improves upon some aspects of React and Angular. It's reactive and component-based, allowing you to create pluggable components you can add to any project.

For purposes of this tutorial, though, we focus on Vue.js because it works very well with serverless architecture. Serverless is becoming the preferred architecture for many developers. It allows you to create and fine-tune products more quickly without having to suffer the headaches like server maintenance, outages, and scaling bottlenecks of server-based architecture.

This tutorial will show you how to add performant content APIs to your Vue.js application. These APIs are easy to navigate for your entire team, enabling you to enjoy simple content management without having to spin up and maintain your own CMS infrastructure.

Table of Contents

    Specifically, we'll cover three use cases with code samples:

    • marketing pages
    • blogging
    • knowledge bases

    ButterCMS

    ButterCMS

    This tutorial uses ButterCMS to build a serverless Vue.js application. ButterCMS is a headless CMS and blogging platform that lets you build CMS-powered apps using any programming language.

    At it's core is a JSON API is easy to integrate with apps built in any language/framework.

    You can use it for marketing websites, blogs, and custom content modeling.

    Create an account for free to follow along with this tutorial. You can find your own API Key here.

    Getting started

    First, install the ButterCMS JS SDK. We'll be using this to query the content API.

    npm install buttercms --save

    Once installed you can follow the tutorials below for creating three types of content: marketing pages, blogging, and knowledge bases (e.g., an FAQ).

    Add Marketing Pages

    Using Vue and ButterCMS, you can create an environment where a non-technical person on your team to add customer case study pages to your marketing site. To do this, we'll create a case study page as an example. Using the dashboard on ButterCMS, you can create a “page type” entitled “Customer Case Study” and define the fields.

    Once you've done this, you can create your first page. Specify the name and URL of the page using Butter's dashboard and populate the content of the page. Once this is all done, ButterCMS's API will return your defined page in JSON format. It should look something like this:

    {
        "data": {
            "slug": "acme-co",
            "fields": {
                "facebook_open_graph_title": "Acme Co loves ButterCMS",
                "seo_title": "Acme Co Customer Case Study",
                "headline": "Acme Co saved 200% on Anvil costs with ButterCMS",
                "testimonial": "<p>We've been able to make anvils faster than ever before! - <em>Chief Anvil Maker</em></p>\r\n<p><img src=\"https://cdn.buttercms.com/NiA3IIP3Ssurz5eNJ15a\" alt=\"\" caption=\"false\" width=\"249\" height=\"249\" /></p>",
                "customer_logo": "https://cdn.buttercms.com/c8oSTGcwQDC5I58km5WV",
            }
        }
    }

    Next, open your code editor and create a file called buttercms.js in your src directory.

    If you don't have an existing project, create one by entering the following:

    vue init webpack buttercms-project
    cd buttercms-project
    npm i
    npm i -S buttercms
    npm run dev

    Now, update the routes in your app. Access router/index.js and

    import Vue from 'vue'
    import Router from 'vue-router'
    import CustomersHome from '@/components/CustomersHome'
    import CustomerPage from '@/components/CustomerPage'
    
    Vue.use(Router)
    
    export default new Router({
      mode: 'history',
      routes: [
        {
          path: '/customers/',
          name: 'customers-home',
          component: CustomersHome
        },
        {
          path: '/customers/:slug',
          name: 'customer-page',
          component: CustomerPage
        }
      ]
    })

    Now, to set up a customers page to list all your customers, we're going to define a getpages() method to get all customer pages. In components/CustomersHome.vue we do:

    <script>
      // import ButterCMS from 
      import { butter } from '@/buttercms'
      export default {
        name: 'customers-home',
        data() {
          return {
            page_title: 'Customers',
            // Create array to hold the pages from ButterCMS API
            pages: []
          }
        },
        methods: {
          // Get List of Customer Pages
          getPages() {
            butter.page.list('customer_case_study')
              .then((res) => {
                // console.log(res.data.data) // Check the results in the console
                this.pages = res.data.data
              })
          }
        },
        created() {
          // Fire on page creation
          this.getPages()
        }
      }
    </script>

    Display the results:

    <template>
      <div id="customers-home">
        <h1>{{ page_title }}</h1>
        <div v-for="(page,index) in pages" :key="page.slug + '_' + index">
          <router-link :to="'/customers/' + page.slug">
            <div>
              <img :src="page.fields.customer_logo" alt="">
              <h2>{{ page.fields.headline }}</h2>
            </div>
          </router-link>
        </div>
      </div>
    </template>

    Now, we're going to set up the customer page to view a single customer. To do so, in components/CustomerPage.vue we define a getPage() method to get a particular customer page based on its slug:

    <script>
      import { butter } from '@/buttercms'
      export default {
        name: 'customer-page',
        data() {
          return {
            slug: this.$route.params.slug,
            page: {
              slug: '',
              fields: {}
            }
          }
        },
        methods: {
          getPage() {
            butter.page.retrieve('customer_case_study', this.slug)
              .then((res) => {
                console.log(res.data.data)
                this.page = res.data.data
              }).catch((res) => {
                console.log(res)
              })
          }
        },
        created() {
          this.getPage()
        }
      }
    </script>

    When you display the result, you should get:

    <template>
      <div id="customer-page">
        <figure>
          <img :src="page.fields.customer_logo">
        </figure>
        <h1>{{ page.fields.headline }}</h1>
        <h3>Testimonials</h3>
        <div v-html="page.fields.testimonial"></div>
        <div v-html="page.fields.body"></div>
      </div>
    </template>

    Success! Now you can navigate to the Customer Page you created in your Butter dashboard via the list of all Customer Pages or directly via url.

    Add a Knowledge Base

    The tutorial below will take you through the creation of a knowledge base for your Vue.js app. We'll be using ButterCMS “Content Fields” for this. Content fields are simply global pieces of content that can be managed by your team. This content can span multiple pages and each content field has a unique ID for querying, as you'll see below.

    Setup content fields

    First, you'll need to set up some custom content fields. Using the dashboard, you can set up a workspace to organize content fields. Workspaces will allow content editors to curate content without affecting development or API.

    Once you're in a workspace click the button to create a new content field. Choose the "Object" type and name the field "FAQ Headline". It will have an API slug of “faq_headline”.

    After saving, add another field but this time choose the "Collection" type and name the field FAQ Items. It will have an API slug of “faq_items”. On the next screen setup two properties for items in the collection. Now go back to your workspace and update your heading and add some FAQ items.

    Integrating your app

    Now that you've created dynamic content, it's time to display the dynamic content in your app. To do this, you'll fetch the fields with an API call and reference them in your view. First, set up a route to your FAQ page:

    In your code editor:

    router/index.js
    
    import Vue from 'vue'
    import Router from 'vue-router'
    
    import FAQ from '@/components/FAQ'
    
    Vue.use(Router)
    
    export default new Router({
      mode: 'history',
      routes: [
        {
          path: '/faq',
          name: 'faq',
          component: FAQ
        }
      ]
    })

    Then create components/FAQ.vue with a call to get the FAQ's from the API:

    <script>
      import { butter } from '@/buttercms'
      export default {
        name: 'faq',
        data() {
          return {
            page_title: 'FAQ',
            faq_items: []
          }
        },
        methods: {
          getFaqs() {
            butter.content.retrieve(['faq_headline', 'faq_items'])
              .then((res) => {
                console.log(res.data.data)
                this.page_title = res.data.data.faq_headline
                this.faq_items = res.data.data.faq_items
              })
          }
        },
        created() {
          this.getFaqs()
        }
      }
    </script>

    Notice that we predefined page_title as ‘FAQ' and then updated it with the API call to 'Frequently Asked Questions'.

    Your displayed result should look like this:

    <template>
      <div id="faq">
        <h1>{{ page_title }}</h1>
        <div v-for="(faq, index) in faq_items" :key="index">
          <p>{{ faq.question }}</p>
          <p>{{ faq.answer }}</p>
        </div>
      </div>
    </template>

    Now your team can update the values from the Butter dashboard and the corresponding content in your app will automatically update.

    Blog Engine

    Last, we'll tackle adding a blog engine. You can quickly create a CMS-powered blog using Vue.js.

    Displaying Posts

    We'll start out by creating a blog route using vue-router. To display posts we create a simple /blog route in our app and fetch blog posts, as well as a /blog/:slug route to handle individual posts.

    In your code editor:

    router/index.js:
    
    import Vue from 'vue'
    import Router from 'vue-router'
    import BlogHome from '@/components/BlogHome'
    import BlogPost from '@/components/BlogPost'
    
    Vue.use(Router)
    
    export default new Router({
      mode: 'history',
      routes: [
        {
          path: '/blog/',
          name: 'blog-home',
          component: BlogHome
        },
        {
          path: '/blog/:slug',
          name: 'blog-post',
          component: BlogPost
        }
      ]
    })

    To create your blog homepage that will display your most recent posts, you'll create components/BlogHome.vue:

    <script>
      import { butter } from '@/buttercms'
      export default {
        name: 'blog-home',
        data() {
          return {
            page_title: 'Blog',
            posts: []
          }
        },
        methods: {
          getPosts() {
            butter.post.list({
              page: 1,
              page_size: 10
            }).then((res) => {
              // console.log(res.data)
              this.posts = res.data.data
            })
          }
        },
        created() {
          this.getPosts()
        }
      }
    </script>

    Your result should look like this:

    <template>
      <div id="blog-home">
          <h1>{{ page_title }}</h1>
          <!-- Create v-for and apply a key for Vue. Example is using a combination of the slug and index -->
          <div v-for="(post,index) in posts" :key="post.slug + '_' + index">
            <router-link :to="'/blog/' + post.slug">
              <article class="media">
                <figure>
                  <!-- Bind results using a ':' -->
                  <!-- Use a v-if/else if their is a featured_image -->
                  <img v-if="post.featured_image" :src="post.featured_image" alt="">
                  <img v-else src="http://via.placeholder.com/250x250" alt="">
                </figure>
                <h2>{{ post.title }}</h2>
                <p>{{ post.summary }}</p>
              </article>
            </router-link>
          </div>
      </div>
    </template>

    Next, create components/BlogPost.vue which will be your Blog Post page to list a single post.

    <script>
      import { butter } from '@/buttercms'
      export default {
        name: 'blog-post',
        data() {
          return {
            post: {}
          }
        },
        methods: {
          getPost() {
            butter.post.retrieve(this.$route.params.slug)
              .then((res) => {
                // console.log(res.data)
                this.post = res.data
              }).catch((res) => {
                console.log(res)
              })
          }
        },
        created() {
          this.getPost()
        }
      }
    </script>

    You should get this result:

    <template>
      <div id="blog-post">
        <h1>{{ post.data.title }}</h1>
        <h4>{{ post.data.author.first_name }} {{ post.data.author.last_name }}</h4>
        <div v-html="post.data.body"></div>
    
        <router-link v-if="post.meta.previous_post" :to="/blog/ + post.meta.previous_post.slug" class="button">
          {{ post.meta.previous_post.title }}
        </router-link>
        <router-link v-if="post.meta.next_post" :to="/blog/ + post.meta.next_post.slug" class="button">
          {{ post.meta.next_post.title }}
        </router-link>
      </div>
    </template>

    At this point, your app is pulling all blog posts, allowing you to navigate to individual posts. But you will notice the next/previous buttons aren't working. Why? When using routes with params, when the user navigates from /blog/foo to /blog/bar, the same component instance will be reused.

    Since both routes render the same component, this is more efficient than destroying the old instance and creating a new one. But this also means that the lifecycle hooks of the component will not be called.

    There is a fix for this. We need to watch the $route object and call getPost() when the route changes. To do this, you need to update the script section in components/BlogPost.vue:

    <script>
      import { butter } from '@/buttercms'
      export default {
        name: 'blog-post',
        data() {
          return {
            post: {}
          }
        },
        methods: {
          getPost() {
            butter.post.retrieve(this.$route.params.slug)
              .then((res) => {
                // console.log(res.data)
                this.post = res.data
              }).catch((res) => {
                console.log(res)
              })
          }
        },
        watch: {
          $route(to, from) {
            this.getPost()
          }
        },
        created() {
          this.getPost()
        }
      }
    </script>

    At this point, your app has a working blog that can be updated easily from the CMS dashboard.

    You can also use APIs to filter and feature content on your blog with categories, tags, and authors. In fact, there's a lot you can do with an API in terms of managing different aspects of your blog, including RSS, Atom, and Sitemap markup, and content styling via CSS.

    Takeaway

    This tutorial shows how easy it is to build a serverless CMS-powered Vue.js app. Vue's progressive framework for building user interfaces can be quickly implemented, saving you and your team valuable time.

    Building a serverless CMS-powered Vue.js app marries the agility of Vue with the freedom and opportunity of a serverless architecture, further minimizing headaches and allowing you to try things without worrying about compromising your existing codebase.

    Give ButterCMS a look if you want to focus on the frontend and don't want to be bogged down by backend operations like hosting, scaling, security, and more.

    Jake Lumetta

    3 posts

    Founder and CEO of ButterCMS, an API-first CMS. For more content like this: