Power your Vue.js apps with a CMS
Tags: vue, sanity, javascript
Published: 2021-04-01 03:09:05


In this article, we explore how to build a CMS-powered blog with Vue.js. Our content will be stored in Sanity's Content Lake and will be editable in the [Sanity Studio](https://www.sanity.io/studio).  We'll start by installing Vue.js CLI and setting up a Vue.js project using the CLI. We will then integrate Sanity, our content management system, with the Vue.js app. Then we will write the query to fetch remote data in Vue.js and setup dynamic routes with Vue Router.

## What is Vue.js ?

Vue.js is an open-source model–view–view model frontend JavaScript framework for building user interfaces and single-page applications.

With its easy learning curve and great documentation, Vue.js is one of the most popular and used web frameworks according to [the 2020 StackOverflow Developer Survey](https://insights.stackoverflow.com/survey/2020).

## Prerequisites

If you want to jump right into the code, you can check out the [GitHub Repo](https://github.com/lelouchB/vue-sanity-blog) and [the deployed version of the blog](http://vuesanity.netlify.app):

![Vue+Sanity Blog](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5dtjnouzccc77j80k4ug.png)

Before we get started, you should have:

1. Knowledge of [HTML, CSS, and JavaScript](https://www.freecodecamp.org/learn/responsive-web-design/)
2. Basic knowledge of [Vue.js](https://vuejs.org/)
3. [Node](https://nodejs.org/en/) and NPM installed on your local dev machine
4. [Vue Dev Tools](https://github.com/vuejs/vue-devtools) (optional)

## How to setup and install Vue.js

In this tutorial, we will use the official [Vue CLI](https://cli.vuejs.org/) to initialize your project. The CLI is the best way to scaffold Vue Single Page Applications (SPAs), and it provides batteries-included build setups for a modern frontend workflow. 

Run the following command in the terminal to install the Vue CLI globally.

```bash
npm install -g @vue/cli
```

Next, we'll scaffold our application.

```bash
vue create vue-sanity-blog
```

Select `Default (Vue 3 Preview) ([Vue 3] babel, eslint)` when prompted to choose the preset.

```bash
? Please pick a preset: 
  Default ([Vue 2] babel, eslint) 
❯ Default (Vue 3 Preview) ([Vue 3] babel, eslint) 
  Manually select features
```

We will use [Vue Router](https://router.vuejs.org/), the official router for Vue.js, for creating dynamic routes to the posts in this tutorial. Run the following command to install the [Vue Router](https://router.vuejs.org/) plugin.

```bash
vue add router
```

When prompted for the [history mode](https://router.vuejs.org/guide/essentials/history-mode.html#html5-history-mode), type `Yes` and hit Enter.

```bash
? Use history mode for router? (Requires proper server setup for index fallback in production) Yes
```

Run the following command in the project's root directory to start the development server.

```bash
npm run serve
```

Then we can open our app in the browser at [http:localhost:8080](http://localhost:8080).

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f8rx2iypkmppae2mklbv.png)

You can stop your development server now by hitting `Ctrl + C` in the terminal.

## How to set up Sanity

Sanity Studio is an open-source headless CMS built with React that connects to Sanity's real-time datastore. Sanity's datastore treats your content as data that's fully accessible via a robust API, that we'll use to integrate with Vue. 

To start a new project with Sanity, we'll install the [Sanity CLI](https://www.npmjs.com/package/@sanity/cli) globally. 

```bash
npm install -g @sanity/cli
```

The next step is to create a new project using the CLI. Run the following command inside your project's root directory (`vue-sanity-blog`).  

```bash
sanity init
```

If this is your first time creating a project using the CLI, you may also need to log into your Sanity account or create a new Sanity account in the browser first.

After this, you will be prompted to create a new project, hit **Enter**. Name your project **`vue-sanity-studio`** and choose the default dataset configuration.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e7vi17u5m7rbgszfb4x5.png)

Confirm your project's output path and choose `Blog (schema)` as the project template.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lfal9yqcgjwnqwlqsem0.png)

It is recommended to rename the folder `vuesanitystudio`  to `studio` on your local machine. 

You will also need to update the `browserslist` in the Vue.js project's `package.json`. Replace `not dead` with `not ie <= 8`.

```bash
"browserslist": [
        "> 1%",
        "last 2 versions",
        "not ie <= 8"
    ]
```

To start the Sanity Studio, run the following commands in the terminal after renaming the folder.

```bash
cd studio
sanity start
```

After compiling, the studio will open on [http://localhost:3333](http://localhost:3333/).  To start, the studio will have sections for posts, authors, and categories but no data.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9vfxo9w2s097dkiup63j.png)

Before adding any content to the studio, let's modify the default blog schema to include the post description in the content. 

Sometimes a title alone cannot express the core of the article entirely, having a good description or excerpt gives an insight about the post to the reader.

To update the `studio/schemas/post.js` file to include the description field, we need to add the following code after the `slug` field. You can see the entire code of `studio/schemas/post.js` [here](https://github.com/lelouchB/vue-sanity-blog/blob/main/studio/schemas/post.js).

```jsx
{
      name: "excerpt",
      title: "Description",
      type: "string",
      options: {
        maxLength: 200,
      },
},
```

To add our first post, we can click on the edit icon next to the project's name in our dashboard.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/suneu3e55wtl9w1e9dfk.png)

On the next prompt, choose **Post,** and an untitled page will appear with the fields for the post as defined in the schema we just edited.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/swbawz2zvltytuw176dt.png)

Create a sample blog article and author for our code to fetch.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/q6p2l459kadbim5m9w2h.png)

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/eq0a76ya9s8qt5vq4z52.png)

## How to connect Sanity with Vue.js App

We need to install few dependencies to connect Vue.js app to Sanity.

Run the following command in your project's root directory to install the **[Sanity Client](https://www.npmjs.com/package/@sanity/client)** and other packages you will need in the future.

```bash
npm install @sanity/client @sanity/image-url sanity-blocks-vue-component
```

- `@sanity/client` —   [Sanity Client](https://www.npmjs.com/package/@sanity/client) is the official JavaScript client by Sanity and can be used both in node.js and modern browsers.
- `sanity-blocks-vue-component` — Vue component for rendering block content from Sanity. You can learn more about this in the official [docs here](https://www.sanity.io/plugins/portable-text-to-vue).
- `@sanity/image-url` — A helper library to generates image URLs and perform helpful image transformations through Sanity asset pipeline. You learn more about this in the official [docs here](https://www.sanity.io/docs/image-url).

Once these packages are installed, we'll create a new file named `client.js` inside the `src` directory.

Add the following code to the `client.js` file.

```jsx
import sanityClient from "@sanity/client";

export default sanityClient({
  projectId: "Your Project ID Here", // find this at manage.sanity.io or in your sanity.json
  dataset: "production", // this is from those question during 'sanity init'
  useCdn: true,
  apiVersion: "2021-03-25"
});
```

This code configures the Sanity client with information about the specific project we're accessing.

You will also need to add the port where the Vue.js development server is running to the CORS origins of your Sanity project.

Head over to [https://www.sanity.io/teams](https://www.sanity.io/teams) and click on your Sanity project.  On your project's dashboard, click on **Settings → API settings** and then add [http://localhost:8080/](http://localhost:8080/) to the CORS origins field.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bfl8iqs72qv50rjwch38.png)

You can also use the Sanity CLI to add the CORS origins to your project. Run the following command in the `studio` folder of your project.  You can read more about this [here](https://www.sanity.io/docs/cors-in-cli).

```jsx
sanity cors add http://localhost:8080
```

## How to Display the Posts on the Homepage

Next, we need to fetch the data from Sanity and display the posts on our Vue.js app. For this, we need a function named `fetchData` and inside this function, fetch the data using the client we just configured, and then map over the response containing posts returned by Sanity.

Modify `src/Views/Home.vue` like this.

```jsx
<template>
  <div class="home">
    <h1>Welcome to your Vue + Sanity Blog</h1>
    <div class="posts">
      <div class="loading" v-if="loading">Loading...</div>
      <div v-if="error" class="error">
        {{ error }}
      </div>
      <div class="container">
        <div v-for="post in posts" class="post-item" :key="post._id">
          <router-link :to="`/blog/${post.slug.current}`">
            <h2>{{ post.title }}</h2>
          </router-link>
          <p>{{post.excerpt}}</p>
          <hr />
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import sanity from "../client";

const query = `*[_type == "post"]{
  _id,
  title,
  slug,
  excerpt
}[0...50]`;

export default {
  name: "Home",
  data() {
    return {
      loading: true,
      posts: [],
    };
  },
  created() {
    this.fetchData();
  },
  methods: {
    fetchData() {
      this.error = this.post = null;
      this.loading = true;
      sanity.fetch(query).then(
        (posts) => {
          this.loading = false;
          this.posts = posts;
        },
        (error) => {
          this.error = error;
        }
      );
    },
  },
};
</script>

<style scoped>
.home h1{
    text-align: center;

}
.container {
  margin: 0 auto;
  max-width: 42em;
  width: 100%;
}
.post-item {
  box-sizing: border-box;
}
</style>
```

Let's break down the above code piece by piece.

First, we need to import the client from the file we created in the last step.

```jsx
import sanity from "../client";
```

In this tutorial, you will use Sanity's GROQ API to query your data. GROQ, Graph-Relational Object Queries,  is Sanity's open-source query language. You can learn more about  [GROQ here](https://www.sanity.io/docs/overview-groq).

The following GROQ query is used to fetch the  `_id`, `title`, `slug`, and `excerpt` of posts from Sanity's backend. You can have thousands of posts but it doesn't make sense to display all of them on the homepage, hence the result is slice using `[0...50]`. This means that only the first 50 posts will be fetched.

```jsx
*[_type == "post"]{
  _id,
  title,
  slug,
  excerpt
}[0...50]
```

To execute the query, we'll create a `fetchData` function inside the `methods` object to request the data using `sanity.fetch()` and pass the `query` variable in it.

The fetched content is stored in the `posts` array using `this.posts=posts`.

```jsx
 fetchData() {
      this.error = this.post = null;
      this.loading = true;
      sanity.fetch(query).then(
        (posts) => {
          this.loading = false;
          this.posts = posts;
        },
        (error) => {
          this.error = error;
        }
      );
    },
  },
```

Inside the `template`, we map over the `posts` array using the `v-for` directive to display posts on the page. The `v-for` directive renders a list of items based on an array. You can read more about this directive [here](https://vuejs.org/v2/guide/list.html).

So that Vue's virtual DOM can differentiate between the different VNodes, we'll provide the `:key` attribute a value of our post's ID.

```jsx
<div v-for="post in posts" class="post-item" :key="post._id">
  <router-link :to="`/blog/${post.slug.current}`">
    <h2>{{ post.title }}</h2>
  </router-link>
  <p>{{post.excerpt}}</p>
  <hr />
</div>
```

The `<router-link>` component enables user navigation in a router-enabled app. The `slug` of the post is passed to its `to` prop. You can read more about this component [here](https://router.vuejs.org/api/#router-link).

```jsx
<router-link :to="`/blog/${post.slug.current}`">
  <h2>{{ post.title }}</h2>
</router-link>
```

Restart the development server using the `npm run serve` command and navigate to [http://localhost:8080/](http://localhost:8080/) in your browser. 

Here is how the app will look.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wf28rig74y3bnk9fx5yf.png)

We now have blog posts populating the homepage, but if you click on the post we created, it will take you to an empty page. This is because we have not yet created the routes for this post. 

## How to create dynamic routes for posts

To create a dynamic route, we'll create a new file named `SinglePost.vue` in the `src/components` directory.

Add the following code to `SinglePost.vue`.

```jsx
<template>
  <div>
    <div class="loading" v-if="loading">Loading...</div>

    <div v-if="error" class="error">
      {{ error }}
    </div>

    <div v-if="post" class="content">
      <h1>{{ post.title }}</h1>
      <img v-if="post.image" :src="imageUrlFor(post.image).width(480)" />

      <h6>By: {{ post.name }}</h6>
      <SanityBlocks :blocks="blocks" />
    </div>
  </div>
</template>

<script>
import { SanityBlocks } from "sanity-blocks-vue-component";
import sanity from "../client";
import imageUrlBuilder from "@sanity/image-url";

const imageBuilder = imageUrlBuilder(sanity);

const query = `*[slug.current == $slug] {
  _id,
  title,
  slug,
  body, 
 "image": mainImage{
  asset->{
  _id,
  url
}
},
"name":author->name,
"authorImage":author->image
}[0]
`;

export default {
  name: "SinglePost",
  components: { SanityBlocks },
  data() {
    return {
      loading: true,
      post: [],
      blocks: [],
    };
  },
  created() {
    this.fetchData();
  },
  methods: {
    imageUrlFor(source) {
      return imageBuilder.image(source);
    },
    fetchData() {
      this.error = this.post = null;
      this.loading = true;

      sanity.fetch(query, { slug: this.$route.params.slug }).then(
        (post) => {
          this.loading = false;
          this.post = post;
          this.blocks = post.body;
        },
        (error) => {
          this.error = error;
        }
      );
    },
  },
};
</script>

<style scoped>
.content {
  display: flex;
  flex-direction: column;
  margin: 0 auto;
  max-width: 42em;
}
h1 {
  text-align: center;
}
h6 {
  color: #aaa;
  padding: 1em;
}
</style>
```

In the above code, we use `imageUrlBuilder` from `@sanity/image-url` to generate image URLs for our images. To do this, we create a method called `imageUrlFor()` and use it inside `template`. We can chain additional methods onto this template tag to do things like specify width, height, or a crop. You can read more about `imageUrlBuilder` [here](https://www.sanity.io/docs/image-url).

```jsx
<img v-if="post.image" :src="imageUrlFor(post.image).width(480)" />
```

To fetch the data for a specific post we'll use its unique slug which is accessed using `this.$route.params.slug`. This is the `route` object present in Vue Router which represents the state of the current active route.  You can read more about Vue Router `route` object [here](https://router.vuejs.org/api/#the-route-object).

```jsx
sanity.fetch(query, { slug: this.$route.params.slug }).then(
  (post) => {
    this.loading = false;
    this.post = post;
    this.blocks = post.body;
  },
  (error) => {
    this.error = error;
  }
);
```

Another thing to notice here is `SanityBlocks`  component from `sanity-blocks-vue-component` package which renders an array of block content to Vue Components or Vue Nodes which is stored in `blocks`, passed inside the `blocks` prop of the component.

```jsx
<SanityBlocks :blocks="blocks" />
```

We also need to define this route in `router/index.js` file.

```jsx
const routes = [
  {
    path: "/",
    name: "Home",
    component: Home,
  },
  {
    path: "/about",
    name: "About",
    component: () =>
      import(/* webpackChunkName: "about" */ "../views/About.vue"),
  },
  {
    path: "/blog/:slug",
    name: "SinglePost",
    component: () => import("../components/SinglePost.vue"),
  },
];
```

 

In Vue Router, we create a [dynamic segment](https://router.vuejs.org/guide/essentials/dynamic-matching.html#dynamic-route-matching). This is denoted by a colon `:` as seen in the above code, `/blog/:slug`. Once this route is saved, you can navigate from the homepage to the blog post.

## How to add styles to the app

Our app works great but doesn't look as good as it could so update `src/App.vue` like this to include the global styles for your Vue app.

```jsx
<template>
  <div id="nav">
    <router-link to="/">Home</router-link> |
    <router-link to="/about">About</router-link>
  </div>
  <router-view />
</template>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  color: #2c3e50;
  display: flex;
  flex-direction: column;
  min-height: 100%;
  min-height: 100vh;
  padding: 1em 2em;
  width: 100%;
  margin-bottom: 8em;
}

#nav {
  text-align: center;

  padding: 30px;
}

#nav a {
  font-weight: bold;
  color: #2c3e50;
}

#nav a.router-link-exact-active {
  color: #42b983;
}
*,
*:before,
*:after {
  box-sizing: border-box;
}

h1,
h2,
h3,
h4,
h5,
h6 {
  font-family: Avenir, sans-serif;
  font-weight: 700;
  line-height: 1.2;
  margin: 0 0 0.5em 0;
}

h1 {
  font-family: Roboto, serif;
  font-size: 4em;
  margin: 0 0 1em 0;
}

h2 {
  margin: 1.6em 0 0 0;
  font-size: 1.8em;
}

h3 {
  font-size: 1.5em;
}

h4 {
  font-size: 1.4em;
}

h5 {
  font-size: 1.3em;
}

h6 {
  font-size: 1.2em;
}

p,
ul,
ol {
  font-size: 1.3rem;
  line-height: 1.75em;
  margin: 1.2em 0;
}

a {
  color: inherit;
  transition: color linear 0.15s;
}

a:hover {
  color: #42b983;
}

img {
  max-width: 100%;
}

hr {
  background-image: linear-gradient(
    to right,
    rgba(0, 0, 0, 0),
rgba(66, 185, 131, 1),    rgba(0, 0, 0, 0)
  );
  border: 0;
  height: 2px;
  margin: 40px auto;
}

blockquote {
  border-left: 4px solid #cccccc;
  font-size: 1.4em;
  font-style: italic;
  margin: 2rem 0;
  padding-left: 2rem;
  padding-right: 2rem;
}

.content h1 {
  font-size: 3em;
  margin: 1em 0;
}

@media (max-width: 1020px) {
  h1 {
    font-size: 3em;
  }

  .content h1 {
    font-size: 2.4em;
  }
}

@media (max-width: 480px) {
  body {
    font-size: 14px;
  }

  p,
  ul,
  ol {
    font-size: 1.2rem;
    margin: 1em 0;
  }
}
</style>
```

These are some basic styling for your app. You can experiment with different Vue.js UI component libraries like [Element](https://element.eleme.cn/#/), [Vuetify](https://vuetifyjs.com/en/), [BootstrapVue](https://bootstrap-vue.org/), etc. to style your app.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z2yrmthpk8nszba59ipd.png)

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ilsq4nqepn2mz3wu409e.png)

## Conclusion

In this article, we built a Vue.js app to function as a blog. We set up Sanity Studio as a headless CMS to power our Vue app. You can follow this tutorial and create your own unique version of this project with additional features and functionalities.

Here are a few ideas to get you started:

- Add `Author` routes and link them to the blog posts.
- Add an `SEO` component to the posts
- Add  `Filter`, `Sort`, and `Search` functionality.
- Style the app using UI libraries like [Vuetify](https://vuetifyjs.com/en/), [BootstrapVue](https://bootstrap-vue.org/), etc.

Here are some additional resources that can be helpful.

- [How to deploy Vue.js application](https://cli.vuejs.org/guide/deployment.html)
- [How to deploy Sanity Studio](https://www.sanity.io/docs/deployment)
- [vue-sanity -  Composition API methods to incorporate Sanity into a Vue project.](https://www.npmjs.com/package/vue-sanity)
- [Headless CMS explained in 2 minutes](https://www.sanity.io/blog/headless-cms-explained)
- [Get started with the Nuxt.js event website](https://www.sanity.io/guides/the-event-template)

Happy coding!