|
| 1 | +--- |
| 2 | +title: Create a CMS-Powered Blog |
| 3 | +type: cookbook |
| 4 | +order: 5 |
| 5 | +--- |
| 6 | + |
| 7 | +# Create a CMS-Powered Blog Using Vue.js |
| 8 | + |
| 9 | +So you've just launched your Vue.js website, congrats! Now you want to add a blog that quickly plugs into your website and you don't want to have to spin up a whole server just to host a Wordpress instance (or any DB-powered CMS for that matter). You want to just be able to add a few Vue.js blog components and some routes and have it all just work, right? What you're looking for a blog that's powered entirely by API's you can consume directly from your Vue.js application. This tutorial will teach you how to do just that, let's dive in! |
| 10 | + |
| 11 | +We're going to quickly build a CMS-powered blog with Vue.js. It uses [ButterCMS](https://buttercms.com/), an API-first CMS that lets you manage content using the ButterCMS dashboard and integrate our content API into your Vue.js app. You can use ButterCMS for new or existing Vue.js projects. |
| 12 | + |
| 13 | + |
| 14 | + |
| 15 | +## Install |
| 16 | + |
| 17 | +Run this in your commandline: |
| 18 | + |
| 19 | +`npm install buttercms --save` |
| 20 | + |
| 21 | +Butter can also be loaded using a CDN: |
| 22 | + |
| 23 | +`<script src="https://cdnjs.buttercms.com/buttercms-1.1.0.min.js"></script>` |
| 24 | + |
| 25 | +## Quickstart |
| 26 | + |
| 27 | +Set your API token: |
| 28 | + |
| 29 | +`var butter = require('buttercms')('your_api_token');` |
| 30 | + |
| 31 | +Using ES6: |
| 32 | + |
| 33 | +```javascript |
| 34 | +import Butter from 'buttercms'; |
| 35 | +const butter = Butter('your_api_token'); |
| 36 | +``` |
| 37 | + |
| 38 | +Using CDN: |
| 39 | + |
| 40 | +```javascript |
| 41 | +<script src="https://cdnjs.buttercms.com/buttercms-1.1.0.min.js"></script> |
| 42 | +<script> |
| 43 | + var butter = Butter('your_api_token'); |
| 44 | +</script> |
| 45 | +``` |
| 46 | + |
| 47 | + Import this file into any component you want to use ButterCMS. Then from the console run: |
| 48 | + |
| 49 | +```javascript |
| 50 | +butter.post.list({page: 1, page_size: 10}).then(function(response) { |
| 51 | + console.log(response) |
| 52 | +}) |
| 53 | +``` |
| 54 | + |
| 55 | +This API request fetches your blog posts. Your account comes with one example post which you'll see in the response. |
| 56 | + |
| 57 | +## Display posts |
| 58 | +To display posts we create a `/blog` route (using vue-router) in our app and fetch blog posts from the Butter API, as well as a `/blog/:slug` route to handle individual posts. |
| 59 | + |
| 60 | +See the ButterCMS [API reference](https://buttercms.com/docs/api/?javascript#blog-posts) for additional options such as filtering by category or author. The response also includes some metadata we'll use for pagination. |
| 61 | + |
| 62 | +`router/index.js:` |
| 63 | + |
| 64 | +```javascript |
| 65 | +import Vue from 'vue' |
| 66 | +import Router from 'vue-router' |
| 67 | +import BlogHome from '@/components/BlogHome' |
| 68 | +import BlogPost from '@/components/BlogPost' |
| 69 | + |
| 70 | +Vue.use(Router) |
| 71 | + |
| 72 | +export default new Router({ |
| 73 | + mode: 'history', |
| 74 | + routes: [ |
| 75 | + { |
| 76 | + path: '/blog/', |
| 77 | + name: 'blog-home', |
| 78 | + component: BlogHome |
| 79 | + }, |
| 80 | + { |
| 81 | + path: '/blog/:slug', |
| 82 | + name: 'blog-post', |
| 83 | + component: BlogPost |
| 84 | + } |
| 85 | + ] |
| 86 | +}) |
| 87 | +``` |
| 88 | + |
| 89 | +Then create `components/BlogHome.vue` which will be your blog homepage that lists your most recent posts. |
| 90 | + |
| 91 | +```javascript |
| 92 | +<script> |
| 93 | + import { butter } from '@/buttercms' |
| 94 | + export default { |
| 95 | + name: 'blog-home', |
| 96 | + data() { |
| 97 | + return { |
| 98 | + page_title: 'Blog', |
| 99 | + posts: [] |
| 100 | + } |
| 101 | + }, |
| 102 | + methods: { |
| 103 | + getPosts() { |
| 104 | + butter.post.list({ |
| 105 | + page: 1, |
| 106 | + page_size: 10 |
| 107 | + }).then((res) => { |
| 108 | + // console.log(res.data) |
| 109 | + this.posts = res.data.data |
| 110 | + }) |
| 111 | + } |
| 112 | + }, |
| 113 | + created() { |
| 114 | + this.getPosts() |
| 115 | + } |
| 116 | + } |
| 117 | +</script> |
| 118 | +Display the result |
| 119 | + |
| 120 | +<template> |
| 121 | + <div id="blog-home"> |
| 122 | + <h1>{{ page_title }}</h1> |
| 123 | + <!-- Create v-for and apply a key for Vue. Example is using a combination of the slug and index --> |
| 124 | + <div v-for="(post,index) in posts" :key="post.slug + '_' + index"> |
| 125 | + <router-link :to="'/blog/' + post.slug"> |
| 126 | + <article class="media"> |
| 127 | + <figure> |
| 128 | + <!-- Bind results using a ':' --> |
| 129 | + <!-- Use a v-if/else if their is a featured_image --> |
| 130 | + <img v-if="post.featured_image" :src="post.featured_image" alt=""> |
| 131 | + <img v-else src="http://via.placeholder.com/250x250" alt=""> |
| 132 | + </figure> |
| 133 | + <h2>{{ post.title }}</h2> |
| 134 | + <p>{{ post.summary }}</p> |
| 135 | + </article> |
| 136 | + </router-link> |
| 137 | + </div> |
| 138 | + </div> |
| 139 | +</template> |
| 140 | +``` |
| 141 | + |
| 142 | +Here's what it looks like (note we added CSS from https://bulma.io/ for quick styling): |
| 143 | + |
| 144 | + |
| 145 | + |
| 146 | + |
| 147 | +Now create `components/BlogPost.vue` which will be your Blog Post page to list a single post. |
| 148 | + |
| 149 | +```javascript |
| 150 | +<script> |
| 151 | + import { butter } from '@/buttercms' |
| 152 | + export default { |
| 153 | + name: 'blog-post', |
| 154 | + data() { |
| 155 | + return { |
| 156 | + post: {} |
| 157 | + } |
| 158 | + }, |
| 159 | + methods: { |
| 160 | + getPost() { |
| 161 | + butter.post.retrieve(this.$route.params.slug) |
| 162 | + .then((res) => { |
| 163 | + // console.log(res.data) |
| 164 | + this.post = res.data |
| 165 | + }).catch((res) => { |
| 166 | + console.log(res) |
| 167 | + }) |
| 168 | + } |
| 169 | + }, |
| 170 | + created() { |
| 171 | + this.getPost() |
| 172 | + } |
| 173 | + } |
| 174 | +</script> |
| 175 | +Display the results |
| 176 | +<template> |
| 177 | + <div id="blog-post"> |
| 178 | + <h1>{{ post.data.title }}</h1> |
| 179 | + <h4>{{ post.data.author.first_name }} {{ post.data.author.last_name }}</h4> |
| 180 | + <div v-html="post.data.body"></div> |
| 181 | + |
| 182 | + <router-link v-if="post.meta.previous_post" :to="/blog/ + post.meta.previous_post.slug" class="button"> |
| 183 | + {{ post.meta.previous_post.title }} |
| 184 | + </router-link> |
| 185 | + <router-link v-if="post.meta.next_post" :to="/blog/ + post.meta.next_post.slug" class="button"> |
| 186 | + {{ post.meta.next_post.title }} |
| 187 | + </router-link> |
| 188 | + </div> |
| 189 | +</template> |
| 190 | +``` |
| 191 | + |
| 192 | +Here's a preview: |
| 193 | + |
| 194 | + |
| 195 | + |
| 196 | + |
| 197 | +Now our app is pulling all blog posts and we can navigate to individual posts. However, our next/previous post buttons are not working. |
| 198 | + |
| 199 | +One thing to note when using routes with params is that 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 then creating a new one. |
| 200 | + |
| 201 | +<p class="tip">Be aware, that using the component this way will mean that the lifecycle hooks of the component will not be called. Visit the Vue.js docs to learn more about [Dynamic Route Matching](https://router.vuejs.org/en/essentials/dynamic-matching.html)</p> |
| 202 | + |
| 203 | +To fix this we need to watch the `$route` object and call `getPost()` when the route changes. |
| 204 | + |
| 205 | +Updated `script` section in `components/BlogPost.vue`: |
| 206 | + |
| 207 | +```javascript |
| 208 | +<script> |
| 209 | + import { butter } from '@/buttercms' |
| 210 | + export default { |
| 211 | + name: 'blog-post', |
| 212 | + data() { |
| 213 | + return { |
| 214 | + post: {} |
| 215 | + } |
| 216 | + }, |
| 217 | + methods: { |
| 218 | + getPost() { |
| 219 | + butter.post.retrieve(this.$route.params.slug) |
| 220 | + .then((res) => { |
| 221 | + // console.log(res.data) |
| 222 | + this.post = res.data |
| 223 | + }).catch((res) => { |
| 224 | + console.log(res) |
| 225 | + }) |
| 226 | + } |
| 227 | + }, |
| 228 | + watch: { |
| 229 | + $route(to, from) { |
| 230 | + this.getPost() |
| 231 | + } |
| 232 | + }, |
| 233 | + created() { |
| 234 | + this.getPost() |
| 235 | + } |
| 236 | + } |
| 237 | +</script> |
| 238 | +``` |
| 239 | + |
| 240 | +Now your app has a working blog that can be updated easily in the ButterCMS dashboard. |
| 241 | + |
| 242 | +## Categories, Tags, and Authors |
| 243 | + |
| 244 | +Use Butter's APIs for categories, tags, and authors to feature and filter content on your blog. |
| 245 | + |
| 246 | +See the ButterCMS API reference for more information about these objects: |
| 247 | + |
| 248 | +* [Categories](https://buttercms.com/docs/api/?ruby#categories) |
| 249 | +* [Tags](https://buttercms.com/docs/api/?ruby#tags) |
| 250 | +* [Authors](https://buttercms.com/docs/api/?ruby#authors) |
| 251 | + |
| 252 | +Here's an example of listing all categories and getting posts by category. Call these methods on the `created()` lifecycle hook: |
| 253 | + |
| 254 | +``` |
| 255 | +methods: { |
| 256 | + ... |
| 257 | + getCategories() { |
| 258 | + butter.category.list() |
| 259 | + .then((res) => { |
| 260 | + console.log('List of Categories:') |
| 261 | + console.log(res.data.data) |
| 262 | + }) |
| 263 | + }, |
| 264 | + getPostsByCategory() { |
| 265 | + butter.category.retrieve('example-category', { |
| 266 | + include: 'recent_posts' |
| 267 | + }) |
| 268 | + .then((res) => { |
| 269 | + console.log('Posts with specific category:') |
| 270 | + console.log(res) |
| 271 | + }) |
| 272 | + } |
| 273 | +}, |
| 274 | +created() { |
| 275 | + ... |
| 276 | + this.getCategories() |
| 277 | + this.getPostsByCategory() |
| 278 | +} |
| 279 | +``` |
| 280 | + |
| 281 | +## Alternative Patterns |
| 282 | + |
| 283 | +An alternative pattern to consider, especially if you prefer writing only in Markdown, is using something like [Nuxtent](https://nuxtent.now.sh/guide/writing#async-components). Nuxtent allows you to use `Vue Component` inside of Markdown files. This approach would be akin to a static site approach (i.e. Jekyll) where you compose your blog posts in Markdown files. Nuxtent adds a nice integration between Vue.js and Markdown allowing you to live in a 100% Vue.js world. |
| 284 | + |
| 285 | + |
| 286 | +## Wrap up |
| 287 | + |
| 288 | +That's it! You now have a fully functional CMS-powered blog running in your app. We hope this tutorial was helpful and made your development experience with Vue.js even more enjoyable :) |
| 289 | + |
0 commit comments